跳到主要内容

语义验证层:为什么 JSON Schema 不足以应对生产环境中的 LLM 输出

· 阅读需 12 分钟
Tian Pan
Software Engineer

到 2025 年,每家主流 LLM 服务商都已推出结构化输出的受约束解码功能。OpenAI、Anthropic、Gemini、Mistral——它们都允许你向模型传入一个 JSON Schema,并保证返回结果在结构上完整无误。各个团队纷纷采用这一功能,长舒一口气:解析错误消失了,重试循环缩短了,监控面板一片绿色。

然后,微妙的故障开始出现。

一个情感分类器在两周内对每个输入——包括乱码——都锁定在 0.99 的置信度,无人察觉。一个信贷风险智能体返回了合法的 JSON,批准了一笔本应被拒绝的贷款申请,风险分数高出了五十分。一条金融数据管道将 "$500,000"(字符串,技术上符合 Schema)强制转换为整数字段中的零,破坏了六周的风险计算数据。这些故障全部通过了 Schema 验证。

教训是:结构有效性是必要条件,但并不充分。你需要一个语义验证层,而大多数团队并没有这一层。

结构与语义的鸿沟

受约束解码的工作原理是将你的 JSON Schema 编译成一个有限状态机,在生成时屏蔽无效的 token。模型在字面意义上无法生成违反 Schema 的输出。这是真正的工程成就,消除了整整一类故障——那种在凌晨三点以 JSONDecodeError 形式出现的错误。

但它无法消除语义上的错误。一个类型为 number、范围为 [0, 100] 的字段,其值始终会是 0 到 100 之间的数字。然而这个数字仍可能以任何类型检查器都无法检测到的方式出现错误:置信度分数冻结在 0.99,风险分数反映的是错误的风险画像,年龄限制服务中年龄字段包含 3。输出在形式上符合合约,只是其含义并不符合预期。

基准测试数据使这一点更加具体。对生产环境 LLM API 调用的研究发现,语义参数错误——结构有效但值违反业务语义——在前沿模型中高达 16.83%,在其他模型中超过 27%。这些故障在结构层面是不可见的。没有 Schema 验证器能捕获它们,这也意味着不会触发自动重试,它们在无声无息中不断积累。

还有一个额外的麻烦:受约束解码会带来"格式税"。最新基准测试显示,通过受约束解码强制输出 JSON 平均会使推理质量下降 3-9 个百分点,在难度较高的数学基准测试中甚至下降高达 12.7 个百分点。当你强制模型在语法约束内生成 token 时,它同时需要做到推理正确并满足 Schema 要求。这两个目标并不总是一致的,结构正确性可能以语义正确性为代价。

语义故障的分类

在构建防御措施之前,先给各种故障模式命名会有所帮助。

必填字段中的自信幻觉。 当 Schema 要求某个字段必须存在且非空时,一个没有底层知识的模型会编造一个看似合理的值,而不是表达不确定性。从形状上看,这个输出与正确输出无法区分。这是一起广为人知的法律事故中的故障模式——虚构的法庭案例引用被格式化得与真实引用一模一样。

分布冻结。 分类器或评分系统返回有效值,但这些值停止了变化。置信度分数在每个输入上都是 0.99,包括乱码。上游模型更新后,情感分类器把所有内容都标记为正面。Schema 验证通过;而分布监控本可发现这一问题。

跨字段逻辑不可能。 结束日期早于开始日期。items_count 字段报告 47,而 items 数组只有 1 个条目。入职日期晚于离职日期。0.01 美元订单上的运费却是 500 美元。每个字段单独看都是有效的,但它们的组合在语义上不连贯。

枚举漂移。 当你的枚举定义 "ORG" 时,模型返回 "ORGANIZATION"。当 Schema 要求 "Swedish" 时,返回 "swedish"。返回一个不在允许列表中的合理类别同义词。这些故障可能很微妙,开发者只在下游字符串比较开始返回意外结果时才会注意到。

貌似合理的空值。 一个感觉必填的可选字段返回 null 或空字符串。应用程序静默地使用默认值。数据不断积累,存在一个没有任何监控浮出水面的缺口,直到数周后依赖该字段的查询返回错误结果。

类型强制掩盖。 Pydantic 在默认模式下会静默地将 "4" 强制转换为 4。约束 Field(gt=0) 在强制转换前无法捕获作为字符串传入的 "0"。类型系统给你一种虚假的安全感,因为验证器从未以原始形式看到病理性的值。

双层架构

在生产环境中经得起考验的解决方案如下所示:

第一层:结构验证。 这是受约束解码和 JSON Schema 提供的内容。字段名称、类型、必填存在性、token 级别的枚举成员资格。这里的目标是格式合规——使输出可解析且结构可预测。对于开放权重模型,使用 OpenAI Structured Outputs、Outlines 或 Guidance;在应用层使用 Pydantic 类型提示或 Zod 进行强制执行。

加载中…
References:Let's stay in touch and Follow me for more thoughts and updates