跳到主要内容

结构化输出并非已解决的问题:生产环境中的 JSON 模式失效模式

· 阅读需 14 分钟
Tian Pan
Software Engineer

你开启了 JSON 模式,你的 LLM 开始返回有效的 JSON,然后你发布了它。三周后,生产环境悄无声息地挂了。JSON 在语法上是有效的。Schema 在技术上也是满足的。但某个字段包含了一个虚构的实体,finish_reason"length" 导致数据负载在 95% 处被静默截断,或者模型对任何人类读起来都感到刺耳的文本分类为 "positive" 情感——而你的下游流水线毫无怨言地吞下了它。

JSON 模式被解决的方式,就像“使用互斥锁(mutex)”解决并发问题一样。原语(primitive)是存在的。但故障模式并不在于你把锁放在哪里。

这是一份针对生产环境中结构化 LLM 输出的全面故障分类,以及在用户发现之前捕获损坏的验证模式。

结构化 LLM 输出的三个时代(以及每个时代的失败之处)

了解故障模式需要了解你的集成方案处于哪个时代。

提示词工程时代 (2020–2023): 你在系统提示词中加入了“输出有效的 JSON”并提供了一个 few-shot 示例。失败率在 5–20% 之间,具体取决于 Schema 的复杂程度。最常见的失败是前导词污染(preamble contamination)——模型输出“当然!这是你要求的 JSON:”然后才是 JSON,导致生成的负载无法被任何解析器干净地处理。

JSON 模式时代 (2023–2024): OpenAI 的 response_format: { type: "json_object" } 保证了语法上有效的 JSON——任何有效的 JSON,而不一定是你要的结构。原始解析的失败率降至 2–5%。但模型现在可以随意返回 {"data": "whatever"},即便你想要的是一个包含十几个必填字段的精心结构化的对象。你用解析失败换取了静默的 Schema 不匹配。

严格结构化输出时代 (2024–至今): OpenAI 在 2024 年 8 月引入了严格的 Schema 强制执行。Google Gemini 增加了 response_schema。Anthropic 在 2025 年底发布了原生结构化输出。所有主要供应商对于语法和结构无效的 JSON 故障率降至 0.3% 以下。这就是大多数团队放松警惕的地方,也是大多数生产问题的潜伏之处。

故障分类

1. 静默截断

这是最危险的故障模式,因为它看起来像是成功的。当 max_tokens 在生成中途耗尽时,模型停止产生 token。finish_reason"length"。输出是一个不完整的 JSON 对象——缺少闭合括号、字符串中途断开、数组被截断。大多数供应商仅保证完整生成的 Schema 一致性。如果模型在 2,000 字符负载的第 1,847 个字符处用完了配额,你将得到一个无效的 JSON 对象,且没有报错。

解决方法并不复杂:在尝试解析之前始终检查 finish_reason。如果是 "length",不要解析——使用更高的 max_tokens 配额重试,或者将任务分解为更小的调用,每个调用产生一个更小的负载。操作规范是难点;首版集成代码中很少包含这种检查。

2. 绕过 JSON 模式的结构违规

JSON 模式保证的是语法,而不是语义或 Schema 遵守。在 JSON 模式(非严格)下,以下内容都将作为有效的 JSON 返回并毫无错误地通过:

  • 幻觉键:模型在你的 Schema 定义为 "status" 时返回了 "current_state"
  • 类型漂移:返回 "42"(字符串)而不是 42(整数)
  • 缺失必填字段:Schema 要求的键不存在
  • 在数据缺失时使用空数组而不是 null,或者在你的代码期望空数组的字段中使用 null

严格的 Schema 强制执行消除了大部分此类问题——但仅适用于实际支持它的供应商和模型版本。OpenAI 严格结构化输出(Strict Structured Outputs)需要 gpt-4o-2024-08-06 或更新版本。Anthropic 原生结构化输出需要带有 beta 标头的 Sonnet 4.5 或 Opus 4.1。如果你使用的是旧模型或仅提供 JSON 模式的供应商(如 Mistral),你仍处于 JSON 模式时代。

3. Schema 复杂度故障

受限解码(Constrained decoding)——在 token 级别强制执行 Schema 的机制——在复杂的 Schema 上会崩溃。崩溃的程度取决于后端:

对 10,000 个真实 Schema 的多个框架进行的基准测试显示,在复杂输入上的覆盖率差异巨大。Outlines(一个广泛使用的受限解码库)在复杂的 GitHub Schema 上仅达到 3% 的覆盖率,因为 Schema 编译超时(最糟糕的情况下从 40 秒到超过 10 分钟不等)。相比之下,Guidance 达到了 96% 的覆盖率,编译时间约为 0.01 秒。

