Mindket

OpenAI 结构化输出:拿到符合你 schema 的 JSON

黑色横幅,标题是“OpenAI API 实战”,下面是“Responses、工具调用、结构化输出与智能体”。

结构化输出让 OpenAI 模型按你提供的 JSON Schema 回复 JSON,必填的键不会缺失,也不会编造枚举值。在 Responses API 里,你可以把 schema 放进 text.format 并打开 strict,也可以把一个 Pydantic 模型或 Zod 对象交给 SDK,再从 output_parsed 读取带类型的结果。

本节内容

  • 在代码里把结构只定义一次,让 SDK 据此生成 schema。
  • 先判断拒答和未完成状态,再相信解析出来的对象。
  • 告诉模型,输入对不上这个结构时该返回什么。
  • 根节点保持为普通对象,可选字段写成可为空。
  • 模型要做事时用工具调用,模型要回答时用回复格式。
全部课程
本页目录
  1. 开始之前
  2. 用 Pydantic 拿到带类型的回复
  3. 用 Zod,或者自己传入 JSON Schema
  4. 处理拒答和被截断的回复
  5. schema 里能放什么,不能放什么
  6. Michelle Pokrass 和 OpenAI Developers 谈 schema 的可靠性
  7. 常见错误
  8. 自己试一试
  9. 接下来
  10. 自测一下
  11. 常见问题
  12. 资料来源

开始之前

你需要第 1 课的准备,也就是 OPENAI_API_KEY 里的 API 密钥和官方 SDK,再加一个 schema 库。Python 用 Pydantic,运行 pip install openai pydantic。JavaScript 用 Zod,示例会和 SDK 一起导入它,运行 npm install openai zod。Pydantic 的 BaseModel 和 Zod 的 z.object 都是在代码里描述数据结构,SDK 会替你把结构转成 JSON Schema。

JSON Schema 是描述 JSON 数据的标准写法,规定一个对象有哪些键、每个值是什么类型、哪些键必须出现。

结构化输出适用于 GPT-4o 及之后的 OpenAI 模型,gpt-4-turbo 这类更早的模型只能退回 JSON 模式。指南建议新项目用 gpt-6-astra,也就是 2026 年 11 月 5 日本课所有示例用的模型。它有两种形式。第 2 课已经用过一种:函数定义上的 strict,让模型给出的参数符合你的 schema。这一课讲另一种,text.format,让模型回复用户的内容符合 schema。

指南划的界线很简单。模型要连接你的函数、数据或工具时,用函数调用。你想规定模型回复的样子时,比如一个数学辅导应用要在界面上逐步渲染解题步骤,就用 text.format。查询订单的退款流程是函数,界面要拆成摘要和后续步骤的客服回复是回复格式。从 Chat Completions 迁移过来的代码要注意,response_format 在 Responses API 里变成了 text.format。

指南展示了它的四种用法。数学辅导按顺序返回解题步骤。信息抽取从研究论文里取出标题、作者、摘要和关键词。界面生成把 HTML 表示成一棵受枚举约束的递归元素树。内容审核把输入归入不同类别。

用 Pydantic 拿到带类型的回复

附上一个 schema,问数学辅导“how can I solve 8x + 7 = -23”,文档给出的示例回复就是纯粹的 JSON。它有一个包含五个对象的 steps 列表,每个对象都把一句 explanation 和一个 output 配成一对,前者解释这一步做了什么,后者是这一步得到的式子。最后是 "final_answer": "x = -15 / 4"。JSON 外面没有包任何文字,也没有缺任何键。

指南里,正是下面的代码生成了这个结构。两个 Pydantic 类描述了它:一个 Step 带两个字符串,一个 MathReasoning 装着步骤列表和最终答案。client.responses.parse 把这个类作为 text_format 接收,打开严格模式发送对应的 JSON Schema,再把回复解析回一个 MathReasoning 实例。

接着你的代码把 response.output_parsed 当作带类型的对象来读,所以 math_reasoning.final_answer 是一个编辑器认得的字符串。指南列出了这样做省掉的工作:不用再校验和重试格式错误的回复,拒答可以在代码里检测出来,提示词也不必再苦苦要求某种格式。

同一个示例的 JavaScript 版本用 z.object 构建 Step 和 MathReasoning,列表用 z.array(Step) 表示。它把 zodTextFormat(MathReasoning, "math_reasoning") 作为 text.format 传入,读取的也是同一个 output_parsed 字段。

