用 JSON Schema 约束模型输出,边界在哪
让模型返回 JSON,然后 JSON.parse —— 这是最容易翻车的地方。模型会在 JSON 前后加解释,会在字符串里塞未转义的引号,会在数组末尾多一个逗号。
现在的模型大多支持结构化输出(response_format 或工具调用),能保证返回合法 JSON。但"合法"不等于"符合你的业务预期"。
它能保证什么
- 返回一定能被解析成 JSON
- 你定义的必填字段一定存在
- 枚举字段一定是枚举里的值
- 数字字段一定是数字类型,不是字符串
这四条已经解决了绝大部分解析崩溃。
它不能保证什么
一、内容质量
字段存在,但值是空的、是无意义的填充、或者跟原文不符。schema 管不了这个,只能靠提示词和评估。
二、长度限制
你写 description: 不超过 50 字,模型可能给你 120 字。schema 层面没有长度约束的强制力。工程上要在拿到结果后截断或重新请求。
三、嵌套深度
嵌套三层以上的 schema,模型出错率明显上升。我们试过五层嵌套的配置对象,字段错位的情况经常出现。现在的原则是最多两层,更深的拆成多次请求。
四、数组元素的一致性
要求返回 10 个对象,每个对象有相同的键。实际经常出现前 8 个有 reason 字段,后 2 个没有。这不是 schema 的问题,是模型在长输出后期"忘了"。
解决办法是把数组拆小:一次最多 5 个,分批请求。虽然多了几次调用,但稳定性提升明显。
一个实际的设计取舍
我们有个抽取任务,要从合同里抽出甲乙方、金额、期限、违约条款。最早设计成一个对象,一次抽完。
问题是一份合同十几页,模型经常漏字段。后来改成分三次请求:主体信息、金额与期限、条款摘要。每次只关注一小块,准确率从 72% 提到 91%。
代价是三次调用的成本,但比起人工返工,这笔账划算。
兜底还是要写
即使有结构化输出,解析失败也要有兜底。我们的做法是:解析失败 → 用更小的模型重试一次(小模型在简单 schema 上反而更稳)→ 还失败就转人工队列。
不要写"重试三次还失败就抛异常",用户看到的应该是"这条需要人工确认",不是 500 错误。