用户让 Agent 改一个配置文件的三个字段。它读了一遍文件,改对了两个,第三个改成了"看起来对但语义不同"的值,然后报告"已完成"。复盘时说"模型能力不够"就结束了——但真正的问题在任务描述里:第三个字段的名字在当前文件里出现过两次,而描述里只说了一次"改 port"。
"Agent 没做好"不是一个可修的结论。可修的结论必须落到具体的一层:是输入的信息不足、是工具的返回不明确、是任务描述有歧义、还是模型的判断错。这四层对应的修法完全不同。
1. 复盘的四个层次
| 层 | 问题形态 |
|---|---|
| 输入 | 需要的信息不在上下文里 |
| 工具 | 返回不足以支持下一步判断 |
| 描述 | 指令有歧义或多解 |
| 判断 | 信息齐全但结论错 |
四层还有一个顺序特性:信息不足会伪装成判断错误。上下文缺一个字段,模型只能按现有信息给出最合理的结论,而这个结论在事后看就是"判断错"。所以归因必须从第一层往第四层查,不能跳。
多数失败集中在第一层与第三层,而团队习惯归因到第四层——因为"模型不行"最容易说出口,也最不需要改任何东西。
2. 逐层取证
| 层 | 取证方式 |
|---|---|
| 输入 | 检查该轮的完整上下文 |
| 工具 | 看工具返回的原始内容 |
| 描述 | 换个人读一遍,看会不会有第二种理解 |
| 判断 | 把信息补齐后重跑,看是否仍错 |
第一层的取证要看得足够细:不是"上下文里提没提",而是"提到时是否唯一、是否带单位、是否在关键位置"。同一个词在上下文里出现五次,信息量反而下降。
第四项是最有分辨力的实验:如果补齐信息后它就做对了,那问题从来不是"能力"。这个实验只需要重跑一次,成本极低。
第二项的取证常常有意外:工具返回了 200 但 body 里是错误信息,Agent 据此继续往下做,于是错在后面几步。这类问题看起来像"判断错",实际是"工具撒谎"。
3. 任务描述的三类缺陷
| 缺陷 | 例子 |
|---|---|
| 指代不明 | "改 port"(文件里有两处) |
| 完成条件模糊 | "优化一下" |
| 缺失败预案 | 没说找不到时该怎么办 |
三类缺陷有一个共同修法:把"要做什么"改成"怎么判断做完了"。完成条件一旦可判定,指代与预案的问题会同时消失大半,因为 Agent 会先去找判据。
第一项在多文件、多位置的任务里最常见:人靠上下文能猜对,而 Agent 只能按字面执行。所以给 Agent 的描述需要的精度高于给人。
第三项被忽略得最多:没有失败预案,Agent 就会"尽力完成",而"尽力完成"往往表现为编造一个合理的结果。
// 可执行的描述:三要素齐备
task: "把 config/app.yaml 里 database.port 从 5432 改为 5433"
locate: "只改 database.port;若存在多个 database 段,先报告不要改"
done_when: "文件里 database.port 的值为 5433,且其它字段未变"
on_failure: "若定位不到唯一目标,停止并输出候选位置,不要猜测"
复盘的证据来源要固定:完整的一轮记录(用户输入、每次工具调用与返回、最终输出)。只凭最终输出复盘,等于看着结果猜过程,而 Agent 的失败大多是过程性的。
// 复盘需要的五段记录
1. 用户原始输入(不改写)
2. 每轮的上下文摘要(长度与关键内容)
3. 每次工具调用:参数 + 原始返回
4. 模型的中间结论(若可见)
5. 最终输出与它声称"已完成"的依据
// 缺 3 → 无法判断是"工具撒谎"还是"判断错"
4. 工具返回的可判断性
| 返回 | 能否支撑下一步 |
|---|---|
| 只有"成功" | 不能,缺细节 |
| 成功 + 新状态 | 能 |
| 失败 + 原因 + 可行动项 | 能 |
| 失败 + 泛化错误码 | 不能 |
还有一个容易忽视的返回缺陷:成功但结果为空。"没找到"与"请求成功但没有结果"在语义上是两件事,若工具只返回空列表而不说明原因,Agent 往往会再试一遍同样的调用。
// 空结果要带原因,否则会被当成"再试一次"
{"ok": true, "items": [], "empty_reason": "no_match",
"searched": {"path": "config/", "pattern": "*.yaml"},
"suggestion": "扩大到 config/** 或以 --recursive 重试"}
第四项会让 Agent 陷入"改参数重试"的死循环:它不知道错在哪,只能随机调整。所以工具的错误返回要带 field、expected、retryable 这类结构化信息。
// 失败返回:结构化到"能直接修"
{"ok": false, "error": {
"code": "INVALID_VALUE",
"field": "database.port",
"expected": "integer 1-65535",
"received": "5433 ",
"retryable": true,
"hint": "值末尾有空格"
}}
5. 复盘记录的写法
| 写法 | 可修性 |
|---|---|
| "模型理解错了" | 无 |
| "第三步改错字段" | 低 |
| "文件里两处 port,描述未限定" | 高 |
| 上一条 + "改为限定 database.port" + "重跑验证通过" | 最高 |
第三条开始才是有效复盘:它指向一个可以改的东西(描述),而不是一个无法改的东西(能力)。
复盘记录还应当带"这一层有几类失败"的统计:连续十次任务里第一层占几次、第三层占几次。单次复盘的结论只是个案,分布才决定该优先修描述还是修工具。
第四条加上"重跑验证",把结论从"推测"变成"已验证"。
复盘的产出应当是"改动清单"而不是"结论":改哪句话、加哪个字段、拆哪个工具、补哪条验证。清单上的每一项都要能在下一次任务里被验证是否有效。
// 可验证的改动清单(每项带验证方式)
改:描述里"改 port" → "改 database.port"
验:同一任务重跑 5 次,5 次都定位到同一行
改:read_file 返回加 total_lines 与 matched_ranges
验:构造 3 处匹配的文件,Agent 不再只改第一处
6. 排错:五种"改了很多次还是不行"
| 现象 | 成因 | 处理方向 |
|---|---|---|
| 每次错在不同地方 | 描述歧义 | 限定目标与完成条件 |
| 反复重试同一动作 | 错误返回无信息 | 工具返回结构化 |
| 编造一个结果 | 无失败预案 | 明确"停下并报告" |
| 明明信息在上下文里却不用 | 上下文过长被挤出 | 精简或摘要 |
| 简单任务也失败 | 工具粒度太粗 | 拆细工具 |
第五项与工具粒度是同一个问题:一个工具干三件事,模型就得先判断该用哪一件,而这个判断本身就是一次可能出错的动作。
第四项很实际:上下文塞得太满时,关键信息会被挤到边缘,模型倾向于使用最近与最显著的内容。所以"信息齐全"不等于"信息可用"。
7. 边界:什么情况不必深入复盘
| 场景 | 为什么 | 做法 |
|---|---|---|
| 一次性探索任务 | 不重复 | 记结论即可 |
| 任务本身不可判定 | 无客观标准 | 换评判方式 |
| 失败代价极低 | 不值得 | 重跑一次 |
| 已定位到明确原因 | 无需再拆 | 直接修 |
判断口径一句话:这个失败会不会再发生一次。会,就必须拆到可修的那一层;不会,记录一次就够。
8. 自测
- 四层归因中,团队最常归到哪一层?为什么那一层"最不需要改任何东西"?
- "补齐信息后重跑"这个实验能区分什么?为什么它的成本极低却少有人做?
- 任务描述的三类缺陷里,哪一类会导致 Agent"编造一个合理结果"?
- 工具返回"失败 + 泛化错误码"会让 Agent 陷入什么循环?正确的返回该包含哪些字段?
- 为什么"信息在上下文里"不等于"信息可用"?
- 判断"是否需要深入复盘"的那个问题是什么?