from openai import OpenAI
from pydantic import BaseModel

client = OpenAI()

class Step(BaseModel):
    explanation: str
    output: str

class MathReasoning(BaseModel):
    steps: list[Step]
    final_answer: str

response = client.responses.parse(
    model="gpt-6-astra",
    input=[
        {
            "role": "system",
            "content": "You are a helpful math tutor. Guide the user through the solution step by step.",
        },
        {"role": "user", "content": "how can I solve 8x + 7 = -23"},
    ],
    text_format=MathReasoning,
)

math_reasoning = response.output_parsed
  1. 用 pip install openai pydantic 安装 SDK 和 Pydantic。
  2. 为你想拿回的结构里的每个对象写一个 BaseModel 类。
  3. 调用 client.responses.parse,传入你的输入,并把 text_format 设为最顶层的类。
  4. 读取结果之前,先确认模型既没有拒答,也没有中途停下。
  5. 读取 response.output_parsed,像使用普通 Python 对象一样使用它的字段。

用 Zod,或者自己传入 JSON Schema

在 JavaScript 里,同样的思路靠 Zod 实现。指南开头的示例定义了一个 CalendarEvent,带名称、日期和参与者列表,然后把 zodTextFormat(CalendarEvent, "event") 作为 text.format 传给 openai.responses.parse。第二个参数是这个格式的名字。输入“Alice and Bob are going to a science fair on Friday.”,回复会被解析成这个事件结构,response.output_parsed 里就是带类型的对象。

不用辅助方法时,你要手写 schema。给 responses.create 传入 text: { format: { type: "json_schema", name: "math_response", schema: {...}, strict: true } },再从 output_text 读取 JSON 字符串。name 字段是这个格式的名字,schema 里放的就是 JSON Schema 本身。schema 里的每个对象都要有 additionalProperties: false,每个属性都要列进 required,和第 2 课严格模式函数的规则一样。模型会按 schema 里键的顺序输出。数学示例把 steps 放在 final_answer 前面,所以解题过程会先于答案写出来。

任何新 schema 的第一次请求,都会因为 API 要处理它而多一些延迟。之后用同一个 schema 的请求就没有这笔开销。每次调用都换 schema,就每次都要付这笔延迟。

两种写法也可以混用。指南里有一个 Python 示例,格式是手写的,schema 却是用 CrateCount.model_json_schema() 从 Pydantic 类生成的。这个类设置了 model_config = ConfigDict(extra="forbid"),生成的 schema 因此不允许多余的键。

手写 schema 还有一个风险:schema 和你代码里的类型会渐渐对不上。指南正是因此强烈建议使用 SDK 的辅助方法。如果你坚持手写,它建议在 CI 里加一步,从类型定义生成 schema,或者加一条规则,任何一边被改动就发出提醒。至于 schema 本身,键名要清楚,重要的键要有标题和描述,再用评估来找出效果最好的结构。

JSON 模式是更早、更弱的设置,下表列出了两者的区别。它只保证 JSON 合法,不保证符合你的 schema。上下文里还必须出现“JSON”这个词,否则 API 会报错。如果缺了这条指示,模型可能一直输出空白,直到碰到 token 上限。只有在模型不支持结构化输出时才用它,并且要配上校验库和重试。

import OpenAI from "openai";
import { zodTextFormat } from "openai/helpers/zod";
import { z } from "zod";

const openai = new OpenAI();

const CalendarEvent = z.object({
  name: z.string(),
  date: z.string(),
  participants: z.array(z.string()),
});

const response = await openai.responses.parse({
  model: "gpt-6-astra",
  input: [
    { role: "system", content: "Extract the event information." },
    {
      role: "user",
      content: "Alice and Bob are going to a science fair on Friday.",
    },
  ],
  text: {
    format: zodTextFormat(CalendarEvent, "event"),
  },
});

const event = response.output_parsed;
出自结构化输出指南,2026 年 11 月 5 日阅读。
结构化输出JSON 模式
合法的 JSON是是
符合你的 schema是,限于支持的子集否
怎样打开text.format 类型设为 json_schema,并设 strict: truetext.format 类型设为 json_object
适用模型从 gpt-4o-mini 和 gpt-4o-2024-08-06 开始较早的 GPT-3.5、GPT-4 和 GPT-4o 模型,以及兼容的 GPT-5 模型

