结构化输出与 JSON Schema:如何让模型稳定返回 JSON
模型返回的 JSON 经常少字段、类型错误或夹带 Markdown?本文解释 JSON Mode 与结构化输出的区别,并给出 OpenAI、Claude、Gemini 配置、Schema 设计、校验与重试方案。

“请只返回 JSON”不是接口契约。
它可能在简单 Demo 里有效,但进入生产后,你迟早会遇到这些结果:
- JSON 外面包了一层 Markdown 代码块;
- 缺少必填字段;
price一会儿是数字,一会儿是字符串;- 分类值超出允许范围;
- 输出因 Token 上限被截断;
- 模型拒答,但你的代码仍把它当业务对象解析;
- JSON 语法正确,业务语义却完全错误。
真正稳定的方案不是把提示词写得更凶,而是使用 Provider 提供的结构化输出能力,用 JSON Schema 约束生成过程,再在应用层完成解析、业务校验和错误处理。

本文会讲清楚:
- Prompt、JSON Mode 与 JSON Schema 有什么区别;
- 如何设计一个适合模型生成的 Schema;
- OpenAI、Claude、Gemini 当前怎么配置;
- 为什么“符合 Schema”仍不等于“业务正确”;
- 如何处理拒答、截断、流式输出和重试。
如果你还不熟悉三家的原生请求结构,可以先看 OpenAI、Claude 和 Gemini API 格式对比。
三种控制方式不是一回事

