它保证什么,不保证什么
Structured Outputs 的承诺是“符合 schema”:必填键都在、枚举值在你给的集合里、类型对得上。它不承诺内容是对的——schema 只能约束形状,不能约束事实。校验通过但内容是编的,这种情况仍然要靠你自己的业务校验挡住。
| 能力 | JSON mode | Structured 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,形状类似。
{
"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
}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,同时把返回值解析成对象,省掉一次手写映射。
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)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 的一部分:每一步给一个说明和一个中间结果,最后单独给最终答案。这样解释是能被程序校验、也能被前端分块渲染的。
{
"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 解析失败”,掩盖真正的原因。
官方文档与参考
本文依据上述官方文档整理;价格、额度与功能会随官方更新变化,第三方中转站的行为以其自身说明为准。