教程 / OpenAI 与 API

Structured Outputs:让模型稳定输出 JSON 的写法与坑

用正则去抠模型输出里的 JSON,是最容易在半夜挂掉的一环。Structured Outputs 把这件事变成接口层面的保证:你在请求里给出 schema,模型返回的内容就按这个 schema 填。这篇讲怎么用、怎么不踩坑。

更新于 2026-10-24由 GPTHubs 团队 整理约 9 分钟读完OpenAI 与 API

它保证什么,不保证什么

Structured Outputs 的承诺是“符合 schema”:必填键都在、枚举值在你给的集合里、类型对得上。它不承诺内容是对的——schema 只能约束形状,不能约束事实。校验通过但内容是编的,这种情况仍然要靠你自己的业务校验挡住。

能力JSON modeStructured Outputs
保证是合法 JSON是是
保证字段齐全否是
保证枚举值合法否是
需要提前提供 schema否是
适合场景偶尔要个 JSON 块程序要直接消费的输出

支持起点是 gpt-4o-2024-08-06 / gpt-4o-mini 之后的模型,新模型基本都支持。具体哪些模型可用,以官方文档的模型列表为准。

Responses API:用 text.format 声明 schema

Responses API 里,结构化输出写在 text.format 上:type 固定为 json_schema,name 是这个格式的名字,strict 打开严格模式,schema 就是标准的 JSON Schema。Chat Completions 走的是 response_format,形状类似。

schema 本体:一个工单分类结果
{
  "type": "object",
  "properties": {
    "category": { "type": "string", "enum": ["billing", "bug", "other"] },
    "summary": { "type": "string" },
    "order_id": { "type": ["string", "null"] },
    "confidence": { "type": "number" }
  },
  "required": ["category", "summary", "order_id", "confidence"],
  "additionalProperties": false
}
把 schema 放进请求
const response = await client.responses.create({
  model: "gpt-4o-mini",
  input: [
    {
      role: "developer",
      content: [{ type: "input_text", text: "把工单归类,只输出 JSON。" }],
    },
    { role: "user", content: [{ type: "input_text", text: ticketText }] },
  ],
  text: {
    format: {
      type: "json_schema",
      name: "ticket_classification",
      strict: true,
      schema: {
        type: "object",
        properties: {
          category: { type: "string", enum: ["billing", "bug", "other"] },
          summary: { type: "string" },
          order_id: { type: ["string", "null"] },
          confidence: { type: "number" },
        },
        required: ["category", "summary", "order_id", "confidence"],
        additionalProperties: false,
      },
    },
  },
});

返回的文本在 response.output_text 里,直接用 JSON.parse 就能拿到对象——因为格式已经被 schema 约束住了,不需要再写容错解析。

schema 的三条硬规则

strict 模式对 schema 有结构要求。不满足时会直接报错,而不是悄悄降级。这三条几乎覆盖了所有报错:

规则怎么写常见错法
禁止额外字段每个 object 都写 additionalProperties: false漏写,或写成 true
所有字段必填把所有属性名都放进 required 数组只把“重要”的字段放进 required
可选字段用 null 表达类型写成 ["string", "null"],字段仍在 required 里把字段从 required 里删掉当可选
可选字段的正确写法
{
  "type": "object",
  "properties": {
    "order_id": { "type": ["string", "null"] }
  },
  "required": ["order_id"],
  "additionalProperties": false
}

第一次接 schema 建议先用最小对象跑通,再逐步加字段。报错信息通常会指出哪个路径不合规,照着改比反复猜快。

Pydantic 与 Zod:让 schema 从类型里长出来

手写 JSON Schema 容易和代码里的类型脱节。官方的 SDK 助手可以直接从类型定义生成 schema,同时把返回值解析成对象,省掉一次手写映射。

Python:responses.parse + Pydantic
from typing import Literal, Optional
from pydantic import BaseModel

class Ticket(BaseModel):
    category: Literal["billing", "bug", "other"]
    summary: str
    order_id: Optional[str]
    confidence: float

response = client.responses.parse(
    model="gpt-4o-mini",
    input=[{"role": "user", "content": ticket_text}],
    text_format=Ticket,
)

ticket = response.output_parsed
print(ticket.category, ticket.confidence)
TypeScript:zodTextFormat + responses.parse
import { z } from "zod";
import { zodTextFormat } from "openai/helpers/zod";

const Ticket = z.object({
  category: z.enum(["billing", "bug", "other"]),
  summary: z.string(),
  orderId: z.string().nullable(),
  confidence: z.number(),
});