OpenAI 的严格模式将 Schema 深度限制在 5 层,且不支持递归 Schema。Google Gemini 对于超过内部复杂度阈值的 Schema 会返回 InvalidArgument: 400,而 API 层面并没有明确记录这些阈值。

对生产环境的影响:你的 Schema 在开发阶段运行良好,只有 8 个字段和 2 层嵌套。六个月后,你增加了一个带有嵌套数组的对象,而这些对象本身又有嵌套数组,受限解码会静默退回到非受限生成或超时。

4. 推理性能下降问题

这是 2024 年的一项研究 (Tam 等人, "Let Me Speak Freely?", EMNLP 2024) 最直接量化的失败模式。将 JSON 模式应用于推理任务会导致准确率大幅下降:

  • GPT-3.5-Turbo 在 GSM8K 数学基准上:自由格式为 76.6%,JSON 模式为 49.3% —— 下降了 27 个百分点。
  • Claude-3-Haiku 在同一基准上:自由格式为 86.5%,JSON 模式为 23.4% —— 下降了 63 个百分点。
  • LLaMA-3-8B:自由格式为 74.7%,JSON 模式为 48.9%。

其机制在于,受限解码(constrained decoding)迫使模型选择符合 JSON 结构的 token,即使自由推理本会引导至不同的路径。模型需要同时处理推理链并满足语法约束,两者会相互干扰。

一个违反直觉的发现是,分类和提取任务在 JSON 模式下反而会有 提升。Gemini-1.5-Flash 在一项诊断分类任务中,当强制执行结构化输出时,准确率提升了 18 个百分点。当任务是“从有效值空间中选择正确的值”时,该模式很有帮助;而当任务需要开放式的中间推理时,该模式则有害。

实际的解决方法:永远不要在受 JSON 约束的生成过程中进行推理。采用两步法 —— 第一步进行自由格式推理,第二步再进行独立的受限格式化调用。

5. 语义偏移:Schema 验证无法捕获的故障

一旦你解决了语法和结构问题,剩下的就是最难的问题:结构有效但语义错误的 JSON 值。

情感分类器对一段明显的负面评论返回 "positive" —— 枚举值有效,但答案错误。实体提取器返回了一个在源文本中从未出现的组织名称 —— 字符串有效,但纯属幻觉。一个在 10 月份表现良好的分类模型,在服务商更新模型后的 1 月份开始出现标签分布偏移 —— 输出有效,准确率下降,但没有错误信号。

Schema 验证无法捕获这些问题。finish_reason 检查也无法捕获。它们需要一个独立的监控层:标签值的分布跟踪、LLM-as-judge 抽样,或人工抽检流程。那些止步于 Schema 强制执行并宣称结构化输出问题已解决的团队,往往是在模型更新六周后,从用户投诉中才发现语义偏移。

6. 拒绝回答(Refusals)作为一种新的故障模式

严格结构化输出(Strict structured outputs)引入了一种在仅靠提示词或 JSON 模式集成中不存在的故障模式:refusal 字段。当请求触发服务商的安全过滤器时,严格模式不会返回 JSON 负载或错误,而是返回一个带有 refusal 字段且 parsed 字段为 null 的对象。如果生产代码只处理成功情况而不处理拒绝路径,将会导致空指针错误或类型错误,而不是返回有用的信息。

务必同时处理这两个分支:生成结构化输出时的解析情况,以及安全过滤器介入时的拒绝情况。在实践中,拒绝回答是 OpenAI 严格结构化输出中低于 0.1% 故障率的主要原因 —— Schema 强制执行本身几乎是完美的,真正的边缘情况是生产代码中未预料到拒绝路径。

真正关键的 Schema 设计决策

通过一些设计规范,大多数 Schema 层面的问题都是可以避免的。

将嵌套深度保持在两到三层。 当嵌套深度超过三到四层时,错误会集中出现,且受限解码的复杂性会随嵌套深度增加。如果你的领域模型天然具有更深的嵌套,请在 Schema 层将其扁平化,然后在客户端重建层次结构。

将推理字段放在答案字段之前。 如果你的 Schema 包含 "reasoning" 字段和 "conclusion" 字段,将推理字段排在前面会让模型在确定答案之前先推导出问题。如果将其排在最后,结论 token 会在支持它的思维链产生之前就被采样。

将字段描述作为字段级的指令。 Schema 中的 description 属性是模型上下文的一部分。“描述发生了什么”和“仅根据明确陈述而非暗示的情感,将情感分类为 positive、neutral 或 negative 之一”都是有效的描述 —— 后者是一个指令,相比于埋在冗长的系统提示词中的等效指令,模型在字段级别遵循它的可靠性更高。

尽量减少必填字段。 每个必填字段都是一个潜在的失败点 —— 即使源数据中不包含某个值,模型也必须生成一个。对于可能缺失的数据,请使用带有默认值的可选字段。对于必须填写但可能没有自然值的字段,请定义一个明确的 "unknown""not_found" 枚举成员。

