出错之后第一件事是归因:环境、代码、需求,还是模型。 只看报错首行,十二条能归对七条;环境和代码基本能归,需求和模型一条都归不动。 补规则救不回这一类:加一条救回一条,同时打错另一条,总数一动不动。
这一章是全书最后一章,讲边界。 边界不是「能力不够」,是「知道它在哪一步会失手,然后把这一步变成可检查的」。 Demo 把十二条典型报错喂给一个只看关键词的分类器,看它能归对几条、错在哪、补规则能不能救。
一、三类错误的归因表
先分类,再动手。跳过分类直接动手,最常见的结局是「改了半天,错还在」。
| 类别 | 典型表现 | 该谁动手 |
|---|---|---|
| 环境 | 模块没装、权限不够、命令不在 PATH | 改环境 |
| 代码 | 断言失败、键不存在、类型不匹配 | 改代码 |
| 需求 | 边界没定义、格式没约定、谁负责没说 | 回去改需求 |
| 模型 | 编了不存在的参数、把 A 库写法套到 B 库 | 补上下文 |
为什么是四类而不是两类? 因为四类的「下一步动作」完全不同:改环境是装东西,改代码是改逻辑,改需求是回去谈, 补上下文是换输入。 动作不同,所以必须先分开。 四类里最容易混的是后两类。 需求类错误看起来像代码错——它的报错往往是一条断言; 模型类错误看起来像环境错——它用一个不存在的参数,报错是「没有这个选项」。 这两组混淆都有一个特征:报错文本里没有任何一个字提到真原因。 记住这一点,比记住具体哪一条更像哪一类有用。
二、别把工程问题归成模型问题
Demo 的做法是写一个只吃报错首行的分类器,把它当成「不查上下文的新人」。 十二条里归对七条,五成八。
| 真原因 | 条数 | 归对 | 命中率 |
|---|---|---|---|
| 环境 | 3 | 3 | 100% |
| 代码 | 5 | 4 | 80% |
| 需求 | 2 | 0 | 0% |
| 模型 | 2 | 0 | 0% |
这张表比总数更有信息量。 前两类能归,是因为报错文本和原因一对一:看到「模块找不到」就是没装, 看到「键不存在」就是代码访问了没有的字段。 后两类归不动,原因却不一样。 需求类错误在日志里根本没有痕迹。 Demo 第 8 条是这个现象的标本:需求说「金额保留两位」,模型返回了字符串,报错是断言失败—— 需求问题穿了一件代码错误的衣服。 模型类错误更干脆:连关键词都没有。 「用了不存在的参数」跟「模块没装」在文本上毫无共同点,而分类器只认文本。 补规则也补不上。 Demo 试了四个版本:原版、加「找不到文件算环境」、加「断言失败算需求」、两条都加,四行全是七条。 每加一条会救回一条,同时打错一条,总数一动不动。 这不是分类器写得不好,是输入里就没有那一类信息。 要归需求类,得回头看需求原文,而不是看日志。 误判的代价不对称,这是最该记住的一点。 把代码错归成模型错,会去让它重写那段,而重写会把 diff 越滚越大,最后连原来哪里对都看不清了。 四个误判方向里,这一条最贵。
三、别把模型问题归成人的问题
反过来的错更伤团队。 同一个问题反复出现时,如果归成「人不够仔细」,动作就是换人或者提醒; 而归成「输入不够」或「验收缺失」,动作才是改流程。 Demo 里有一个很干净的对照:同一句「导入 util 并调用 load」,在两个目录里报错完全不同。 目录里没有 util.py,报「没有这个模块」;目录里有个空的 util.py,报「模块在但没有 load 这个属性」。 两条的成因是同一件事:函数还没写。 但报错文本通篇没有一个字提到「忘了写」。 按报错去搜,第一条会让你去装包,第二条会让你去查版本,都不对。 一条能挡掉大半误判的规则:改之前先问一句「如果这是我自己写的代码,我会先看哪一行」。 这一问把「谁的责任」换成了「哪一行」,而后者有答案。 最后是一张落地的建议:把归因表放进 AGENTS.md。 让 Agent 出错时先自报「我认为这是哪一类」,再动手。 它自报错了,你一眼就能看出来,比你事后翻 diff 快得多。
四、动手 Demo:幻觉、越界与错误归因:只看日志能归对几条
拿十二条典型报错,喂给一个只认关键词的分类器。 再试四个版本的规则,看能不能把命中率抬上去。 最后真跑两个目录,看同一件事能报出多不一样的错。
CASES = [
(1, "ModuleNotFoundError: No module named 'yaml'", "env"),
(2, "FileNotFoundError: [Errno 2] 'config.yml'", "code"),
(3, "AssertionError: 3 != 4", "code"),
(7, "ValueError: empty list not handled", "spec"),
(9, "error: unrecognized argument -K", "model"),
# …共十二条
]
def classify(line, extra=()):
s = line.lower()
if "assertionerror" in s and "spec" in extra:
return "spec"
if any(k in s for k in ("modulenotfound", "permissionerror",
"command not found")):
return "env"
if any(k in s for k in ("assertionerror", "keyerror",
"typeerror", "syntaxerror")):
return "code"
return "?"
实跑输出:
--------------------------------------------------------------------
幻觉、越界与错误归因:只看日志能归对几条
--------------------------------------------------------------------
出错之后第一件事是归因:环境没装、代码写错、需求没说清,还是模型自己编的。
归错了,下一步动作就全歪。
① 四类错误,各自长什么样
--------------------------------------------------------------------
| 类别 | 典型表现 | 该谁动手 |
|---|---|---|
| 环境 | 模块没装、权限不够、命令不在 PATH | 改环境 |
| 代码 | 断言失败、键不存在、类型不匹配 | 改代码 |
| 需求 | 边界没定义、格式没约定、谁负责没说 | 回去改需求 |
| 模型 | 编了不存在的参数、把 A 库写法套到 B 库 | 补上下文 |
这张表的用处是「先分类,再动手」。跳过分类直接动手,最常见的结局是「改了半天,错还在」。
② 十二个真实形态的报错,让分类器只看首行
--------------------------------------------------------------------
做法是写一个只吃报错文本的分类器,把它当成「不查上下文的新人」,看它能归对几条。
| # | 报错首行 | 真原因 | 分类器 | 判定 |
|---|---|---|---|---|
| 1 | ModuleNotFoundError: No module nam | 环境 | 环境 | 对 |
| 2 | FileNotFoundError: [Errno 2] 'conf | 代码 | 看不清 | 错 |
| 3 | AssertionError: 3 != 4 | 代码 | 代码 | 对 |
| 4 | KeyError: 'price' | 代码 | 代码 | 对 |
| 5 | TypeError: unsupported operand '+' | 代码 | 代码 | 对 |
| 6 | PermissionError: [Errno 13] 'C:/Wi | 环境 | 环境 | 对 |
| 7 | ValueError: empty list not handled | 需求 | 无从判断 | 错 |
| 8 | AssertionError: '100.50' != 100.5 | 需求 | 代码 | 错 |
| 9 | error: unrecognized argument -K | 模型 | 无从判断 | 错 |
| 10 | error: no such option --strict-mod | 模型 | 无从判断 | 错 |
| 11 | SyntaxError: invalid syntax (3:14) | 代码 | 代码 | 对 |
| 12 | command not found: codex | 环境 | 环境 | 对 |
十二条归对 7 条,58.3%。
! 这十二条报错是按实践里的典型形态写的,不是某一次真实日志;
真原因是事后复盘标的,报错文本本身都是引擎会真打出来的形状。
③ 按类别拆开看,哪一类归不动
--------------------------------------------------------------------
| 真原因 | 条数 | 归对 | 命中率 |
|---|---|---:|---:|
| 环境 | 3 | 3 | 100% |
| 代码 | 5 | 4 | 80% |
| 需求 | 2 | 0 | 0% |
| 模型 | 2 | 0 | 0% |
环境和代码两类基本能归:报错文本里有关键词,关键词和原因是一对一的。
需求和模型两类一条都没归对。
原因不同:需求类错误在日志里根本没有痕迹(第 7 条报的是 ValueError,看着像代码错);
模型类错误更干脆,连关键词都没有。
! 第 8 条最能说明问题:需求说「金额保留两位」,模型返回了字符串,
报错是 AssertionError——需求问题穿了一件代码错误的衣服。
④ 补规则也补不上:天花板就是七条
--------------------------------------------------------------------
| 分类器版本 | 归对条数 |
|---|---:|
| 原始三组规则 | 7 |
| 加「找不到文件=环境」 | 7 |
| 加「断言失败=需求」 | 7 |
| 两条都加 | 7 |
四行都是 7。补一条规则会救回一条,同时打错另一条,总数一动不动。
这不是分类器写得不好,是「报错文本」这个输入里就没有需求类信息。
要归需求类,得回头看需求原文,而不是看日志。
⑤ 同一件事,报错可以长得完全不同
--------------------------------------------------------------------
两个目录都跑「导入 util 并调用 load」,区别只在第二个目录里有个空的 util.py。
目录里没有 util.py:
[err] ModuleNotFoundError: No module named 'util'
目录里有个空的 util.py:
[err] AttributeError: module 'util' has no attribute 'load'
第一条说「没有这个模块」,第二条说「模块在,但没有 load 这个属性」。
两条的成因是同一件事:函数还没写。但报错文本通篇没有一个字提到「忘了写」。
如果按报错去搜,第一条会让你去装包,第二条会让你去查版本——都不对。
⑥ 误判一次,白花几步
--------------------------------------------------------------------
| 误判方向 | 会去做的动作 | 白花 | 实际结局 |
|---|---|---|---|
| 环境→模型 | 换模型、改 prompt | 3 步 | 问题还在 |
| 模型→环境 | 装依赖、查环境变量 | 4 步 | 问题还在 |
| 需求→模型 | 加 prompt 约束 | 2 步 | 下一轮又错 |
| 代码→模型 | 让模型重写那段 | 5 步 | 越改越乱 |
四个方向里最贵的是「代码→模型」:一旦认定是模型的问题,就会让它重写,而重写会把 diff 越滚越大,
最后连原来哪里对都看不清了。
! 这张表里的步数是我按经验估的,不是测出来的。能看的是排序:把代码错归成模型错,代价最高。
一条可操作的规则:改之前先问一句「如果这是我自己写的代码,我会先看哪一行」。
这一问能挡掉大半误判。
⑦ 归因表贴在哪
--------------------------------------------------------------------
这张表别只放在脑子里。放到 AGENTS.md 里,让 Agent 出错时先按表自报「我认为这是哪一类」,再动手。
它自报错了,你一眼就能看出来,比你事后翻 diff 快得多。
十二行看下来,最该注意的是第 2 条和第 8 条。 第 2 条报的是「文件找不到」,真原因是代码里路径写错了—— 分类器把它归成了「看不清」,因为「找不到文件」既可能是没放,也可能是写错了。 第 8 条报的是断言失败,真原因是需求没说清格式。 这一条最能说明「报错文本里没有真原因」:整个错误链里只有「'100.50' != 100.5」, 没有任何线索指向需求。 按类别看,环境和代码两类几乎全能归,需求和模型两类一条都归不动。 这不是分类器弱,是输入缺信息。 补规则那一段值得单独看:四个版本全是七条。 加「找不到文件算环境」把第 2 条从「看不清」改成「环境」,但它真原因是代码,所以仍然错; 加「断言失败算需求」把第 8 条救回来,同时把第 3 条打错。 一进一出,总数不动。 最后那个双目录对照是全篇最实用的一段。 同一个「导入并调用」,一个报没有模块、一个报没有属性,而成因都是「函数还没写」。 知道这件事之后,你就不会一看到「模块不存在」就去装包了。 这一节的结论可以压成一句:日志能告诉你「错了」,很少能告诉你「为什么错」。 为什么错要去查需求、查改动、查环境,那是三个不同的地方。
30 秒实验
- 找出你最近三次报错,各写一句「真原因」和「报错首行」。 两者对不上号的,就是归因会出错的地方。
- 把归因表抄进 AGENTS.md,加一句「出错先自报类别」。 下次它自报错了,你就知道该补哪一段上下文。
- 回看你误判过的一次,问一句「如果这是我自己写的代码,我会先看哪一行」。 这一问通常能直接跳到真原因。
常见误区
- 「报错说缺模块,先装上再说。」 装之前先确认是「没装」还是「装在了另一个解释器里」。 后者装十次也没用。
- 「同一类错误多了,说明模型不行。」 同类错误反复出现,通常说明输入里少了同一样东西。 换模型能治一次,治不了这一类。
- 「归因靠经验,不用写下来。」 经验只在你自己脑子里,Agent 拿不到。 写进 AGENTS.md 之后,它出错时能自报,你才看得见它的判断。
自测三问(点开对答案)
1. 为什么需求类错误在日志里没有痕迹? —— **因为日志记录的是「程序做了什么」,而需求缺失是「没规定要做什么」。** **没做的事不会留下报错,只会在断言处露一面。** 2. 四个误判方向里,为什么「代码错归成模型错」最贵? —— **因为它触发的动作是「重写」,而重写会扩大改动面。** **改动面一大,原来对的部分也进了 diff,判断成本翻倍。** 3. 补规则为什么救不回命中率? —— **因为规则是在同一份输入上做划分,而那一类信息压根不在输入里。** **划得再细,也只是把「看不清」改个名字。**归因解决「错了怎么看」。下一节解决「什么东西不该让它看见」。出域是一条单向的坡。
- ☐ (本篇完,下一篇 14.2 授权、隐私与安全边界)