Structured OutputJSON SchemaOpenAI APIClaude APIGemini APIAPI EngineeringNbility

结构化输出与 JSON Schema:如何让模型稳定返回 JSON

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

结构化输出与 JSON Schema:如何让模型稳定返回 JSON

“请只返回 JSON”不是接口契约。

它可能在简单 Demo 里有效,但进入生产后,你迟早会遇到这些结果:

  • JSON 外面包了一层 Markdown 代码块;
  • 缺少必填字段;
  • price 一会儿是数字,一会儿是字符串;
  • 分类值超出允许范围;
  • 输出因 Token 上限被截断;
  • 模型拒答,但你的代码仍把它当业务对象解析;
  • JSON 语法正确,业务语义却完全错误。

真正稳定的方案不是把提示词写得更凶,而是使用 Provider 提供的结构化输出能力,用 JSON Schema 约束生成过程,再在应用层完成解析、业务校验和错误处理。

结构化输出将自然语言转换为受约束 JSON

本文会讲清楚:

  1. Prompt、JSON Mode 与 JSON Schema 有什么区别;
  2. 如何设计一个适合模型生成的 Schema;
  3. OpenAI、Claude、Gemini 当前怎么配置;
  4. 为什么“符合 Schema”仍不等于“业务正确”;
  5. 如何处理拒答、截断、流式输出和重试。

如果你还不熟悉三家的原生请求结构,可以先看 OpenAI、Claude 和 Gemini API 格式对比

三种控制方式不是一回事

Prompt、JSON Mode 与 JSON Schema 的可靠性差异

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"]
}

不要让模型自由生成 approveApprovedpass,再在业务代码里猜它们是否等价。

明确空值策略

“没有数据”至少有三种表达:字段缺失、null、空字符串。

生产接口应选择一种,并写进 Schema。跨 Provider 时,显式要求字段存在且允许 null,通常比依赖可选字段更容易统一:

{
  "type": ["string", "null"]
}

具体写法必须以目标 Provider 支持的 JSON Schema 子集为准。

限制额外字段

对象结构建议设置:

{
  "additionalProperties": false
}

这可以阻止模型临时添加 reasoningnote 或拼错的字段。但不同 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,并允许 datanull,比强迫模型输出伪数据更安全。

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_reasonrefusalmax_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_schemaresponse_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 ResponsesClaude MessagesGemini GenerateContent
Schema 入口text.formatoutput_config.formatgenerationConfig.responseJsonSchema / 当前文档对应字段
JSON MIME格式对象决定格式对象决定responseMimeType: application/json
严格工具参数Function Calling 的严格 Schema工具 strict: trueFunction Calling 使用独立声明
文本结果output[]output_text 内容块content[0].textcandidates[].content.parts[].text
注意事项处理 refusal 与 incomplete首次 Schema 编译可能增加延迟API 与 SDK 字段不要混用

模型与 API 会演进,以上字段应以你实际使用的 Endpoint、SDK 版本和官方文档为准。

结构化输出不等于工具调用

两者都可能使用 JSON Schema,但目的不同:

  • 结构化输出:让最终答案成为稳定业务对象,例如分类、抽取、报告;
  • 工具调用:让模型请求应用执行动作,例如查库存、发邮件、创建工单。

如果输出会触发真实副作用,不应仅因为模型返回:

{"action":"refund","order_id":"A-1001"}

就直接退款。正确流程是把动作定义成受控工具,服务端再执行权限、金额、幂等和审计检查。

一条生产级处理流水线

请求、Schema、解析、校验与重试流水线

建议把结构化输出处理拆为明确状态:

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 怎么设计得更稳定

稳定 JSON 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()

安全做法有两种:

  1. 缓冲完整文本,等正常完成事件后统一解析和校验;
  2. 使用 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.formatoutput_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,要建立三层防线:

  1. 生成约束:使用 Provider 原生 Structured Outputs 和受支持的 JSON Schema;
  2. 技术校验:检测拒答、截断和工具调用,解析并验证最终 JSON;
  3. 业务校验:核对事实、范围、权限和副作用。

JSON Mode 只能解决“像不像 JSON”;JSON Schema 解决“结构是否符合契约”;业务代码才负责“结果是否真的可用”。

当这三层都存在时,模型输出才能从 Demo 文本变成可维护的生产数据接口。

官方资料

相关文章

OpenAI、Claude 和 Gemini API 格式有什么区别?
OpenAI APIClaude APIGemini API

OpenAI、Claude 和 Gemini API 格式有什么区别?

对比 OpenAI Responses 与 Chat Completions、Claude Messages、Gemini GenerateContent 的认证、消息、系统指令、流式输出、工具调用、多模态和结构化输出格式。

用 Nbility 跑通你的 Agent 工作流

获取 API Key,统一接入 OpenAI 兼容模型和开发工具。

管理 API Key