结构化输出
让模型稳定输出 JSON:Schema 约束、校验与失败重试。
一句话理解:结构化输出就是用 JSON Schema 约束模型的输出格式,让结果能被程序直接解析;但“格式正确”不等于“内容正确”,业务校验与失败处理仍然要由程序完成。
Agent 的输出很少只给人看。客服工单分类 Agent 的判断要写进工单系统,SEO 诊断结果要生成报表,销售分析结论要触发补货任务。如果模型有时输出 JSON、有时在 JSON 前面加一句“好的,以下是结果”、有时少一个字段,下游系统就会随机崩溃。
早期大家靠在提示词里写“请只输出 JSON”加上正则清洗来应付,效果时好时坏。现在主流 API 都提供了原生的结构化输出能力。这一课讲它怎么用、能保证什么、不能保证什么。
三种做法的可靠性对比
| 做法 | 原理 | 可靠性 | 适用场景 |
|---|---|---|---|
| 提示词要求 | 在指令里描述格式,附一个示例 | 多数时候可用,但偶尔多字、缺字段、类型错 | 原型验证、只给人看的输出 |
| 工具调用 | 把输出定义成一个工具的参数,模型“调用”它 | 较高;开启严格模式后参数符合 Schema | 模型本来就要调用工具的 Agent |
| 原生结构化输出 | API 在生成时按 Schema 约束 Token 选择 | 高:输出是合法 JSON 且符合 Schema | 需要稳定对接下游系统的结果 |
原生结构化输出的原理可以直觉地理解为:模型每生成一个 Token 时,API 会把不符合 Schema 的候选排除掉。比如 Schema 要求下一个位置是布尔值,模型就不可能写出 "是的"。
用一张图表示这个过程:
Claude:output_config.format
以客服工单分类为例。我们希望模型读完工单内容和订单数据后,输出一个固定结构的判断:
import anthropic
client = anthropic.Anthropic()
schema = {
"type": "object",
"properties": {
"ticket_id": {"type": "string"},
"issue_type": {
"type": "string",
"enum": ["发货延迟", "商品质量", "退款申请", "账单问题", "无法判断"],
},
"confidence": {"type": "string", "enum": ["高", "中", "低"]},
"evidence": {"type": "array", "items": {"type": "string"}},
"suggested_action": {"type": "string"},
"need_human_review": {"type": "boolean"},
},
"required": [
"ticket_id", "issue_type", "confidence",
"evidence", "suggested_action", "need_human_review",
],
"additionalProperties": False,
}
response = client.messages.create(
model="claude-opus-5",
max_tokens=4096,
system="你是客服工单分析助手。只依据提供的数据判断;证据不足时 issue_type 选“无法判断”。",
messages=[{"role": "user", "content": "工单内容:……\n订单数据:……"}],
output_config={"format": {"type": "json_schema", "schema": schema}},
)
返回的 text 内容块就是一段符合 Schema 的 JSON:
{
"ticket_id": "T-10231",
"issue_type": "发货延迟",
"confidence": "中",
"evidence": ["订单于 9 月 12 日 08:14 支付成功", "此后 72 小时仍无发货记录"],
"suggested_action": "核实仓库是否缺货,并向客户同步预计发货时间",
"need_human_review": false
}
如果模型本来就要调用工具,也可以在工具定义上加 strict: true,让工具参数严格符合 input_schema(同样要求 additionalProperties: false)。工具调用的完整机制见 Function Calling 原理。
OpenAI:text.format
OpenAI Responses API 的对应写法是 text.format,需要给 Schema 起一个名字,并通过 strict 开启严格模式:
from openai import OpenAI
client = OpenAI()
response = client.responses.create(
model="gpt-6-astra", # 模型名以官方文档为准
input=[
{"role": "developer", "content": "你是客服工单分析助手。只依据提供的数据判断。"},
{"role": "user", "content": "工单内容:……\n订单数据:……"},
],
text={
"format": {
"type": "json_schema",
"name": "ticket_classification",
"schema": schema, # 复用上面定义的 Schema
"strict": True,
}
},
)
print(response.output_text)
OpenAI 的文档还说明:当模型出于安全原因拒绝时,输出中会出现 refusal 类型的内容,而不是符合 Schema 的 JSON,代码需要单独处理这一分支。两家的 SDK 也都提供了配合 Pydantic 等类型库直接解析成对象的辅助方法,具体用法以各自 SDK 文档为准。
“格式保证”之外,还要做什么
原生结构化输出解决的是 语法层 的问题。一个生产级实现至少还要处理下面四件事:
1. Schema 本身有能力边界。 以 Claude 为例,官方列出了不支持的 JSON Schema 特性,包括:递归 Schema、数值约束(minimum、maximum、multipleOf)、字符串长度约束(minLength、maxLength)、minItems 为 0 或 1 以外的数组约束,以及 additionalProperties 不为 false 的情况。这意味着“金额必须大于 0”“摘要不超过 200 字”这类规则 不能指望 Schema 保证,必须写进程序校验。
2. 非正常结束时,内容可能不完整或不存在。 输出被 max_tokens 截断、模型拒答、触到上下文上限时,拿到的可能不是完整 JSON。先检查停止原因,再解析。
3. 业务语义需要程序校验。 JSON 合法、字段齐全,不代表内容对:工单号可能和输入的不一致,证据可能是编造的,need_human_review 可能在高风险情况下被设成了 false。
4. 失败要有明确的重试与兜底策略。 完整流程如下:
用 Python 表达“解析 + 业务校验”的核心部分(示意):
import json
from typing import Literal
from pydantic import BaseModel, ValidationError
class TicketClassification(BaseModel):
ticket_id: str
issue_type: Literal["发货延迟", "商品质量", "退款申请", "账单问题", "无法判断"]
confidence: Literal["高", "中", "低"]
evidence: list[str]
suggested_action: str
need_human_review: bool
def parse_result(response, input_ticket_id: str) -> TicketClassification | None:
if response.stop_reason != "end_turn":
return None # 截断、拒答等情况交给上层处理
text = next(b.text for b in response.content if b.type == "text")
try:
result = TicketClassification.model_validate(json.loads(text))
except (json.JSONDecodeError, ValidationError):
return None
# 业务规则:Schema 保证不了的,由程序保证
if result.ticket_id != input_ticket_id:
return None
if result.issue_type == "退款申请" and not result.need_human_review:
result.need_human_review = True
return result
Schema 设计的几个经验
- 用
enum代替自由文本。 问题类型、置信度、状态这类字段,枚举值能直接对接下游逻辑,也方便统计和评测。 - 一定要有“兜底值”。 比如
无法判断、数据不足。没有兜底选项时,模型只能在错误选项里硬选一个。 - 把证据和结论分开。
evidence字段让结论可追溯,也方便程序核对,这是降低 幻觉 风险的常用手段。 - 字段名和描述要自解释。 模型通过字段名和指令理解每个字段的含义,
flag1、type2这类命名会直接降低准确率。 - 别把所有东西都塞进一个巨大的 Schema。 字段越多,每个字段的质量越难保证;复杂任务可以拆成多次调用。更系统的方法见 输出契约。
常见误区
- “用了结构化输出,就不需要校验了。” 它保证的是格式,不是事实和业务规则。
- “在 Schema 里写了 maxLength,模型就不会超长。” 不少约束并不被支持或不会被强制执行,要查文档,并在程序里再校验一遍。
- “字段越细越好。” 过多字段会增加 Token 成本和出错面,只保留下游真正会消费的字段。
产品经理检查清单
- 模型输出最终被谁消费?下游需要哪些字段、什么类型、哪些枚举值?
- 每个枚举字段是否有“无法判断 / 其他”这样的兜底值?
- 哪些业务规则 Schema 无法表达,必须由程序校验?
- 解析失败、业务校验失败时,是重试、转人工,还是标记待复核?
- 是否记录了解析失败率和校验失败率,作为质量监控指标?
练习
为「SEO 诊断 Agent」设计一个结构化输出(约 45 分钟):
- 定义 Schema:每个页面输出
url、issues(数组,每项含type枚举、severity枚举、evidence、fix)和overall_score_level(枚举)。 - 挑 3 个真实网页的 HTML 片段,用 Claude 或 OpenAI 的结构化输出跑一遍。
- 写下至少 3 条 Schema 无法保证、需要程序校验的规则(例如
evidence里引用的标签必须真实出现在 HTML 中),并说明校验失败时的处理方式。
延伸阅读
- Claude Structured outputs:JSON 输出、严格工具调用与支持的 Schema 特性。
- OpenAI Structured Outputs:
text.format、严格模式与拒答处理。
下一步
格式可以被保证,内容却不行。下一课「幻觉」专门讲模型为什么会编造,以及如何用检索、引用、验证与拒答设计把风险降下来。