跳到主要内容

生产级 LLM 系统中结构化输出的可靠性

· 阅读需 12 分钟
Tian Pan
Software Engineer

你的 LLM 管道在测试中达到了 97% 的成功率。但在它发布后,在实际使用的长尾场景中,JSON 解析失败会静默地损坏下游状态,缺失字段会在三步之后导致空指针异常,或者包裹在 Markdown 代码块(fences)中的响应会在凌晨 2 点破坏你的提取逻辑。结构化输出失败是生产级 AI 系统中鲜为人知的可靠性杀手——它们很少出现在基准测试中,但在多步管道中会无形地累积,而且只要你理解了问题的核心,它们是完全可以避免的。

令人不安的事实是:在生产环境中,简单的 JSON 提示词(prompting)失败率高达 15–20%。对于一个每天进行 1000 次 LLM 调用的管道来说,这意味着 150–200 次静默失败。由于这些错误通常不会立即显现——它们作为格式错误的数据而非异常向前传播——它们是检测和调试难度最高的一类 Bug。

为什么 LLM 会破坏 JSON

LLM 经过训练,可以预测自然语言文本中的下一个 token。而 JSON 是一种形式语法。这种不匹配比看起来更根本。

当模型生成 JSON 响应时,输出的正确性是每个单独 token 决策正确性的乘积。一个在单 token 准确率为 99% 的模型上运行、生成 200 个 token 的 JSON 对象时,产生有效结果的概率大约只有 87%——这甚至还没考虑到模式合规性(schema compliance)。在单 token 准确率为 98% 时,这一比例会下降到 70%。错误率不是相加,而是相乘。

你在生产中遇到的具体失败模式是可预测的:

语法故障 (Syntax failures) 是最常见的:混合引号样式(在 Python 字典上训练的模型倾向于使用单引号)、尾随逗号(有效的 JavaScript,但无效的 JSON)、未加引号的键,以及散落在实际对象之前的解释性文本——“当然!这是你要求的 JSON:”——出现在实际对象之前。字符串中间的 token 截断尤其棘手,因为输出看起来大部分是正确的。

模式合规性故障 (Schema compliance failures) 则更为微妙。大多数供应商实现的 JSON 模式(JSON mode)保证生成有效的 JSON,但不保证该 JSON 符合你的模式(schema)。当模型不确定使用什么值时,必需字段会被遗漏。类型会出现错误——数字 ID 被返回为字符串,数组字段被返回为单个对象。深层嵌套结构受损程度不成比例:当嵌套超过三或四层时,失败率呈非线性增长,因为模型必须在越来越长的上下文窗口中保持结构的一致性。

幻觉结构 (Hallucinated structure) 是最容易悄无声息地破坏系统的失败模式。模型返回了有效且符合模式的 JSON,但字段名为 analysis_result 而不是 analysis,因为在提示词上下文中这看起来更合理。你的代码成功解析了它,但却静默地丢弃了你实际需要的数据。

早期版本的 GPT-4 在仅使用提示词时,其模式合规性不足 40%。新模型表现更好——在启用了原生模式强制执行的结构化输出基准测试中达到了 85% 以上——但这种改进源于机制的改变,而不是因为模型在处理 JSON 方面变得更聪明了。机制才是关键所在。

受限解码:正确的思维模型

解决结构化输出不可靠性的结构性方案是受限解码(constrained decoding)——这是一种修改 token 生成过程本身的技术,而不是寄希望于模型产生有效输出后再进行修复。

其核心工作原理如下:在每个生成步骤中,语言模型都会为其词表(vocabulary)中的每个 token 分配概率。通常情况下,模型会自由地从该分布中采样。而在受限解码下,系统会根据目前为止生成的局部输出,计算哪些 token 根据模式或语法是有效的。违反约束的 token 其概率会被设为零。模型仍然利用其学习到的分布来选择哪一个有效的 token 作为下一个输出,但它在物理上无法发射一个无效的 token。

该实现使用能够表达 JSON、SQL、正则表达式和任意编程语言的上下文无关语法。生产系统会将模式预编译为有限状态机(finite state machines),每步 token 查找的时间复杂度为 O(1),相对于模型的推理时间,其开销几乎可以忽略不计。像 Outlines 这样的库(已被 100 多家组织使用,并集成到 vLLM、TGI 等主流推理框架中)为自托管模型实现了这种方法。

关键的洞察在于受限解码做什么:它不会限制模型的推理或领域知识。模型仍然根据它认为正确的答案来挑选 token。它只是被防止选择了那些会产生结构无效输出的 token。这就是为什么受限解码在消除结构故障的同时能够保持输出质量——你并不是在惩罚模型的智能,而是在为其语法设立护栏(guardrailing)。

对于使用云端 API 的团队,供应商已将受限解码内置到其原生的结构化输出功能中。当你向 OpenAI 的 response_format 传递 type: "json_schema"strict: true,或者向 Anthropic 的 API 传递 tool_use 响应类型时,供应商会在服务器端运行等效的机制。使用原生模式强制执行的工具调用失败率接近于零。如果你仍在使用 JSON 模式(它保证 JSON 有效但不保证模式合规),那么你错失了显著提升可靠性的机会。

模式设计即可靠性工程

在考虑约束解码或重试逻辑之前,先优化你的模式(Schema)。模式设计是最高杠杆率的可靠性干预手段,因为设计不当的模式即使在完美执行的情况下也会出问题 —— 模型可能会生成符合模式但语义错误的输出。

