Structured Outputs 生产笔记:三个静默失效点
Structured Outputs(结构化输出)解决的不是“模型能不能输出 JSON”,而是输出能否稳定满足字段、枚举、嵌套、必填这些结构约束。支付回调字段解析、工单分类、表单抽取这类把 LLM 结果直接喂给下游代码的场景,它比传统 JSON mode 可靠两个数量级;但 schema 写得不够严谨时,会以很隐蔽的方式静默失效。
记录三个真正会坑到人的点,以及为什么后端校验一层都不能省。
一、先分清 JSON mode 和 Structured Outputs
JSON mode 只保证合法 JSON;Structured Outputs 进一步保证符合 schema。
启用方式:
JSON mode:
response_format:{type:"json_object"}
Structured Outputs:
response_format:{
type:"json_schema",
json_schema:{
strict:true,
schema:...
}
}
底层差异在受约束解码:模型每生成一个 token,解码器先算“当前状态下哪些 token 还能让输出对 schema 合法”,非法 token 在采样前被屏蔽。约束发生在生成过程中,不是事后校验。
所以需要固定“回复数据的形状”用 Structured Outputs;需要模型决定是否去调系统动作用 Function Calling(工具参数同样可开 strict)。两者意图不同,混用只会徒增往返。
二、坑一:嵌套对象漏写 additionalProperties:false,静默退回非 strict
OpenAI strict 模式隐性要求:每一层嵌套 object 都必须显式写 "additionalProperties": false。少写一层 API 不报错,但整个请求静默退回非 strict,模型可以自由添加 schema 外的字段。
Pydantic 的 model_json_schema() 和 Zod 的 toJSONSchema() 默认都不生成这个字段。直接用它们定义完喂给 OpenAI,大概率一直在跑非 strict 而你不知道。
自查:
- 遍历 schema,确认每个
{type:"object"}节点下都有additionalProperties:false。 - 更稳做法:用 Instructor 或
zod-to-json-schema的openaiStrictMode转换,别让手写 schema 直接进请求。
三、坑二:optional 字段不能从 required 里删
strict 要求 required 数组包含 properties 下每个字段。把 optional 字段直接从 required 删掉会 400。
正确写法:字段仍在 required,类型允许 null(anyOf string/null)。JSON Schema 没有“可选字段”概念,可空 = 联合类型加 null。
strict 只支持 JSON Schema 子集:
- 根必须是
object - 所有 object 都要
additionalProperties: false minimum/maxLength/pattern等约束关键字可能被丢弃或直接 400- 递归 schema 有属性总数与嵌套深度限制
开 strict 传不支持的 schema 会直接报错,不会悄悄降级。
四、坑三:refusal 和截断都以 200 成功返回
即使开 strict,仍有两种“状态成功但 JSON 不合 schema”的情况:
- 拒答(refusal)。Chat Completions 在
message.refusal,Responses API 在output[0].type === "refusal"。跳过检查直接.parsed,安全审核触发时会报属性缺失。 - 截断。撞
max_tokens返回finish_reason:"length",JSON 残缺。
处理顺序:
- 先查
refusal/finish_reason。 - 再
JSON.parse。 - 最后用 Pydantic/Zod 或服务端 validator 做 schema 校验。
这层省不掉——它兜的不是格式错误(strict 已兜),而是拒答和截断这两条 strict 覆盖不到的路径。
五、把 schema 当生产接口代码
- schema 会和下游类型漂移,改字段时 LLM 无感知。用 Pydantic/Zod 做单一来源生成,别手写两份。
- 字段顺序在 JSON 无意义,但 LLM 从左到右生成时有意义。既有
reasoning又有classification时把reasoning放前,让模型先分析再下结论;把 20 个取值的 enum 放第一个字段,会逼模型过早做离散选择。 - strict 限制 token 生成速度,超长输出延迟明显上升;短结构化抽取才适合。
- 结构合法不代表业务正确,分项之和、日期先后等规则由后端兜底。
结论
Structured Outputs 是比“请返回 JSON”强得多的接口约束,但它是强类型边界,不是可靠性全部来源。每层补 additionalProperties:false、optional 用 null 联合、refusal 与截断单独处理、后端校验兜底——做完这些才真能进生产。
参考: