只要你把模型输出接到数据库、表单或下游 Agent,就不能再靠「提示词里写请返回 JSON」碰运气。OpenAI Structured Outputs 用受限解码把输出钉在你提供的 JSON Schema 上:字段不会丢、枚举不会乱编、类型不会在字符串和数字之间漂移。本文面向要做抽取、工单分类、评测打分和多步工作流的工程师,走完从 JSON Mode 升级到 strict: true 的完整路径。
JSON Mode 为什么不够稳
JSON Mode(type: "json_object")只保证「能被 JSON 解析器读进去」。它不保证键名、必填字段、枚举集合或数字类型。生产里最常见的三种翻车是:漏掉 severity、把 42 写成 "42"、在 enum 外发明一个 urgent-plus。下游一旦用强类型解析,整条流水线就会在随机样本上失败。
| 能力 | JSON Mode | Structured Outputs |
|---|---|---|
| 输出合法 JSON | 是 | 是 |
| 遵守你给的 JSON Schema | 否(靠提示词) | 是(受限解码) |
| 开启方式 | json_object |
json_schema + strict: true |
| 典型模型 | 较早期 GPT-4o / GPT-3.5 等 | gpt-4o-2024-08-06、gpt-4o-mini 及更新快照 |
| 拒答处理 | 仍可能吐出「看起来像 JSON」的拒绝话术 | 走独立的 refusal 字段 |
官方把 Structured Outputs 称为 JSON Mode 的演进:新项目应默认走 Schema,而不是在解析失败后再重试三次。若你同时在比较不同模型的 Token 账单,可以对照 Kimi K3 与 GPT-5.5 的 API 成本对比——结构化输出本身几乎不增加输出体积,真正拉开账单的是重试和过长推理。
response_format;Responses API 放在 text.format。字段路径不同,strict、additionalProperties: false 和「全部 required」这些约束完全一样。
最小可运行请求
下面这个工单抽取 Schema 覆盖了生产里 80% 的需求:字符串、枚举、整数,以及用 null 联合类型模拟可选字段。模型必须返回所有键;没有账号 ID 时显式给 null,而不是省略键。
{
"model": "gpt-4o-2024-08-06",
"messages": [
{"role": "system", "content": "把用户描述抽取成工单对象。"},
{"role": "user", "content": "结账页用已保存卡会 500。今天开始。账号 acct_8842。"}
],
"response_format": {
"type": "json_schema",
"json_schema": {
"name": "support_ticket",
"strict": true,
"schema": {
"type": "object",
"properties": {
"summary": { "type": "string" },
"category": {
"type": "string",
"enum": ["billing", "bug", "account", "other"]
},
"severity": { "type": "integer" },
"account_id": { "type": ["string", "null"] }
},
"required": ["summary", "category", "severity", "account_id"],
"additionalProperties": false
}
}
}
}
Responses API 的等价写法是把同一份 Schema 放进 text.format,type、name、schema、strict 作为兄弟字段。首次用某个 Schema 时,服务端会编译约束,延迟可能略高;之后同一 Schema 会被缓存,后续请求回到正常水平。
strict Schema 必须满足的规则
打开 strict: true 之后,你提交的不再是「宽松 JSON Schema」,而是官方支持的子集。不合规会直接 400,而不是「尽量遵守」。
- 根必须是 object:根节点不能是
anyOf或数组。Zod 的discriminatedUnion若生成顶层anyOf,需要包一层对象,例如{ "result": ... }。 - 每个 object 都要
additionalProperties: false:包括嵌套对象。漏一个就会被拒绝。 - properties 里出现的键必须全部进入
required:可选语义用"type": ["string", "null"]或anyOf加null。 - 支持的类型:string、number、integer、boolean、object、array、enum、
anyOf。 - 字符串约束:
pattern、format(email、date-time、uuid、ipv4等)。 - 数字与数组:
minimum/maximum/multipleOf;minItems/maxItems。 - 明确不支持:
allOf、not、if/then/else、dependentRequired等组合关键字。 - 规模上限:合计最多约 5000 个对象属性、10 层嵌套;所有属性名、定义名、枚举值字符串总长不超过 12 万字符;枚举值总数最多 1000。
pattern、format、minLength、minimum、minItems 等约束目前仍可能不受支持。先在基础快照上验证 Schema,再决定是否微调。
工具调用 vs 回复正文
同一套 strict 也能用在函数 / 工具参数上。差别是意图:工具 Schema 描述「模型要调用什么」;response_format / text.format 描述「模型要回复用户什么形状」。抽取、评分、生成 UI 状态用后者;查天气、写文件、跑命令用前者。若你在对比终端 Agent 与 GUI 操控能力,可参考 Claude Code 与 OpenAI Computer Use 的关系——那是另一层「怎么行动」,本文解决的是「行动前后的数据合同」。
用 SDK 直接解析成对象
手写 JSON Schema 容易漏 required。官方 SDK 提供 Pydantic / Zod 助手:从类型生成 Schema,并在响应侧直接给出解析后的对象。下面是 Python 的典型写法。
from typing import Literal, Optional
from pydantic import BaseModel
from openai import OpenAI
class SupportTicket(BaseModel):
summary: str
category: Literal["billing", "bug", "account", "other"]
severity: int
account_id: Optional[str]
client = OpenAI()
completion = client.beta.chat.completions.parse(
model="gpt-4o-2024-08-06",
messages=[
{"role": "system", "content": "抽取工单。"},
{"role": "user", "content": "结账页 500,账号 acct_8842。"},
],
response_format=SupportTicket,
)
ticket = completion.choices[0].message.parsed
if completion.choices[0].message.refusal:
raise RuntimeError(completion.choices[0].message.refusal)
print(ticket.category, ticket.severity)
报错、拒答与工程清单
上线前按这张清单过一遍,能消掉绝大多数「本地 curl 能跑、流水线却 400」的问题。
- 400 Unsupported schema:缺
additionalProperties: false、有未 required 字段、根是anyOf,或用了allOf。 - 模型拒答:安全策略触发时,内容不会塞进 Schema;读取
refusal,在 UI 里展示,不要当 JSON 解析失败重试。 - 可选字段变成空字符串:合同里写了
null就要接受null;下游入库前把null映射成 SQLNULL,不要再二次猜测。 - 枚举过宽:把业务状态收成 5–8 个值;上百个枚举既烧上下文,也逼近 1000 条上限。
- 键顺序:输出键顺序与 Schema 中声明顺序一致,流式消费时可以按字段增量解析。
- 提示词仍有用:Schema 管形状,提示词管语义。例如「severity 1–5,5 为全站故障」要写在 system 里,整数范围也可以用
minimum/maximum再锁一层。
落地时建议固定三件事:Schema 进 Git;对拒答、400、超时分别打点;用真实工单做一组回归样本,而不是只用「Hello 返回 JSON」这种玩具输入。结构化输出让解析层稳定之后,你才能把精力放到分类准确率和延迟上,而不是每周修一次正则。
在 Mac mini 上跑抽取流水线,环境更省心
Structured Outputs 的调试循环很短:改 Schema、跑一小批样本、看解析对象。macOS 上 Python、Node.js、Docker 和 Homebrew 开箱即用,不必先搭 WSL。Apple Silicon 统一内存让你同时开编辑器、本地校验脚本和长上下文评测,不至于在类型检查时把机器拖进 swap。
Mac mini M4 待机大约 4W,适合把回归样本集整晚挂着跑;macOS 崩溃率低,Gatekeeper 与 SIP 也降低了依赖脚本被污染的风险。和同价位 Windows 盒子比,长期无人值守跑 Agent 评测时,稳定性和电费都更友好。
若你希望有一台一直在线、专门跑 JSON 回归与模型对比的节点,了解 Kvmzen 的套餐方案,把 Schema 合同从笔记本搬到固定规格的云端 Mac 上。