如果 Agent 的下游是代码,那么「模型说了什么」就必须变成「程序能解析什么」。结构化输出是 Agent 从玩具走向系统的第一道门槛。
为什么需要它
自由文本适合给人看,不适合给程序用:
模型:好的,我建议退款,金额大概是 299 元,应该 1 到 3 天到账。
程序:???结构化输出把同一件事变成:
{
"action": "refund",
"order_id": "A1024",
"amount": 299.0,
"currency": "CNY",
"eta_days": 3,
"confidence": 0.92
}程序可以校验、可以入库、可以触发审批。模型仍然负责判断,但接口是确定的。
三种约束强度
| 方式 | 做法 | 稳定性 | 适用 |
|---|---|---|---|
| 提示要求 | 「请返回 JSON」 | 低 | 原型、内部试验 |
| JSON 模式 | 强制输出合法 JSON | 中 | 结构简单、字段少 |
| Schema 严格模式 | 按 JSON Schema 约束字段与类型 | 高 | 生产、工具参数 |
严格模式(不同厂商叫法不同:strict、json_schema、response_format)会约束解码过程,显著降低「多一个字段」「少一个引号」的问题。但它通常有约束:
- 所有字段必须
required(可选字段用["string", "null"]表达); - 不支持所有 JSON Schema 特性(如复杂的
oneOf、递归); - 首次使用有编译/缓存开销;
- 模型仍可能填错语义,格式对不等于内容对。
Schema 能保证 amount 是数字,但不能保证它等于订单金额。金额、权限、状态流转必须由服务端校验。
设计 Schema 的七个原则
1. 扁平优先
嵌套越深,模型越容易丢字段。能扁平就扁平;确实需要嵌套时,控制在两层以内。
2. 用枚举代替自由文本
{
"category": {
"type": "string",
"enum": ["billing", "technical", "refund", "other"]
}
}枚举既约束输出,也让下游分支更清晰。不要用「billing 或账单相关」这种模糊描述。
3. 必填项就是必填项
如果某个字段在业务上必须存在,不要给它默认值糊弄过去。让校验失败,比让空值流入数据库安全。
4. 可选字段用 null,不要用空字符串
{ "coupon_code": { "type": ["string", "null"] } }空字符串会被当成有效值,导致「用了优惠券但没填」这类脏数据。
5. 描述写给模型看
{
"amount": {
"type": "number",
"description": "退款金额,单位为元,最多两位小数,不得超过订单实付金额"
}
}字段名之外,description 是模型理解语义的主要来源。把单位、范围、边界写清楚。
6. 避免 oneOf 和深层递归
很多严格模式不支持。用「类型字段 + 可选字段」的扁平结构替代:
{
"kind": { "enum": ["text", "image", "file"] },
"text": { "type": ["string", "null"] },
"image_url": { "type": ["string", "null"] },
"file_id": { "type": ["string", "null"] }
}7. 给模型留「不知道」的出口
{
"answer": { "type": ["string", "null"] },
"confidence": { "type": "number" },
"missing_info": { "type": "array", "items": { "type": "string" } }
}没有出口,模型只能编。允许 null 和 missing_info,能显著降低幻觉。
一个完整的 Schema
{
"name": "support_ticket",
"strict": true,
"schema": {
"type": "object",
"properties": {
"category": {
"type": "string",
"enum": ["billing", "technical", "refund", "other"],
"description": "工单主分类;无法判断时用 other"
},
"priority": {
"type": "string",
"enum": ["low", "normal", "high", "urgent"],
"description": "urgent 仅用于服务不可用或资金损失"
},
"summary": {
"type": "string",
"description": "一句话概括,不超过 80 字,不包含用户隐私"
},
"order_id": {
"type": ["string", "null"],
"description": "订单号;用户未提供时为 null,不要猜测"
},
"needs_human": {
"type": "boolean",
"description": "是否涉及退款、法律、账号安全等必须人工处理的场景"
},
"confidence": {
"type": "number",
"minimum": 0,
"maximum": 1
}
},
"required": ["category", "priority", "summary", "order_id", "needs_human", "confidence"],
"additionalProperties": false
}
}校验与修复循环
即使有严格模式,也要在服务端校验。失败时不要直接抛错,而是把错误喂回模型修复一次:
from pydantic import BaseModel, Field, ValidationError
import json
class Ticket(BaseModel):
category: str = Field(pattern="^(billing|technical|refund|other)$")
priority: str
summary: str = Field(max_length=80)
order_id: str | None
needs_human: bool
confidence: float = Field(ge=0, le=1)
def parse_ticket(raw: str) -> Ticket:
try:
return Ticket.model_validate_json(raw)
except ValidationError as e:
repair = client.chat.completions.create(
model=MODEL,
messages=[
{"role": "system", "content": "只修正 JSON 格式与字段,不要改变语义。"},
{"role": "user", "content": raw},
{"role": "user", "content": f"校验错误:{e.errors()}"},
],
response_format={"type": "json_object"},
temperature=0,
)
return Ticket.model_validate_json(repair.choices[0].message.content)三个细节:
- 只修复一次。两次还失败,说明任务或 Schema 有问题,应该降级或转人工。
- 保留原始输出。修复失败时用于排查。
- 记录失败率。按字段统计,能发现是哪个字段设计得不合理。
流式输出下的结构化
流式返回 JSON 时,内容是不完整的:
{"category": "bill不要尝试 JSON.parse。两种处理方式:
- 用增量解析器(如
ijson、json-stream),只消费已经完整的字段; - 让模型先输出一个短的状态字段,完整结果在最后一次性给出。
对 UI 而言,通常只需要流式展示 summary 这类文本字段,其他字段等完整后再生效。
工具参数也是结构化输出
Function Calling 本质上就是「按工具 Schema 生成结构化输出」。所以本文的原则同样适用:
- 参数名清晰、描述完整;
- 必填项真实必填;
- 枚举代替自由文本;
- 不要让模型传它不知道的 ID(先查再传);
- 服务端必须重新校验参数。
常见失败与修复
| 现象 | 可能原因 | 修复 |
|---|---|---|
| 多出未声明字段 | 未开严格模式 / 描述不清 | additionalProperties: false,精简 Schema |
| 枚举值拼错 | 枚举太多或含义重叠 | 减少选项,加「其他」出口 |
| 数字变字符串 | 类型约束弱 | 严格模式 + 服务端转换 |
| 该为 null 却编造 | 没有 null 出口 | 允许 null,并在描述中说明 |
| 嵌套字段丢失 | 结构太深 | 扁平化,拆成两次调用 |
| 格式对但语义错 | 缺少业务校验 | 服务端校验金额、ID、权限 |
| 修复循环不收敛 | 错误信息太模糊 | 返回字段级错误与允许值 |
什么时候不该用
- 纯给人看的创作:散文、营销文案,硬套 JSON 只会伤害质量;
- 结构极度开放:让模型先自由探索,再用第二次调用抽取;
- 超长输出:一次生成巨大 JSON 容易截断,改为分页或分步;
- 需要精确计算:让模型输出表达式,由代码计算,而不是让它算。
小结
- 结构化输出让模型成为可编程组件,严格模式是基础。
- Schema 设计要扁平、用枚举、真实必填、给 null 出口、写清描述。
- 格式正确不代表语义正确,业务校验不可省。
- 失败时做一次定向修复,并统计字段级失败率。
- 工具参数就是结构化输出,同一套原则适用。
下一篇讲工具本身:如何设计模型愿意用、也用得对的接口。