跳到主要内容

JSON 模式救不了你:生产环境 LLM 系统中的结构化输出故障

· 阅读需 11 分钟
Tian Pan
Software Engineer

当开发者第一次接入 JSON 模式时,响应结果感觉就像解决了一个大问题。LLM 不再返回 Markdown 围栏、文字道歉或靠近花括号的乱码。输出可以解析了,测试通过了,生产环境上线了。

然而,三周后,一个后台作业悄无声息地失败了,因为模型在 Schema 要求 {"status": "completed"} 时返回了 {"status": "complete"}。由于一个必填字段返回了 null 而不是被省略,数据流水线崩溃了。智能体工具调用循环(agent tool-call loop)提前终止,因为模型在字符串值中嵌入了一个异常换行符,导致下游解析器卡死。

JSON 模式保证了语法上有效的 JSON。它并不保证该 JSON 的含义与你的预期一致,不保证它包含你的应用程序所期望的字段,也不保证在多次请求之间保持语义一致性。这些是不同的问题,需要不同的解决方案。

没人提及的四个失败层级

生产环境中的结构化输出故障可分为四个不同的层级,每个层级都有不同的原因和缓解措施。大多数文档只涉及第一层。

第 1 层 —— 语法(Syntax)。 模型返回的根本不是可解析的 JSON:多余的逗号、不匹配的括号、字符串内部未转义的控制字符,或者被包裹在 Markdown 围栏中的有效 JSON 块。JSON 模式解决了大部分此类问题。现代供应商通过受限解码处理了其余部分。这一层在很大程度上是一个已解决的问题。

第 2 层 —— Schema 合规性。 模型返回了有效的 JSON,但不符合预期的 Schema:缺少必填字段、整数型字段包含字符串、枚举字段包含不在枚举中的值、嵌套对象具有非预期的键。JSON 模式对此无能为力。严格模式(Strict Mode)和受限解码确实可以解决这个问题,但存在局限性。

第 3 层 —— 语义有效性。 模型返回了符合 Schema 的 JSON,但其中的值在逻辑上不一致或存在事实错误。例如:end_date 早于 start_date 的日期范围;置信度分数为 0.97 却配有说明为“不确定”的推理字段;引用列表中的 URL 可以解析但指向了错误的域名。目前没有结构化输出 API 能捕获此类问题,因为它不是结构性问题。

第 4 层 —— 分布偏移(Distribution shift)。 模型在你的测试集上返回了符合 Schema 且语义连贯的 JSON,但在你未预料到的长尾生产输入中失败了。罕见的实体类型、多语言输入、格式异常的文档、边缘情况的数值 —— 这些都暴露了只有真实流量才能揭示的漏洞。

大多数团队为第 1 层构建验证,假设第 2 层由供应商处理,而对第 3 层和第 4 层没有任何检测手段。这就是静默失败(Silent Failures)存在的地方。

为什么受限解码不是银弹

受限解码(Constrained decoding)—— 即支撑 OpenAI 严格模式、Outlines、XGrammar 及类似工具的技术 —— 通过限制模型在每一步可以生成的 Token 来工作。如果 Schema 要求字段名为 status,解码器就会屏蔽所有不会产生该字符串的 Token。这使得 Schema 违规在结构上变得不可能,而不仅仅是不太可能发生。

这是一个真正的进步,但它引入了一个更微妙的问题:模型是按顺序生成 Token 的,每个 Token 都会影响后续 Token 的概率分布。当解码器强迫模型偏离其首选的下一个 Token 时,下游的生成可能会走偏。模型并不是故意要输出一个语义错误的值 —— 它是被迫离开了其最可能的路径,导致结果在结构上看起来正确,但在更难捕获的方面却是错误的。

研究文献对这一现象进行了量化。对于简单的扁平 Schema,质量下降可以忽略不计。对于具有许多受限字段的复杂嵌套 Schema,差距会扩大。受限开销也因引擎和 Schema 复杂度而异:简单的 Schema 增加 5-15% 的延迟,复杂的 Schema 可能增加 30-60%。递归 Schema 结构 —— 树、嵌套评论、自引用数据 —— 需要基于上下文无关文法(CFG)的引擎(如 XGrammar、llama.cpp 的 grammar 模式),而不是基于有限状态机(FSM)的工具;基于 FSM 的工具要么拒绝递归 Schema,要么将其静默扁平化到固定深度。

Schema 复杂度也存在天花板。OpenAI 严格模式对 Schema 的深度和广度都有实际限制。即使在这些限制之内,非常庞大的 Schema 也会通过增加受限解码压制模型首选 Token 的概率,从而降低输出质量。一个拥有 40 个字段、深度嵌套的可选结构和复杂联合类型的 Schema,不仅对解码器来说更难,对模型本身的生成也更难,因为它在生成过程中必须同时追踪大量的约束条件。

实践意义:应针对模型的生成过程来设计 Schema,而不不仅仅是针对你应用程序的类型系统。

Schema 设计即输出工程

改变你处理结构化输出方式的一个洞察是:LLM 是从左到右生成的。在序列化对象中,JSON Schema 的字段顺序在语义上没有意义,但在生成任务中却至关重要。模型在生成后续字段之前,已经对前面的字段做出了承诺。