处理拒答和被截断的回复

回复对不上你的 schema,有两种情况,而且都很常见。模型可能出于安全原因拒绝回答,也可能在 JSON 闭合之前就用完了篇幅。

拒答不遵循你的 schema,所以它以单独的内容类型返回。在 message 项里,这个内容部分的 type 是 "refusal",还有一个 refusal 字段装着模型的原话。指南的示例 JSON 里,这句话是“I'm sorry, I cannot assist with that request.”。此时响应状态仍然是 completed,所以只检查状态的代码,会去解析一个拒答。

下面的循环是指南拒答示例的结尾部分。它跳过不是消息的输出项,打印拒答内容,遇到没有解析结果的部分就抛出错误。在真正的应用里,你会把拒答显示在界面上,或者转去一个备用流程,而不是把它打印出来。

被截断的回复是另一个样子。如果你设置了 max_output_tokens,而模型用到了上限,response.status 就是 "incomplete",response.incomplete_details.reason 是 "max_output_tokens"。JSON 停在半路,无法解析,所以要先检查这个状态,再提高上限或者把任务缩短。指南里更完整的边界情况示例就是按这个顺序检查的:先看未完成状态,再找第一个 message 项,最后看它的第一个内容部分是拒答还是文本。

带 schema 也能流式输出。在 Python 里,client.responses.stream 接收同样的 text_format,循环里检查 response.refusal.delta、response.output_text.delta、response.error 和 response.completed。流结束后,stream.get_final_response() 返回完整的响应。在 JavaScript 里,openai.responses.stream 接收一个 zodTextFormat,用 .on("response.output_text.delta", ...) 和对应的拒答事件来监听,await stream.finalResponse() 返回最终结果。指南建议让 SDK 来处理流式的结构化输出,而不是自己解析那些片段。

用户输入还需要再加一道防护。模型总会尽力套进 schema,所以一个和任务毫不相干的输入,可能换来一个格式合法、内容却是编出来的结果。要在 instructions 里写明这种情况下该返回什么,比如空字段或者一句固定的话。少了这条指示,一条凭空编出、看着却很正常的记录,到了下游就很难被发现。

for output in response.output:
    if output.type != "message":
        continue

    for item in output.content:
        if item.type == "refusal":
            # If the model refuses to respond, you will get a refusal message
            print(item.refusal)
            continue

        if not item.parsed:
            raise Exception("Could not parse response")

        print(item.parsed)

schema 里能放什么,不能放什么

结构化输出支持 JSON Schema 的一个子集。支持的类型有字符串、数字、布尔值、整数、对象、数组、枚举和 anyOf。字符串可以带 pattern,也就是值必须匹配的正则表达式,还可以带 format,可选 date-time、time、date、duration、email、hostname、ipv4、ipv6 或 uuid。数字可以带 minimum、maximum、它们的不含边界版本,以及 multipleOf。数组可以带 minItems 和 maxItems。

有些关键字暂不支持:allOf、not、if、then、else、dependentRequired 和 dependentSchemas。带着 strict: true 发送其中任何一个,API 会直接报错,而不是悄悄地尽力而为。微调模型还不支持上面那些字符串、数字和数组的约束,以及对象上的 patternProperties。

schema 的根节点必须是对象,而且不能是 anyOf。这一条常常绊倒 Zod 用户。判别联合,比如以 status 区分的成功结构和错误结构,编译后在最顶层就是 anyOf,指南演示了它会失败。办法是把这个联合包进对象的一个字段里。在 schema 内部,只要每个分支本身都是同一子集下合法的 schema,anyOf 就能用。

每个字段都必须在 required 里,所以模型总会给它一个值。要让字段可选,就保留在 required 里,并在它的类型里允许 null,在 Pydantic 里就是把字段类型写成 str | None。指南的内容审核示例展示了效果。它的模型里有 category: Category | None 和 explanation_if_violating: str | None。问“怎样准备工作面试”时,文档的示例回复把 is_violating 设为 false,那两个字段都是 null。每个对象还都要有 additionalProperties: false。$defs 下的定义可以用,用到两次的结构只写一次再引用即可。指南的示例把 step 定义在那里,再用 "$ref": "#/$defs/step" 让 steps 数组指向它。递归 schema 也支持,用 # 指回根节点。