保持嵌套层级较浅。 最多两到三层。深层嵌套的模式更容易失败,且在失败时更难调试。如果你发现自己需要第四层嵌套,这通常是一个信号,提示你应该重构模式,而不是增加更多的重试次数。尽可能扁平化嵌套对象,或将复杂的模式拆分为多个更简单的调用。

将字段描述作为提示词。 在 JSON Schema 中,字段描述会成为发送给模型的模式的一部分。它们直接影响模型的生成内容。一个名为 sentiment 且没有描述的字段将根据模型对情绪的理解进行填充。而一个名为 sentiment 且描述为 "客户情绪:基于消息的明确语气而非隐含意图,分为正面(positive)、负面(negative)或中立(neutral)" 的字段,其产出结果的一致性会大幅提升。这是嵌入在类型系统中的提示词工程(Prompt Engineering)。

将推理置于答案之前。 如果你需要一个 reasoning 字段和一个 answer 字段,请在模式中将 reasoning 排在前面。模型是按顺序生成字段的,一个在给出答案之前先进行推理的模型会产生更好的答案。这是在模式层级而非提示词层级实现的思维链(Chain-of-thought)—— 而且它更可靠,因为模型无法跳过推理步骤。

明确必填字段。 模式中包含某个字段与将其标记为 required 之间的区别,在于模型是“有时包含”还是“始终包含”。务必列举你的必填字段。不要依赖模型去推断省略字段即代表失败。

验证-重试循环

对于云端 API 用户,Instructor 库(每月下载量超过 300 万次)实现了一种处理约束解码无法触及的情况的模式:即输出结构正确但语义错误的语义校验失败。

该模式非常简单:将你的输出模式定义为带有校验器(Validators)的 Pydantic 模型,调用模型,校验响应,如果校验失败,则将校验错误作为反馈连同重试提示词一起发送给模型。模型会看到自己的错误以及具体的错误信息,并进行修正。

import instructor
from pydantic import BaseModel, field_validator

class ExtractedData(BaseModel):
entity_name: str
confidence: float

@field_validator('confidence')
def confidence_must_be_valid(cls, v):
if not 0.0 <= v <= 1.0:
raise ValueError('置信度必须在 0 到 1 之间')
return v

client = instructor.from_openai(openai_client)
result = client.chat.completions.create(
model="gpt-4o",
response_model=ExtractedData,
messages=[{"role": "user", "content": "从以下内容中提取实体:..."}]
)

Instructor 会自动处理重试循环,将 Pydantic 校验错误连同出错背景反馈给模型。模型随后会返回修正后的响应。

两个重要的运维注意事项:首先,监控你的重试率。如果一个提示词经常触发两次或更多次重试,那么问题出在提示词或模式上,而不是模型 —— 这时应该添加更多上下文、简化模式或增加字段描述。重试逻辑应该处理边缘情况,而不是支撑起根本上模糊不清的提示词。其次,设置重试限制。无限重试循环是可用性风险;将上限设定为两到三次尝试,并在达到上限时将失败情况上报给监控系统。

大规模故障检测

即使使用了约束解码和校验,结构化输出的失败仍会发生。能够快速捕捉失败的团队与通过用户投诉才发现问题的团队,其区别在于是否进行了监测(Instrumentation)。

跟踪三个指标:

模式校验失败率 —— 在任何重试之前,LLM 调用中模式校验失败的百分比。这是你的结构可靠性信号。在使用约束解码的情况下,该指标应低于 1%;如果更高,说明你的模式或提示词存在问题。

重试率 —— 为了产生有效输出而至少需要一次重试的调用百分比。非零的重试率是正常的;但上升的重试率则预示着漂移(模型更新、提示词变更或与实际流量分布冲突的模式变更)。

下游数据质量 —— 被下游系统标记为异常或触发人工审核的处理记录百分比。在 LLM 边界捕获的结构性失败很容易处理。只有在这里,那些产生结构正确但内容错误的语义失败才会暴露出来。

针对前两个指标设置告警,并对第三个指标进行抽样检查。当重试率激增时,提取触发重试的特定输入并进行分析 —— 它们通常是类似的边缘情况簇,你可以通过针对性的模式或提示词改进来解决。

在处理结构化输出可靠性方面最吃力的团队,通常是那些将其视为模型问题的团队。当 JSON 报错时,本能反应是更换模型或围绕 JSON 指令增加更多的提示词工程。真正的修复手段是架构性的:通过约束解码保证结构,通过显式的模式设计保证语义可靠性,以及通过校验循环处理残余的长尾问题。模型本身很少需要更换。

生产环境技术栈

对于自托管模型(vLLM、TGI、llama.cpp),请在服务层使用 Outlines 或等效库启用语法约束采样(grammar-constrained sampling)。将你的 JSON schema 作为语法约束传入,服务框架会处理剩下的工作。对于大多数工作负载,运行时开销微乎其微。

对于云端 API,请使用原生的结构化输出功能——例如 OpenAI 的 response_format 并设置 strict: true,以及 Anthropic 和 Google 的基于 Pydantic 模型的工具调用(tool use)。配合 Instructor 库使用可以实现 Pydantic 集成和自动重试处理。

无论采用哪种部署模型,都要强制执行三条 Schema 设计规则:嵌套层级最多两到三层、为每个不显而易见的字段提供字段描述,以及将推理字段置于答案字段之前。即使 API 保证了 Schema 合规性,也要在应用层进行验证——语义验证可以捕捉到结构强制约束所遗漏的问题。

目标并非实现零故障。目标是在流水线中尽早捕捉故障,并提供足够的信号来修复根本原因,防止故障蔓延到纠错成本极高的状态。这就是结构化输出的可靠性工程。

References:Let's stay in touch and Follow me for more thoughts and updates