这对 Schema 设计有直接影响。

将推理字段放在结论字段之前。 如果你的 Schema 包含 reasoning(推理)字段和 classification(分类)字段,请将 reasoning 放在前面。模型在确定答案之前会先进行分析。如果 classification 在前,模型必须先确定一个标签,然后再生成推理来证明其合理性 —— 这往往会产生“事后合理化”而非真正的分析,有时甚至会产生与已输出分类相矛盾的推理。

避免过早强制进行离散选择。 如果你的 Schema 包含一个有 20 个可能值的枚举(enum)字段,并且它是第一个字段,模型就必须在完整处理输入之前选择一个值。那些本可以为该选择提供信息的后续字段尚未生成。应将区分性字段移到 Schema 的后部,或增加一个中间推理步骤。

尽可能保持 Schema 扁平化。 每一层嵌套都会增加顺序约束。一个包含 10 个顶层字段的扁平 Schema 比一个包含 3 个嵌套对象(总共 10 个字段)的 Schema 更容易可靠地生成。嵌套对组织应用程序代码很有用,但对于可靠生成并不总是有益。

即使 Schema 强制执行了约束,也要在 Prompt 中描述它们。 受限解码(Constrained decoding)是在 Token 层面而非语义层面工作的。如果一个字段必须包含有效的 ISO 8601 日期,Schema 可以强制执行格式语法,但 Prompt 必须解释其含义。“start_date 字段必须是活动开始的日期,格式为 YYYY-MM-DD” 与 Schema 中的正则模式并不冗余 —— 它们是互补的,能够弥合 Token 层面约束与语义意图之间的差距。

真正行之有效的验证栈

鉴于供应商 API、受限解码和 Schema 强制执行都是必要但不充分的,生产系统需要一种分层的方法。

API 级强制执行 处理第 1 层和大部分第 2 层。在可用时使用严格模式(Strict Mode)或等效功能。这是基准,而不是解决方案。

库级验证 增加了 API 可能遗漏的 Schema 检查,并提供可以反馈给模型的结构化错误消息。像 Pydantic (Python) 或 Zod (TypeScript) 这样的工具让你能够定义验证器,不仅检查结构,还检查字段级语义 —— 日期范围、URL 格式、数值边界。当验证失败时,将错误序列化并附加到下一个请求中:“你之前的响应未通过验证,错误如下:[error]。请修复并返回正确的响应。” 这种方法对于一两次重试效果很好;除此之外,你通常面临的是更复杂的输入,无论如何重新提示都无法修复。

语义验证 捕获第 3 层失败。这需要针对你业务领域的自定义逻辑:日期一致性检查、字段间的引用完整性、业务规则强制执行。它无法从 Schema 中自动完成,并且应该在输出接触任何下游系统之前运行。

统计监控 发现第 4 层失败。将每个结构化输出与其输入一起记录。跟踪枚举值、数值范围、字符串长度和字段出现率的分布。当这些分布偏离你的开发基准时,说明你发现了一组你的 Schema 未曾设计的输入群体。这也是你检测供应商更新底层模型后出现的“隐性模型退化”的方法 —— 输出格式仍然有效,但数值分布发生了偏移。

大规模失败处理

在链式代理(chained agents)中,不可靠性的数学计算是残酷的。单个工具调用具有 97% 的 Schema 合规性听起来不错。但在一个包含 10 次工具调用的代理循环中,不发生任何验证失败而完成的概率是 0.97^10 ≈ 74%。如果是 20 个步骤,概率会降至 54%。这不是假设 —— 如果你没有在结构化输出强化上投入,这就是多步代理可靠性的实际天花板。

三种模式可以提高这个天花板:

带有错误反馈的重试。 在验证失败时进行一次重试,并将错误序列化到 Prompt 中,可以修复大多数模型犯下的可纠正错误。不要用相同的 Prompt 盲目重试 —— 模型通常会重复同样的错误。

默认值升级。 如果重试失败,返回一个类型化的默认值,并将请求路由到监控队列,而不是让系统崩溃。一个返回 {"action": "unknown", "confidence": 0.0} 的代理比抛出运行时异常的代理更有用,因为下游逻辑可以显式处理默认情况。

输入级 Schema 适配。 如果特定类型的输入始终无法通过复杂的 Schema,请考虑将单个复杂的提取任务拆分为一系列简单的提取步骤。简单的 Schema 失败率更低。两次顺序调用的额外延迟通常比一次复杂调用失败的代价更低。

“结构化”究竟意味着什么

结构化 LLM 输出中的核心张力在于,语言模型并不是结构化数据生成器——它们是下一个词元(next-token)预测器,只是可以被强制产生结构化数据。受限解码(Constrained decoding)和模式强制(schema enforcement)使这种强制手段更加可靠,但它们并不能消除底层的不匹配。

那些能够在结构化输出之上构建可靠系统的团队,并不是因为他们找到了完美的 API 参数。相反,他们将输出验证视为一等工程问题,而不是一个配置选项:为生成过程设计模式(schema),对所有四个层级的失败进行埋点监测,并在需要之前就构建好回退路径。

JSON 模式解决了解析错误。剩下的工作需要由你来构建。

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