大小有硬性上限,见下表。手写的 schema 很少碰到它们。当你从一个大目录生成 schema 时,比如一个列出所有产品名的枚举,这些上限就要紧了,所以要在第一次请求替你发现问题之前,先把数量核对一遍。

OpenAI 更新日志里的结构化输出
  1. 结构化输出发布

    模型输出可靠地遵循你提供的 JSON Schema。

  2. Responses API 发布

    一个为智能体和工具打造的新 API,也就是本课程用的那个。

  3. strict 模式支持并行调用,关键字也更多了

    字符串的正则和 email 这类格式加入了支持的子集。

来源:OpenAI Docs: Changelog (2026年11月5日 查阅)

出自结构化输出指南里支持的 schema 部分,2026 年 11 月 5 日阅读。
限制项上限
整个 schema 的对象属性数5,000
嵌套层数10
属性名、定义名、枚举值和常量值的总字符数120,000
所有枚举属性的枚举值总数1,000
单个超过 250 个值的字符串枚举的总字符数15,000

Michelle Pokrass 和 OpenAI Developers 谈 schema 的可靠性

Michelle Pokrass 在 OpenAI 工作。结构化输出在 2024-08-06 发布,她当天就在 X 上宣布了它,说模型输出从此会遵循你给的 JSON schema。她还给出 OpenAI 自己的数字:开启这个功能后 schema 可靠性是 100%,而之前的 GPT-4o 模型是 86%。

这些数字来自 OpenAI 当年对 GPT-4o 的测试,不是当前模型的成绩。道理倒是没变:开启 strict 之后,schema 是被强制执行的,不是靠提示词去求。

OpenAI Developers 在 2025-05-20 发帖说,strict 模式现在能和并行函数调用一起用,支持的 schema 关键字也多了很多:字符串可以用正则或 email 这类格式,数字可以设范围,数组可以限定元素个数。它们就是本课 schema 规则里的 pattern、format、minimum 和 minItems。把这些限制写进 schema。只有微调模型会跳过它们,也只有那时,你的代码才需要再查一遍。

开启这个功能后,schema 可靠性现在是 100%,而我们之前的 gpt-4o 模型是 86%。

Michelle Pokrass, 在 OpenAI 工作,引文为译文 · X (2026年11月5日 查阅)
Michelle Pokrass(@michpokrass)2024 年 8 月 6 日在 X 上发帖:很高兴宣布结构化输出,这是 API 里最新的功能。模型输出现在会可靠地遵循你给的 JSON schema,准确匹配参数和类型。开启这个功能后,schema 可靠性现在是 100%,而我们之前的 gpt-4o 模型是 86%。
Michelle Pokrass 在 X 上,2024 年 8 月 6 日
OpenAI Developers(@OpenAIDevs)2025 年 5 月 20 日在 X 上发帖:我们对结构化输出做了一些改进。并行函数调用现在可以和 strict 模式一起用,调用会可靠地遵循 schema。支持的关键字多了很多:用正则或 email 这类格式限定字符串,数字的最小值和最大值,数组元素的最少和最多个数。
OpenAI Developers 在 X 上,2025 年 5 月 20 日

X 官方嵌入卡片的截图,拍摄于 2026-11-05,时间为 UTC。点击截图可到 X 上打开原帖。X 用 Show more 截断了 OpenAI Developers 的帖子。

常见错误

  • 还没检查拒答或未完成状态,就去解析 output_text。
  • 把 completed 状态当成解析结果一定存在、可以直接使用的证明。
  • 为了让字段可选,把它从 required 里去掉,结果请求失败。
  • 把 Zod 判别联合用作根 schema。
  • 每次请求都换 schema,每次都付第一次请求的延迟。
  • 手写的 schema 和另外定义的类型各管各的,慢慢就对不上了。
  • 模型支持结构化输出,却选了 JSON 模式。
  • 在严格模式的 schema 里用 allOf 或 if 加 then,而这个子集并不支持它们。
  • 发送用户输入时,没说明输入对不上结构时该返回什么。

自己试一试

  • 运行数学辅导示例,再把 math_reasoning.steps 一步一行地打印出来。
  • 把 final_answer 改成 str | None,问一个无解的问题。会返回什么?
  • 给 Zod 日历示例发一句不含任何事件的话,再为这种情况加一条指示。
  • 把 max_output_tokens 设得足够低,让回复被截断,读一读 incomplete_details。
  • 把数学 schema 手写成 json_schema,去掉 additionalProperties,读一读报错。
  • 写一个 Zod 判别联合,把它作为根节点传入,看请求怎样失败。
  • 用一个无害的问题运行指南里的内容审核示例,打印 category。