1. 只靠 Prompt
最基础的做法是:
Extract the order and return JSON only.
这种方式没有协议级约束。模型仍可能返回解释、代码围栏、错误字段名或无效 JSON。
它适合一次性实验,不适合作为支付、订单、数据库写入或自动化工作流的稳定输入。
2. JSON Mode
JSON Mode 通常保证输出是有效 JSON,但未必保证它符合你的字段结构。
例如你想要:
{
"order_id": "A-1001",
"priority": "high"
}
模型可能合法地返回:
{
"id": "A-1001",
"urgency": 9
}
它是有效 JSON,却不符合业务契约。
3. JSON Schema 结构化输出
结构化输出把允许的对象、字段、类型、必填项和枚举交给模型服务。支持严格约束时,生成过程会被限制在 Schema 允许的结构中。
这能减少:
- JSON 语法错误;
- 字段缺失;
- 类型漂移;
- 非法枚举;
- 多余字段。
但它仍不能替你判断事实是否正确,也不会自动完成数据库权限、金额范围或库存状态等业务校验。
从一个可维护的 Schema 开始
假设你要从客服消息中提取工单:
{
"type": "object",
"properties": {
"category": {
"type": "string",
"enum": ["billing", "technical", "account", "other"],
"description": "The primary support category"
},
"priority": {
"type": "string",
"enum": ["low", "medium", "high"]
},
"summary": {
"type": "string",
"description": "A concise factual summary"
},
"needs_human": {
"type": "boolean"
},
"order_id": {
"type": ["string", "null"],
"description": "Order ID when explicitly present, otherwise null"
}
},
"required": ["category", "priority", "summary", "needs_human", "order_id"],
"additionalProperties": false
}
为什么要写 description
Schema 不只是验证器,也是模型的字段说明书。
category: string 只告诉模型类型;清晰的 description 还能说明字段含义、信息来源和判断边界。但不要把几百字业务规则都塞进每个字段,否则会增加 Token、延迟和维护成本。
枚举优于自由字符串
如果下游只接受固定状态,就直接使用 enum:
{
"type": "string",
"enum": ["pending", "approved", "rejected"]
}
不要让模型自由生成 approve、Approved、pass,再在业务代码里猜它们是否等价。
明确空值策略
“没有数据”至少有三种表达:字段缺失、null、空字符串。
生产接口应选择一种,并写进 Schema。跨 Provider 时,显式要求字段存在且允许 null,通常比依赖可选字段更容易统一:
{
"type": ["string", "null"]
}
具体写法必须以目标 Provider 支持的 JSON Schema 子集为准。
限制额外字段
对象结构建议设置:
{
"additionalProperties": false
}
这可以阻止模型临时添加 reasoning、note 或拼错的字段。但不同 Provider 对 Schema 关键字的支持并不完全一致,不能假定任意 Draft 的所有能力都可用。
OpenAI:Structured Outputs 与 JSON Mode
OpenAI Responses API 的结构化文本输出使用 text.format。一个简化请求如下:
{
"model": "MODEL_ID",
"input": "Classify this support message: I was charged twice.",
"text": {
"format": {
"type": "json_schema",
"name": "support_ticket",
"strict": true,
"schema": {
"type": "object",
"properties": {
"category": {
"type": "string",
"enum": ["billing", "technical", "account", "other"]
},
"priority": {
"type": "string",
"enum": ["low", "medium", "high"]
}
},
"required": ["category", "priority"],
"additionalProperties": false
}
}
}
}
Chat Completions 使用的是 response_format,不要把 Responses 的 text.format 原样复制过去。
OpenAI 还保留 JSON Mode。它主要保证 JSON 可解析,不保证字段与 JSON Schema 一致。能使用 Structured Outputs 时,官方建议优先使用它。
OpenAI 的边缘情况
即使开启严格结构,也必须处理:
- 拒答:安全拒答可能以可检测的 refusal 路径出现,而不是伪装成业务 JSON;
- 不完整响应:达到输出上限时,检查响应状态和 incomplete details;
- 工具调用:如果模型应该调用应用函数,应使用 Function Calling,而不是把“要执行的动作”伪装成普通结构化文本;
- 模型支持范围:Structured Outputs 需要支持该能力的模型,不能只凭 Endpoint 推断。
原始 REST 响应仍可能包含多个 output item。SDK 的便利属性不等于 Wire Format 只有一个顶层字符串。
还有一个容易忽略的风险:当输入与任务无关,但 Schema 又强制每个字段存在时,模型可能为了填满结构而编造值。为这类任务增加 status: ok | insufficient_input | not_applicable,并允许 data 为 null,比强迫模型输出伪数据更安全。
Claude:output_config.format 与严格工具输入
Claude Messages API 当前把 JSON 输出格式放在 output_config.format:
{
"model": "MODEL_ID",
"max_tokens": 800,
"messages": [
{
"role": "user",
"content": "Classify this support message: I was charged twice."
}
],
"output_config": {
"format": {
"type": "json_schema",
"schema": {
"type": "object",
"properties": {
"category": {
"type": "string",
"enum": ["billing", "technical", "account", "other"]
},
"priority": {
"type": "string",
"enum": ["low", "medium", "high"]
}
},
"required": ["category", "priority"],
"additionalProperties": false
}
}
}
}
返回的 JSON 文本位于 response.content[0].text,应用仍需要 JSON.parse() 或 SDK 的类型化解析助手。
Claude 将两个需求分开:
output_config.format:约束最终文本输出;- 工具定义中的
strict: true:约束工具名称和输入参数。
如果模型要先调用工具再给答案,这两种能力可以同时使用,但不能把严格工具调用等同于最终 JSON 输出。
首次 Schema 延迟
Claude 官方说明,结构化输出会编译受约束语法。首次使用某个 Schema 时可能有额外延迟;后续可命中编译缓存。修改 Schema 结构或工具集合可能导致缓存失效。
因此生产监控应区分:
- 首次 Schema 编译延迟;
- 常规模型延迟;
- Provider 网络延迟;
- 应用解析和校验耗时。
不要看到首个请求偏慢,就立即把它归因于模型退化。
还要先检查 Claude 的 stop_reason:refusal 和 max_tokens 都可能优先于 Schema 保证。当前官方文档也列出了一些组合限制,例如 Citations 不能与 output_config.format 同时使用。若依赖枚举,应用层最好仍做规范化和精确校验,而不是把任何正常结束都直接当成业务成功。
Gemini:区分 GenerateContent 与 Interactions API
Gemini 官方文档正在推荐新的 Interactions API,但大量现有应用仍使用 generateContent。两者字段不能混写。
在 GenerateContent 中,常见结构是:
{
"contents": [
{
"role": "user",
"parts": [
{
"text": "Classify this support message: I was charged twice."
}
]
}
],
"generationConfig": {
"responseMimeType": "application/json",
"responseJsonSchema": {
"type": "object",
"properties": {
"category": {
"type": "string",
"enum": ["billing", "technical", "account", "other"]
},
"priority": {
"type": "string",
"enum": ["low", "medium", "high"]
}
},
"required": ["category", "priority"],
"additionalProperties": false
}
}
}
不同 SDK 版本可能暴露 response_schema、response_json_schema 等语言风格字段;REST JSON 使用文档规定的 Wire Format。不要把 Python 的 snake_case 参数直接贴进 REST 请求。
新的 Interactions API 则通过 response_format 配置 MIME Type 与 Schema。选定 API 后,应从对应页面复制同一套示例,不要把 Interactions 的 response_format 填进 GenerateContent。
Google 明确提醒:结构化输出保证可预测的 JSON 形状,但仍应在应用层校验值,因为结果可能语法和结构正确、语义错误。
三家配置字段速查
| 需求 | OpenAI Responses | Claude Messages | Gemini GenerateContent |
|---|---|---|---|
| Schema 入口 | text.format | output_config.format | generationConfig.responseJsonSchema / 当前文档对应字段 |
| JSON MIME | 格式对象决定 | 格式对象决定 | responseMimeType: application/json |
| 严格工具参数 | Function Calling 的严格 Schema | 工具 strict: true | Function Calling 使用独立声明 |
| 文本结果 | output[] 的 output_text 内容块 | content[0].text | candidates[].content.parts[].text |
| 注意事项 | 处理 refusal 与 incomplete | 首次 Schema 编译可能增加延迟 | API 与 SDK 字段不要混用 |
模型与 API 会演进,以上字段应以你实际使用的 Endpoint、SDK 版本和官方文档为准。
结构化输出不等于工具调用
两者都可能使用 JSON Schema,但目的不同:
- 结构化输出:让最终答案成为稳定业务对象,例如分类、抽取、报告;
- 工具调用:让模型请求应用执行动作,例如查库存、发邮件、创建工单。
如果输出会触发真实副作用,不应仅因为模型返回:
{"action":"refund","order_id":"A-1001"}
就直接退款。正确流程是把动作定义成受控工具,服务端再执行权限、金额、幂等和审计检查。
一条生产级处理流水线