分解超过 8 到 10 个字段的 Schema。 具有多个相互依赖项的大型 Schema 更难可靠地满足。两次各有 4 个字段的专注调用,其结果比一次包含 10 个字段的调用更可靠,代价仅是额外的一次往返和略高的 token 消耗。

验证-重试循环:生产环境的真实需求

即使有了严格的结构化输出,你仍然需要客户端校验。原因如下:

  • 并非所有供应商的所有模型版本都支持严格模式 (strict mode)
  • 供应商对 Schema 特性的支持并不完整,且随时间变化
  • 截断 (Truncation) 会破坏长文本生成的 Schema 保证
  • 语义验证(例如:实体必须存在于源文本中,日期必须是未来的)超出了 JSON Schema 的表达能力

标准的生产模式是“验证-重试”:生成 → 解析 JSON → 使用完整的 Pydantic 或 Zod 进行 Schema 验证 → 失败时,将具体的验证错误信息嵌入回重试提示词中 → 重试两到三次。将重试率作为核心质量 KPI 进行跟踪。0.5–2% 的基准重试率是正常的;如果飙升至 5–10%,则预示着提示词偏移 (prompt drift)、模型更新或 Schema 变得难以满足。

像 Instructor(月下载量超过 300 万次)这样的库自动化了这一循环:将你的 LLM 客户端调用包装在一个 Pydantic 模型中作为目标类型,该库会捕获 ValidationError 并在提示词中嵌入错误信息进行重试。重试信息会告知模型哪里出错了,这通常能通过额外一次尝试解决失败。

关于重试循环有两个注意事项。首先,重试会使受影响请求的延迟和 API 开销翻倍 —— 请在你的 SLA 中为此做好预算。其次,100% 的重试率不是解决方案。如果你的 Schema 在特定字段或特定输入模式上总是失败,重试虽然能提高单次调用的可靠性,但会掩盖需要通过设计修复(而非循环)来解决的系统性问题。

监控解析阶段无法捕捉的故障模式

结构化输出的生产监控体系需要三个层级:

第 1 层 —— 解析和 Schema 合规性。 记录每一个 finish_reason,分别跟踪 JSON 解析失败和 Schema 验证失败,监控每个端点的重试率。对任何解析失败的激增进行告警。这一层是基本功。

第 2 层 —— 输出分布监控。 对于分类输出,跟踪枚举值随时间的变化分布。如果一个分类器的五个类别原本分布均匀,突然 80% 偏向其中一个类别,那么即便输出结构有效,也很可能发生了偏移。将滚动窗口与稳定的基准期进行对比。

第 3 层 —— 语义正确性抽样。 将一定比例的结构化输出路由到“LLM 作为裁判 (LLM-as-judge)”或人工抽检流程中,评估提取的内容是否真实存在于原始材料中,或者给定的分类在输入背景下是否合理。这一层是唯一能捕捉到结构有效但内容幻觉的情况。

大多数团队在初始构建期间只实现了第 1 层并觉得大功告成。而第 2 层和第 3 层则是区分“在内部监控中发现语义偏移的团队”与“在用户投诉中才发现问题团队”的关键。

实用决策矩阵

根据你实际想要保证的目标来选择方案:

如果你需要保证 JSON 语法正确且没有特定的 Schema:大多数供应商的 JSON 模式 (JSON mode) 就足够了。跟踪解析失败并实现重试循环。

如果你需要保证 Schema 一致性且 Schema 较浅(两层以内,少于 8 个字段,小型枚举):在支持的模型版本上使用严格的结构化输出。明确处理拒绝响应的情况。

如果你的任务在产出结果前需要推理:使用两步走的方法。自由格式生成用于推理,受限生成用于格式化。如果准确性至关重要,永远不要在 JSON 受限生成过程中运行复杂的推理任务。

如果你的 Schema 层次深或复杂:在投入生产前,针对你所使用的特定受限解码后端测试你的 Schema。一个在 Guidance 上 0.01 秒就能编译的 Schema,在 Outlines 上可能会超时 40 秒。必要时进行分解。

如果你使用的供应商不支持严格的 Schema 强制执行:将所有输出视为未经验证,在客户端使用 Pydantic 模型进行校验,并在失败时重试。根据供应商和 Schema 的复杂度,预留 2–12% 的重试率预算。

结构化输出确实显著提升了开发体验,优于仅靠提示词的 JSON 提取。但从解析失败转向语义偏移,并不是故障减少了,而是故障隐藏的位置变了。理解完整分类体系的团队,才能在用户发现之前找到故障。


延伸阅读:工具结果验证差距 —— 为什么智能体(Agent)会盲目信任工具输出,以及如何在 LLM 进行推理之前捕获异常响应的验证层。

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