结构化模型输出
JSON 是当今应用之间交换数据时最广泛使用的格式之一。
Structured Outputs(结构化输出) 是一项能力,它可以确保模型始终按照你提供的 JSON Schema 来生成响应,因此你不需要再担心模型遗漏必填字段,或幻觉出无效的枚举值。
Structured Outputs 的一些主要优势包括:
- 可靠的类型安全: 无需再对格式错误的响应进行额外校验或重试
- 显式拒绝: 基于安全策略的模型拒绝现在可以被程序明确识别
- 更简单的提示词: 不再需要通过非常强硬的提示词来强制模型保持固定格式
除了在 REST API 中支持 JSON Schema 外,OpenAI 的 Python 与 JavaScript SDK 还提供了便捷方式,可分别通过 Pydantic 与 Zod 来定义对象模式。这样你就可以更轻松地从非结构化文本中提取符合代码中定义 schema 的信息。
支持的模型
Structured Outputs 适用于我们最新的大语言模型,从 GPT-4o 开始支持。较早模型如 gpt-4-turbo 及更早版本,可能需要使用 JSON mode。
何时使用 Structured Outputs:函数调用 vs text.format
Structured Outputs 在 OpenAI API 中有两种主要形式:
- 结合函数调用(function calling)使用
- 使用
json_schema响应格式
当你构建的是一个把模型与应用功能、外部数据或工具连接起来的系统时,函数调用会更合适。
例如,你可以让模型访问一些函数:
- 查询数据库
- 帮助用户处理订单
- 与 UI 进行交互
相对地,如果你的目标是让模型在直接回复用户时就输出符合某个结构化格式的数据,而不是让模型去调用工具,那么通过 response_format 使用 Structured Outputs 会更合适。
例如,如果你正在做一个数学辅导应用,你可能希望助手使用固定 JSON Schema 回答用户,这样你的前端 UI 就可以把模型输出的不同字段分别渲染到不同位置。
简单来说:
- 如果你是要把模型连接到系统里的工具、函数、数据等,应该使用函数调用
- 如果你是要让模型回复用户时输出固定结构,则应该使用结构化的
text.format
本指南接下来的重点是:在 Responses API 中,非函数调用场景下如何使用 Structured Outputs。
如果你希望了解如何在函数调用中使用 Structured Outputs,请参考 Function Calling 指南。
Structured Outputs 与 JSON mode 的区别
Structured Outputs 是 JSON mode 的演进版本。
虽然它们都能确保输出的是合法 JSON,但只有 Structured Outputs 能进一步确保输出符合你定义的 schema。
Responses API、Chat Completions API、Assistants API、Fine-tuning API 和 Batch API 都支持 Structured Outputs 与 JSON mode。
我们建议:只要条件允许,始终优先使用 Structured Outputs,而不是 JSON mode。
不过,使用 response_format: { type: "json_schema", ... } 的 Structured Outputs,仅支持以下模型快照及更高版本:
gpt-4o-minigpt-4o-mini-2024-07-18gpt-4o-2024-08-06
| 对比项 | Structured Outputs | JSON Mode |
|---|---|---|
| 输出合法 JSON | 是 | 是 |
| 严格符合 schema | 是(参见支持的 schema) | 否 |
| 兼容模型 | gpt-4o-mini、gpt-4o-2024-08-06 及之后版本 | gpt-3.5-turbo、gpt-4-* 和 gpt-4o-* |
| 启用方式 | text: { format: { type: "json_schema", strict: true, schema: ... } } | text: { format: { type: "json_object" } } |
示例
使用 text.format 搭配 Structured Outputs
下面的示例展示了如何在数学辅导场景中,让模型按固定结构输出步骤与最终答案。
Chat Completions API:处理拒绝响应
Python
python
class Step(BaseModel):
explanation: str
output: str
class MathReasoning(BaseModel):
steps: list[Step]
final_answer: str
completion = client.chat.completions.parse(
model="gpt-4o-2024-08-06",
messages=[
{"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"},
],
response_format=MathReasoning,
)
math_reasoning = completion.choices[0].message
# If the model refuses to respond, you will get a refusal message
if math_reasoning.refusal:
print(math_reasoning.refusal)
else:
print(math_reasoning.parsed)JavaScript
javascript
const Step = z.object({
explanation: z.string(),
output: z.string(),
});
const MathReasoning = z.object({
steps: z.array(Step),
final_answer: z.string(),
});
const completion = await openai.chat.completions.parse({
model: "gpt-4o-2024-08-06",
messages: [
{ 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" },
],
response_format: zodResponseFormat(MathReasoning, "math_reasoning"),
});
const math_reasoning = completion.choices[0].message;
// If the model refuses to respond, you will get a refusal message
if (math_reasoning.refusal) {
console.log(math_reasoning.refusal);
} else {
console.log(math_reasoning.parsed);
}Responses API:处理拒绝响应
Python
python
class Step(BaseModel):
explanation: str
output: str
class MathReasoning(BaseModel):
steps: list[Step]
final_answer: str
response = client.responses.parse(
model="gpt-4o-2024-08-06",
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,
)
for output in response.output:
if output.type != "message":
raise Exception("Unexpected non message")
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)JavaScript
javascript
const Step = z.object({
explanation: z.string(),
output: z.string(),
});
const MathReasoning = z.object({
steps: z.array(Step),
final_answer: z.string(),
});
const response = await openai.responses.parse({
model: "gpt-4o-2024-08-06",
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: zodTextFormat(MathReasoning, "math_response"),
},
});
for (const output of response.output) {
if (output.type != "message") {
throw new Error("Unexpected non message");
}
for (const item of output.content) {
if (item.type == "refusal") {
// If the model refuses to respond, you will get a refusal message
console.log(item.refusal);
continue;
}
if (!item.parsed) {
throw new Error("Could not parse response");
}
console.log(item.parsed);
}
}使用 Structured Outputs 时的拒绝响应
当你在用户生成输入的场景中使用 Structured Outputs 时,OpenAI 模型有时会因为安全原因拒绝执行请求。由于拒绝响应不一定遵循你在 response_format 中提供的 schema,因此 API 响应中会增加一个新的 refusal 字段,用于表明模型拒绝了该请求。
当 refusal 属性出现在输出对象中时,你可以:
- 在 UI 中向用户展示拒绝原因
- 在代码中加入条件逻辑,专门处理拒绝场景
Chat Completions API 中的拒绝响应示例
json
{
"id": "chatcmpl-9nYAG9LPNonX8DAyrkwYfemr3C8HC",
"object": "chat.completion",
"created": 1721596428,
"model": "gpt-4o-2024-08-06",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"refusal": "I'm sorry, I cannot assist with that request."
},
"logprobs": null,
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 81,
"completion_tokens": 11,
"total_tokens": 92,
"completion_tokens_details": {
"reasoning_tokens": 0,
"accepted_prediction_tokens": 0,
"rejected_prediction_tokens": 0
}
},
"system_fingerprint": "fp_3407719c7f"
}Responses API 中的拒绝响应示例
json
{
"id": "resp_1234567890",
"object": "response",
"created_at": 1721596428,
"status": "completed",
"completed_at": 1721596429,
"error": null,
"incomplete_details": null,
"input": [],
"instructions": null,
"max_output_tokens": null,
"model": "gpt-4o-2024-08-06",
"output": [{
"id": "msg_1234567890",
"type": "message",
"role": "assistant",
"content": [
{
"type": "refusal",
"refusal": "I'm sorry, I cannot assist with that request."
}
]
}],
"usage": {
"input_tokens": 81,
"output_tokens": 11,
"total_tokens": 92,
"output_tokens_details": {
"reasoning_tokens": 0
}
}
}提示与最佳实践
处理用户生成输入
如果你的应用会接收用户生成内容,请确保提示词中包含“当输入无法生成有效响应时该如何处理”的明确说明。
模型始终会尽量遵循你提供的 schema,如果输入与任务完全无关,这反而可能导致模型产生幻觉式填充。
你可以在提示词中指定:
- 如果输入与任务不兼容,就返回空参数
- 或返回一条固定说明语句
处理错误
Structured Outputs 依然可能包含错误。
如果你发现输出有误,可以尝试:
- 调整指令
- 在 system 指令中加入示例
- 把复杂任务拆成更简单的子任务
如需进一步优化输入方式,请参考 提示词工程指南。
避免 JSON Schema 与代码类型定义分叉
为了避免 JSON Schema 与编程语言中的类型定义长期演化后出现偏差,我们强烈建议使用原生的 Pydantic / Zod SDK 支持。
如果你更倾向于手写 JSON Schema,那么建议:
- 在 CI 中加入检查规则,当 JSON Schema 或底层数据对象变化时发出警告
- 或在 CI 里自动从类型定义生成 JSON Schema(反之亦可)
Streaming
结构化输出同样可以配合流式响应使用。
Supported schemas
不同模型与接口对 schema 特性的支持程度不同,具体请参考官方文档中的支持列表。
JSON mode
JSON mode 是 Structured Outputs 的更基础版本。
它能够确保模型输出的是合法 JSON,但不能像 Structured Outputs 那样,可靠地保证输出严格符合你定义的 schema。对于大多数使用场景,我们建议优先使用 Structured Outputs。
当启用 JSON mode 时,模型输出会被确保为合法 JSON,但在某些边界情况下,你仍然需要自行检测并处理异常。
在 Responses API 中启用 JSON mode,可以将 text.format 设置为:
json
{ "type": "json_object" }如果你正在使用函数调用,则 JSON mode 会始终自动开启。
重要说明
- 使用 JSON mode 时,你必须在对话中的某条消息里明确要求模型输出 JSON,例如在 system message 中写明。如果上下文中完全没有出现 “JSON” 字样,模型可能会持续输出空白字符,直到达到 token 限制;为避免你遗漏这一点,API 也会在上下文中不存在字符串
"JSON"时直接报错。 - JSON mode 只能保证输出是合法 JSON,而不能保证它符合某个具体 schema。如果你需要严格匹配 schema,应优先使用 Structured Outputs;如果无法使用,则应结合校验库与必要重试来保证输出符合预期。
- 你的应用仍需自行处理一些边界情况,例如模型输出不是一个完整的 JSON 对象。
处理边界情况
在生产环境中,建议对 JSON mode 输出加入:
- 解析失败重试
- 结构校验
- 截断或不完整 JSON 检测
资源
想进一步了解 Structured Outputs,可以继续参考以下资源:
- 查看官方的Structured Outputs 入门 cookbook
- 学习如何借助 Structured Outputs 构建多智能体系统