建议把结构化输出处理拆为明确状态:
request
-> provider response
-> detect refusal / tool call / incomplete
-> extract text
-> parse JSON
-> validate schema
-> validate business rules
-> accept or retry
1. 先判断响应类型
不要先对所有响应执行 JSON.parse()。先判断:
- 请求是否成功;
- 模型是否拒答;
- 是否达到输出上限;
- 是否返回工具调用;
- 是否存在候选文本;
- 流是否正常结束。
这也能避免把“空内容”误判为 JSON 解析错误。相关排查可参考 AI API 返回空内容怎么处理。
2. 再解析 JSON
解析失败时,记录:
- Provider、模型和 Endpoint;
- Schema 版本或哈希;
- 完成原因;
- 输出长度;
- 请求 ID;
- 脱敏后的错误类别。
不要把包含个人数据、真实 API Key 或完整敏感模型输出的日志直接写入公开监控。
3. 再做 Schema 校验
即使 Provider 承诺严格输出,应用端校验仍有价值:
- 防止网关或兼容层转换错误;
- 发现所选模型不支持严格结构;
- 保护跨 Provider 故障切换;
- 对 Schema 版本升级提供防线;
- 统一 SDK 和 REST 差异。
Node.js 可用 Ajv、Zod,Python 可用 jsonschema、Pydantic。关键不是选哪一个库,而是让同一个 Schema 或同一个类型定义贯穿请求与运行时校验。
4. 最后做业务校验
下面的对象完全可能符合 Schema,却仍不可用:
{
"currency": "USD",
"amount": -999999,
"order_id": "invented-order"
}
业务层至少应检查:
- ID 是否真实存在;
- 金额、日期、数量是否在允许范围;
- 字段之间是否互相一致;
- 信息是否来自输入,而非模型猜测;
- 当前用户是否有权限执行下一步。
Schema 怎么设计得更稳定