const response = await client.responses.parse({
  model: "gpt-4o-mini",
  input: [
    { role: "user", content: [{ type: "input_text", text: ticketText }] },
  ],
  text: { format: zodTextFormat(Ticket, "ticket") },
});

const ticket = response.output_parsed;
  • schema 从代码类型生成,改字段时只需要改一处。
  • 枚举尽量收敛在最少的几个值,值越多越容易误判。
  • 数值字段加约束前先确认模型支持(部分约束在 strict 模式下有限制)。
  • 生成的 schema 建议写进日志,线上排查时能对上当时用的是哪版。

refusal 分支必须单独处理

模型拒答时,返回的不是符合 schema 的 JSON,而是一个 type 为 refusal 的内容块。如果代码只写了一条 JSON.parse 的路径,这一支会直接抛异常,而你看到的是“解析失败”,不是“被拒绝”。

按内容块类型分支处理
const block = response.output[0]?.content?.[0];

if (block?.type === "refusal") {
  // 模型拒绝回答,refusal 字段是拒答理由
  return { ok: false, refused: true, reason: block.refusal };
}

if (block?.type === "output_text") {
  const ticket = JSON.parse(block.text);
  return { ok: true, ticket };
}

return { ok: false, refused: false, reason: "unexpected_output" };
  • 拒答和解析失败要分开记录,否则线上指标会混在一起。
  • 拒答率突然升高,先看是不是提示词把模型逼到了边界上。
  • 业务上需要兜底时,拒答应该走人工或降级流程,而不是重试同一个请求。

函数调用和结构化输出的分工

这两个能力常被混着叫,其实管的是不同的事:函数调用的 strict 模式约束的是“工具参数”——模型决定调用哪个工具、传什么参数;text.format 约束的是“模型最终交给你的输出”。一个管动作,一个管交付物。

维度函数调用(strict)结构化输出(text.format)
约束对象工具的参数模型的最终回答
触发条件模型判断需要调用工具每次回答都按 schema
输出给谁你的工具实现你的业务代码或界面
典型用途查库、下单、发消息抽取、分类、生成配置
能否同时用可以,两者不冲突可以,两者不冲突

典型组合是:模型先按 schema 输出一个“要不要查库”的判断和查询参数,你的代码执行查询,再把结果交回去,最后仍按 schema 输出结论。两步都用同一个思路——把形状交给 schema,把判断留给模型。

需要推理过程时,把思路也结构化

有时你要的不只是答案,还要为什么。与其在提示词里要“详细解释”,不如把解释变成 schema 的一部分:每一步给一个说明和一个中间结果,最后单独给最终答案。这样解释是能被程序校验、也能被前端分块渲染的。

带中间步骤的 schema
{
  "type": "object",
  "properties": {
    "steps": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "explanation": { "type": "string" },
          "output": { "type": "string" }
        },
        "required": ["explanation", "output"],
        "additionalProperties": false
      }
    },
    "final_answer": { "type": "string" }
  },
  "required": ["steps", "final_answer"],
  "additionalProperties": false
}
  • 步骤数量限制在合理范围(例如最多 5 步),否则模型会为了凑步数硬拆。
  • explanation 和 output 分开,前端可以只展示 output,调试时再展开解释。
  • final_answer 不要依赖 steps 的字符串拼接,单独给字段更稳。
  • 先做一版只有一个字段的最小 schema 跑通链路,再加结构。

常见问题

Structured Outputs 能保证内容正确吗?

不能。它保证结构符合 schema:字段齐全、类型正确、枚举在范围内。内容是否真实、判断是否合理,仍然要靠提示词、业务校验和人工复核。

为什么我的 schema 一直报错?

最常见是三条规则没满足:某个 object 漏了 additionalProperties: false、有字段没写进 required、可选字段用删除 required 的方式表达。注意规则对嵌套对象同样生效。

有了结构化输出还需要 JSON mode 吗?

大多数程序消费的场景直接上 Structured Outputs 更省事。JSON mode 只保证是合法 JSON,字段和枚举都不保证,适合输出结构不固定、只是偶尔要个 JSON 的场合。

模型拒答时会发生什么?

返回内容里会出现 type 为 refusal 的块,而不是一条符合 schema 的 JSON。代码里要单独分支处理,否则会表现成“JSON 解析失败”,掩盖真正的原因。

官方文档与参考

本文依据上述官方文档整理;价格、额度与功能会随官方更新变化,第三方中转站的行为以其自身说明为准。