3.2 结构化输出:让模型的话可被程序读
一句话结论:在 Agent 里,模型的输出几乎总要被程序读取。这意味着"格式是否稳定"不是美观问题,而是功能问题——格式解析失败,整个循环就断了。
读这篇之前
需要先读 3.1(三件套)和 2.4(温度与稳定性)。这一篇是把 3.1 的"输出要求"那一项彻底展开。
一、为什么这件事比想象中重要
先算一笔账。在一个典型 Agent 循环里(1.3 讲过),每一轮都要做:
模型输出 → 你的代码解析 → 校验 → 执行 → 结果回填 → 再输出
"你的代码解析"这一步,是纯程序逻辑,容不得半点含糊。
一旦模型输出不合法:
| 情况 | 后果 |
|---|---|
| JSON 少一个括号 | 解析异常,整轮失败 |
| 字段名拼错 | 校验失败,或更糟——静默用了默认值 |
| 类型不对(字符串 vs 数字) | 下游工具报错 |
| 多了一段解释文字 | 解析器不知道该从哪开始 |
而且这些问题不是偶发的:2.4 讲过,采样有随机性;2.3 讲过,模型会"顺手"完善格式。输出越长、嵌套越深,越容易在细枝末节上出错。
二、六个结构单元:把提示词写成"有骨架的"
在讲格式约束之前,先给一个通用骨架。生产级提示词通常由六个结构单元构成:[原文 707]
| 单元 | 作用 | 必填 |
|---|---|---|
| Role 角色 | 设定身份,决定知识边界与回答风格 | ✅ |
| Directive 核心指令 | 描述要做什么 | ✅ |
| Context 背景信息 | 提供决策依据 | 可选 |
| Exemplars 示例 | few-shot,引导输出格式与风格 | 可选 |
| Format 格式约束 | JSON / Markdown / 表格 | 推荐 |
| Style 风格约束 | 语气、长度、细节程度 | 可选 |
注意 Format 被标为"推荐"而非"可选"——因为只要输出要被程序读,它就不是可选项。而 Style 才是真正可选的,它只影响可读性,不影响功能。
原文给了一个示例(天气助手):
## 角色 (Role)
你是气象专家,熟悉天气数据的解读和出行建议。
## 核心指令 (Directive)
根据用户提供的城市名称,调用天气 API 获取数据,
然后给出穿衣建议和出行推荐。
## 背景信息 (Context)
- 当前季节:{{season}}
- 用户所在区域:{{region}}
- 天气 API 返回字段:温度、湿度、风力、降水
这个骨架的价值在于:它让"格式要求"变成提示词里的一个独立区块,而不是散落在散文句子里。模型对"独立区块"的遵循度,明显高于"顺口提一句"。
三、原生结构化解码:最重要的那一条
在 Agent 系统里,最重要的输出配置不是温度,而是结构化输出。现代主流模型都支持原生 JSON 模式,它的关键特征是:由服务端保证输出是合法 JSON 且符合 schema,而不是靠提示词"请求"它。[原文 707]
一个典型的调用形态:
response = client.chat.completions.create(
model="...",
messages=[...],
response_format={
"type": "json_schema",
"json_schema": {
"name": "AgentDecision",
"strict": True, # 严格模式:不允许额外字段
"schema": {
"type": "object",
"properties": {
"thought": {"type": "string"},
"action": {"type": "string", "enum": ["call_tool","respond","ask_user"]},
"tool_name":{"type": "string"},
"tool_args":{"type": "object"},
"answer": {"type": "string"}
},
"required": ["thought","action"]
}
}
}
)
三个细节值得注意:
| 细节 | 作用 |
|---|---|
strict: True |
不允许模型加额外字段——避免它"顺手"多给几个键 |
enum 约束 action |
把"能做什么"限制在固定集合里,天然防住越权动作 |
required 只列两个 |
不是所有字段都必填;只在必要时才必填,减少失败率 |
为什么这一条比提示词技巧重要? 因为它是机制层面的保证,而不是"请求模型配合"。提示词可以被忽略,schema 校验不会。[原文 707]
如果所用 API 不支持原生结构化解码,退回到"指定 schema + 给示例 + 校验重试"的三件套,可靠度会低一档但可用。
四、写提示词的 C.L.E.A.R 原则
结构化输出解决了"格式",但提示词本身写得好不好,还有一套更通用的标准。好提示词有五个共性,可用 C.L.E.A.R 记忆:[原文 707]
| 原则 | 含义 | 反例 → 正例 |
|---|---|---|
| C — Concise | 简洁,剔除冗余 | 「帮我生成一个你觉得合适的设计」→「生成一个 SaaS 产品落地页,包含 Hero、Features、Pricing 三段」 |
| L — Logical | 有逻辑,条理分明 | 大段文字描述 → 用编号步骤:①分析需求 ②设计方案 ③输出代码 |
| E — Explicit | 明确,不留歧义 | 「写得专业一点」→「语气正式,用第三人称,每段不超过 3 句话」 |
| A — Adaptive | 可迭代优化 | 一次性写死 → 先草稿,测试效果后逐步改进 |
| R — Reflective | 可复盘复用 | 用过就丢 → 保存高质量 Prompt 到模板库,标注适用场景 |
这五条里,E(明确)是收益最高的。原话是"不留歧义"——而歧义最常见的表现就是形容词:"专业一点""简洁一点""详细一点"。这些词对模型没有约束力。
把形容词换成可数的事实:
❌ 简洁一点
✅ 每段不超过 3 句,全文不超过 300 字
❌ 专业一点
✅ 使用第三人称,避免口语词,术语首次出现给定义
A 和 R 这两条最容易被忽略,但它们决定了提示词能否积累。 一个团队如果每人都手写提示词、用完就丢,一年下来资产为零。
五、示例(Few-shot):说不如给
当你说不清想要什么时,给一个例子比写一百字描述更有效。这是 Few-shot(少样本提示)的核心思想。[原文 707]
一个典型对比:
Zero-shot(不给例子):
判断以下评论的情感:"这个产品太棒了!"
Few-shot(给例子):
判断以下评论的情感。
示例:
评论:"质量很好,推荐购买" → 正面
评论:"太差了,完全不能用" → 负面
评论:"还行吧,一般般" → 中性
现在判断:
评论:"这个产品太棒了!" →
原文给出的量级判断:Few-shot 的效果通常比 Zero-shot 好 20%~40%,尤其在分类、格式转换、风格模仿等任务上。[原文 707]
原因在 2.1 讲过:模型擅长从已有内容里"检索"模式,不擅长从抽象描述里"推理"出模式。示例把"推理"变成了"检索"。
六、让输出可靠的五个手段
从便宜到昂贵,五个手段叠加使用:
| # | 手段 | 做法 | 效果 |
|---|---|---|---|
| 1 | 指定 schema | 在提示词里给出字段、类型、必填、取值范围 | 基础 |
| 2 | 给一个完整示例 | 展示一个"合法输出长什么样" | 显著提升 |
| 3 | 用原生结构化解码 | 用模型 API 的 JSON mode / structured output 能力 | 大幅提升 |
| 4 | schema 校验 + 重试 | 解析失败就把错误信息回传,让它修 | 兜底 |
| 5 | 降温度 | 输出类环节把温度设低(2.4) | 降低抖动 |
第 3 条值得单独说明:主流模型 API 大多提供了"强制 JSON"或"按 schema 输出"的开关。这不是提示词技巧,而是服务端约束,可靠性远高于靠提示词"请求"它输出 JSON。
优先级判断:如果 API 支持结构化解码,用它;如果不支持,用"指定 schema + 给示例 + 校验重试"。
第 4 条:校验重试怎么做才对
关键在把错误信息喂回去,而不是重发同一句话:
❌ 重试时发同样的话 → 大概率错在同一个地方
✅ 重试时附上:你的输出不符合要求:字段 items 应为数组,
实际收到字符串。请修正后仅输出 JSON。
这本质上是给模型一次自我纠正的机会,而纠正的依据是具体的、可行动的反馈。错误信息的质量决定了纠正的成功率——这条原则在 2.5 讲工具层时也出现过。
七、一个具体场景:让模型输出"可执行的任务清单"
假设你要 Agent 拆解任务。一个脆弱的写法:
❌ 请把任务拆成几步列出。
它会给你一段带编号的散文,格式每次都不一样。
一个稳的写法:
请把任务拆解为步骤,仅输出 JSON,不要输出任何其他文字:
{
"steps": [
{
"id": 1,
"action": "动作名(必须是 read_file / write_file / run_test 之一)",
"target": "目标对象",
"reason": "为什么需要这一步(一句话)"
}
]
}
约束:
- steps 至少 1 个、最多 10 个
- action 只能取上述三个值,不得自创
- 若任务无法拆解,输出 {"steps": []}
注意最后那条空集约定——它给模型一个"合法退出"的出口,避免它为了满足"至少一个"而编造步骤。
这是通用技巧:凡是要求"必须输出某些内容"的 schema,都要想清楚"确实没有内容时"该怎么表达,并显式定义。
八、校验:程序侧的守门
无论提示词写得多好,程序侧都必须校验。三个层次:
| 层 | 检查什么 | 失败处理 |
|---|---|---|
| 语法 | 能否解析成 JSON | 重试或报错 |
| 结构 | 必填字段在不在、类型对不对 | 重试并给出具体缺什么 |
| 语义 | 值是否在允许范围(枚举、区间、长度) | 拒绝并说明原因 |
语义层最容易被省掉,但它是防误操作的关键。 例如:
{ "action": "delete_all_files" }
语法合法、结构合法,但 action 不在枚举里——只有语义校验能拦住它。
这就是 2.5 讲的"工具层参数校验"的第一道关。
九、动手:一个可以直接用的 JSON schema 模板
结构化输出的实操,最关键的是写出好的 schema。
模板
{
"type": "object",
"required": ["summary", "items"],
"additionalProperties": false,
"properties": {
"summary": {
"type": "string",
"description": "一句话结论,不超过 50 字"
},
"items": {
"type": "array",
"maxItems": 20,
"items": {
"type": "object",
"required": ["id", "severity", "detail"],
"properties": {
"id": {"type": "string"},
"severity": {"type": "string", "enum": ["high", "medium", "low"]},
"detail": {"type": "string", "maxLength": 300},
"evidence": {"type": ["string", "null"],
"description": "支持这条结论的材料片段;无依据填 null"}
}
}
}
}
}
五个设计要点
| 要点 | 作用 |
|---|---|
additionalProperties: false |
禁止模型加字段(3.2 strict) |
required 只列必须的 |
减少失败率 |
enum 限定取值 |
天然防越权(14.4) |
maxItems / maxLength |
防止输出失控 |
evidence 可为 null |
允许"无依据"= 允许说不知道(2.3) |
最后一条最巧妙
如果强制 evidence 非空,模型会编一个片段来通过校验。
用 ["string","null"] 允许 null,反而更诚实——它可以选择"我没有依据",而不必编造。
这是一个通用技巧:凡是"希望模型诚实"的字段,都要允许"空"。
十、常见误区
误区一:要求"输出 JSON"就够了。
不够。模型可能包一层 ```json 代码块、加一句"这是您要的 JSON"、或漏掉必填字段。要指定 schema + 给示例 + 校验。
误区二:用正则从长文本里抠 JSON。
极脆弱。嵌套结构、数组、转义字符都会让它崩。用原生结构化解码,或让模型只输出 JSON。
误区三:校验失败就重试同一句话。
大概率重复同一个错误。必须把具体错误回传。
误区四:温度对 JSON 合法性没影响。
有影响。高温会增加格式偏差概率。输出类环节必须降温度。
误区五:schema 定得越细越好。
过细的约束(比如字段长度 20-25 字)会让模型频繁违规、反复重试。约束应覆盖"必须为真"的部分,不必过度限制格式细节。
十一、结构化输出的一个隐蔽成本
结构化输出很可靠,但有一个成本常被忽略:schema 本身占 Token。
| schema 复杂度 | 大约 Token |
|---|---|
| 3 个简单字段 | 50-100 |
| 10 个字段含描述 | 300-600 |
| 嵌套数组 + 枚举 | 600-1500 |
每次调用都要发一遍。
三个优化手段
| 手段 | 效果 |
|---|---|
| 只保留必要字段的 description | 省一半 |
枚举值简短(h/m/l 而非 high/medium/low) |
省一些 |
| 把 schema 放前面以利缓存 | 单价降低(5.3) |
第三条最有效:schema 是稳定的,应该放在上下文的最前部享受缓存。
一个反例
错:每次调用重新生成 schema 字符串(顺序可能不同)
→ 缓存永远不命中
对:schema 定义为模块级常量,一次生成
→ 每次字节级一致,可缓存
十二、一个判断:什么情况下不需要 schema
不是所有输出都需要 schema。过度结构化也是成本。
| 情况 | 需要 schema 吗 |
|---|---|
| 输出给程序解析 | ✅ 必须 |
| 需要多字段分别使用 | ✅ 必须 |
| 只是给人看的一段话 | ❌ 不需要 |
| 结果要存库/传下游 | ✅ 必须 |
| 用于展示的建议文本 | ⚠️ 可选 |
判据一句话:
有没有下游程序依赖它的结构?
有 → schema
没有 → 自然语言即可
很多团队给所有输出都加 schema,结果是"提示词变长、模型变拘谨、可读性下降",但收益为零。
十三、下一篇
格式稳了,但还有一个问题:模型"想"得对不对?
有些任务需要它先想清楚再答,有些任务想了反而更差。这个差别背后是有机制的,也对应了几种不同的提示技术。
下一篇讲思维链:它到底什么时候有用,什么时候反而有害。