接下来

第 4 课往上走一层,进入 OpenAI Agents SDK。那里的智能体可以接收一个 output_type,也就是一个 Pydantic 模型或 Zod schema,SDK 会用同一套结构化输出机制,让它的最终回答符合这个结构。

SDK 还会替你运行第 2 课的工具调用循环,在智能体之间交接工作,用护栏检查输入,并把每一步记进追踪记录。这些都建立在本课程到目前为止亲手发出的调用之上。那一课的示例用 Python,文档给出 TypeScript 写法的地方也会一并列出。

自测一下

  1. 用 Pydantic 类调用 client.responses.parse 之后,带类型的对象在哪里?

    查看答案

    A. response.output_parsed parse 辅助方法把你那个类的实例放在 output_parsed 里。

  2. 你的 schema 需要一个可选字段,在结构化输出里该怎么写?

    查看答案

    C. 保留在 required 里,并在类型里允许 null 每个字段都必须在 required 里,所以可选字段要留在 required 中,并把 null 列为允许的类型。

  3. 模型出于安全原因拒答,这会怎样体现?

    查看答案

    B. 出现一个类型为 refusal 的内容部分 拒答作为消息里单独的内容类型返回,而状态仍可能是 completed。

  4. 你用 json_object 打开 JSON 模式,上下文里必须有什么?

    查看答案

    D. JSON 这个词 上下文里没有出现 JSON 这个词,API 就会对 JSON 模式报错。

常见问题

结构化输出的回复还会出错吗?

会。结构被 schema 约束住了,但里面的值仍然可能有错。遇到这种情况,指南建议把指示写得更清楚,在系统指示里给出示例,或者把任务拆成更简单的子任务。你的应用依赖的那些值,比如总数或日期,仍要像检查任何模型输出一样去核对。

能在对话中途更换 schema 吗?

能,前提是在标准单智能体模式下使用受支持的 GPT-6 及之后的模型。在下一条用户消息之前,往 input 里追加一个带新 text.format 的 configuration_update 项,每次请求都保留原来顶层的格式,这样提示词缓存仍然有效。每次更新都会替换整个格式,用 {"type": "text"} 可以切回纯文本。更新只接受 json_schema 和 text 两种。

除了 Responses,还有哪些接口支持结构化输出?

Chat Completions、Assistants、Fine-tuning 和 Batch 都支持,JSON 模式也一样。在 Chat Completions 里,schema 放在类型为 json_schema 的 response_format 里,需要 gpt-4o-mini、gpt-4o-2024-08-06 或更新的快照。

用函数调用时,JSON 模式是打开的吗?

是的。指南说明,用函数调用时 JSON 模式始终打开,所以参数总是合法的 JSON。完全符合你的 schema 则是另一项保证,需要在每个函数上打开 strict,函数调用指南对此有详细说明。严格模式的函数遵守的 schema 规则,和这一课讲的完全相同。

资料来源

  1. OpenAI Docs: Structured model outputs (2026年11月5日 查阅)https://developers.openai.com/api/docs/guides/structured-outputs
  2. OpenAI Docs: Function calling (2026年11月5日 查阅)https://developers.openai.com/api/docs/guides/function-calling
  3. OpenAI Docs: Migrate to the Responses API (2026年11月5日 查阅)https://developers.openai.com/api/docs/guides/migrate-to-responses
  4. OpenAI Docs: Changelog (2026年11月5日 查阅)https://developers.openai.com/api/docs/changelog
  5. X: Michelle Pokrass announces Structured Outputs (2026年11月5日 查阅)https://x.com/michpokrass/status/1820881057824305567
  6. X: OpenAI Developers on strict mode with parallel calls and new schema keywords (2026年11月5日 查阅)https://x.com/OpenAIDevs/status/1924915341052019166

分享本节课

看看输出干净数据的智能体

在 Mindket 浏览其他开发者上架的 AI 智能体,看看每个怎样规定自己的输出,再把你自己的也上架。

浏览 AI 智能体

本节课的 Markdown 版本 · OpenAI API 实战课:Responses、工具调用、结构化输出与智能体 · 全部课程