保持结构简单
避免一开始就设计十层嵌套、几十个联合类型和复杂递归。Provider 通常只支持 JSON Schema 的一个子集,过度复杂的 Schema 可能直接被拒绝,或增加编译与生成开销。
缩小字段职责
比起一个含义模糊的 result,更推荐:
{
"summary": "...",
"category": "billing",
"confidence": 0.82,
"needs_human": true
}
但 confidence 只是模型自报值,不应被当成校准后的真实概率。
给未知状态留出口
分类任务不要强迫模型在错误选项中二选一。可以加入:
{
"enum": ["billing", "technical", "account", "other", "unknown"]
}
并设计 needs_human,让不确定内容进入人工队列。
版本化 Schema
为业务对象保存版本:
{
"schema_version": "1.0",
"category": "billing"
}
当你增加字段、修改枚举或切换 Provider 时,可以按版本解析,而不是让所有消费者同时失效。
什么时候应该重试
适合重试:
- 临时网络错误或
5xx; - Provider 明确表示响应不完整;
- 兼容层没有执行预期的 Schema 约束;
- 业务校验发现可通过补充指令纠正的缺失信息。
不适合盲目重试:
- 安全拒答;
- Schema 本身不受支持;
- 请求体字段写错;
- 输入根本没有所需信息;
- 权限或认证失败。
重试应设置上限、指数退避和抖动,并记录原因。不要把同一错误请求无限发送给模型。
如果响应被截断,应先增加合理的输出预算或缩小 Schema/任务,而不是只重复请求。上下文预算可参考 Context Length Exceeded 排查指南。
流式结构化输出怎么处理
流式传输中的单个 Delta 通常不是完整 JSON,因此不要对每个片段执行 JSON.parse()。
安全做法有两种:
- 缓冲完整文本,等正常完成事件后统一解析和校验;
- 使用 Provider SDK 提供的结构化流式事件或增量解析器,并在最终完成时再次校验。
无论哪种方式,都要处理:
- 中途断流;
- 错误事件;
- 拒答事件;
- 输出 Token 截断;
- 工具调用与文本混合;
- 客户端取消请求。
前端可以显示“正在生成”,但不要在最终校验前把半成品 JSON 写入数据库。
多 Provider 系统如何统一
不要让业务代码到处判断 if provider === ...。可以定义一个内部接口:
type StructuredRequest = {
task: string
schemaName: string
schema: Record<string, unknown>
maxOutputTokens: number
}
type StructuredResult<T> =
| { status: "ok"; data: T }
| { status: "refused"; reason?: string }
| { status: "incomplete"; reason?: string }
| { status: "invalid"; errors: string[] }
Provider Adapter 负责:
- 把 Schema 转换到官方支持的子集;
- 映射
text.format、output_config.format或 Gemini 配置; - 提取正确的文本块;
- 统一 refusal、finish reason、usage 和 request ID;
- 保留原始错误用于诊断。
如果使用 OpenAI 兼容网关,要特别确认它是否真的透传并执行 Structured Outputs,而不只是接受字段后忽略。兼容 Endpoint 的判断方法可参考 什么是 OpenAI 兼容 API。
上线前检查清单
- 目标模型明确支持结构化输出
- 使用的是目标 Endpoint 对应字段
- Schema 位于 Provider 支持的子集内
- 对象设置了明确的
required - 不允许的额外字段已关闭
- 枚举、空值和未知状态定义清楚
- 能区分文本、工具调用、拒答和截断
- 最终结果经过 JSON 解析与运行时 Schema 校验
- 金额、ID、权限等经过业务校验
- 流式输出在完成前不进入数据库
- 重试有上限、退避和错误分类
- 日志已脱敏并记录 Schema 版本
- 切换 Provider 时有契约测试
在 Nbility 中接入多协议结构化输出
通过 Nbility 统一调用多家模型时,先确认客户端实际发送的是 OpenAI、Claude 还是 Gemini 协议,再选择对应 Endpoint 和字段。
网关可以统一认证、模型入口和用量管理,但不会让三家的原生 JSON Schema 字段自动变成同一种格式。对生产系统,仍建议保留 Provider Adapter 和应用端校验。
如果你需要小额测试不同模型的结构化输出表现,可在 Nbility 工单页面 申请测试额度。
总结
让模型稳定返回 JSON,要建立三层防线:
- 生成约束:使用 Provider 原生 Structured Outputs 和受支持的 JSON Schema;
- 技术校验:检测拒答、截断和工具调用,解析并验证最终 JSON;
- 业务校验:核对事实、范围、权限和副作用。
JSON Mode 只能解决“像不像 JSON”;JSON Schema 解决“结构是否符合契约”;业务代码才负责“结果是否真的可用”。
当这三层都存在时,模型输出才能从 Demo 文本变成可维护的生产数据接口。


