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

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

Mindket 编辑部 · 发布于 2026-11-05 · 更新于 2026-11-05 · https://mindket.com/cn/learn/openai-api/openai-structured-outputs

## 要点

- 在代码里把结构只定义一次，让 SDK 据此生成 schema。
- 先判断拒答和未完成状态，再相信解析出来的对象。
- 告诉模型，输入对不上这个结构时该返回什么。
- 根节点保持为普通对象，可选字段写成可为空。
- 模型要做事时用工具调用，模型要回答时用回复格式。

## 开始之前

你需要第 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。

指南划的界线很简单。模型要连接你的函数、数据或工具时，用[函数调用](https://mindket.com/cn/learn/openai-api/openai-function-calling)。你想规定模型回复的样子时，比如一个数学辅导应用要在界面上逐步渲染解题步骤，就用 `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` 字段。

````python
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 上限。只有在模型不支持结构化输出时才用它，并且要配上校验库和重试。

````javascript
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;
````

|  | 结构化输出 | JSON 模式 |
| --- | --- | --- |
| 合法的 JSON | 是 | 是 |
| 符合你的 schema | 是，限于支持的子集 | 否 |
| 怎样打开 | `text.format` 类型设为 `json_schema`，并设 `strict: true` | `text.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 里写明这种情况下该返回什么，比如空字段或者一句固定的话。少了这条指示，一条凭空编出、看着却很正常的记录，到了下游就很难被发现。

````python
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. **2024年8月6日** 结构化输出发布。模型输出可靠地遵循你提供的 JSON Schema。
2. **2025年3月11日** Responses API 发布。一个为智能体和工具打造的新 API，也就是本课程用的那个。
3. **2025年5月20日** strict 模式支持并行调用，关键字也更多了。字符串的正则和 `email` 这类格式加入了支持的子集。

来源：[OpenAI Docs: Changelog](https://developers.openai.com/api/docs/changelog) (2026-11-05 查阅)

| 限制项 | 上限 |
| --- | --- |
| 整个 schema 的对象属性数 | 5,000 |
| 嵌套层数 | 10 |
| 属性名、定义名、枚举值和常量值的总字符数 | 120,000 |
| 所有枚举属性的枚举值总数 | 1,000 |
| 单个超过 250 个值的字符串枚举的总字符数 | 15,000 |

## Michelle Pokrass 和 OpenAI Developers 谈 schema 的可靠性

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

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

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

> 开启这个功能后，schema 可靠性现在是 100%，而我们之前的 gpt-4o 模型是 86%。
>
> Michelle Pokrass, 在 OpenAI 工作，引文为译文, [X](https://x.com/michpokrass/status/1820881057824305567) (2026-11-05 查阅)

[![Michelle Pokrass（@michpokrass）2024 年 8 月 6 日在 X 上发帖：很高兴宣布结构化输出，这是 API 里最新的功能。模型输出现在会可靠地遵循你给的 JSON schema，准确匹配参数和类型。开启这个功能后，schema 可靠性现在是 100%，而我们之前的 gpt-4o 模型是 86%。](https://mindket.com/blog/shots/x-michpokrass-structured-outputs.webp?v=b288c5c35c34)](https://x.com/michpokrass/status/1820881057824305567)

Michelle Pokrass 在 X 上，2024 年 8 月 6 日

[![OpenAI Developers（@OpenAIDevs）2025 年 5 月 20 日在 X 上发帖：我们对结构化输出做了一些改进。并行函数调用现在可以和 strict 模式一起用，调用会可靠地遵循 schema。支持的关键字多了很多：用正则或 email 这类格式限定字符串，数字的最小值和最大值，数组元素的最少和最多个数。](https://mindket.com/blog/shots/x-openaidevs-parallel-strict.webp?v=88338d1baf8f)](https://x.com/OpenAIDevs/status/1924915341052019166)

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 课](https://mindket.com/cn/learn/openai-api/openai-agents-sdk)往上走一层，进入 OpenAI Agents SDK。那里的智能体可以接收一个 `output_type`，也就是一个 Pydantic 模型或 Zod schema，SDK 会用同一套结构化输出机制，让它的最终回答符合这个结构。

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

## 自测一下

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

   A. response.output_parsed
   B. response.output_text
   C. response.text.format
   D. response.parsed_output

   答案：A。 parse 辅助方法把你那个类的实例放在 output_parsed 里。

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

   A. 把它从 required 里去掉
   B. 给它一个默认值
   C. 保留在 required 里，并在类型里允许 null
   D. 把 additionalProperties 设为 true

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

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

   A. 返回 400 错误
   B. 出现一个类型为 refusal 的内容部分
   C. 状态变成失败，没有任何输出
   D. 出现一个 function_call 项

   答案：B。 拒答作为消息里单独的内容类型返回，而状态仍可能是 completed。

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

   A. 一份 JSON Schema
   B. 设为 true 的 strict
   C. 一个 Pydantic 模型
   D. JSON 这个词

   答案：D。 上下文里没有出现 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 规则，和这一课讲的完全相同。

## 资料来源

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

[浏览 AI 智能体](https://mindket.com/cn/agents)
