🛠️ 第14章 Codex 的边界、风险与工程方法论
📄 一句话概括

出错之后第一件事是归因:环境、代码、需求,还是模型。 只看报错首行,十二条能归对七条;环境和代码基本能归,需求和模型一条都归不动。 补规则救不回这一类:加一条救回一条,同时打错另一条,总数一动不动。

📝 本章和前面几章的关系

这一章是全书最后一章,讲边界。 边界不是「能力不够」,是「知道它在哪一步会失手,然后把这一步变成可检查的」。 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 秒实验

  1. 找出你最近三次报错,各写一句「真原因」和「报错首行」。 两者对不上号的,就是归因会出错的地方。
  2. 把归因表抄进 AGENTS.md,加一句「出错先自报类别」。 下次它自报错了,你就知道该补哪一段上下文。
  3. 回看你误判过的一次,问一句「如果这是我自己写的代码,我会先看哪一行」。 这一问通常能直接跳到真原因。

常见误区

  1. 「报错说缺模块,先装上再说。」 装之前先确认是「没装」还是「装在了另一个解释器里」。 后者装十次也没用。
  2. 「同一类错误多了,说明模型不行。」 同类错误反复出现,通常说明输入里少了同一样东西。 换模型能治一次,治不了这一类。
  3. 「归因靠经验,不用写下来。」 经验只在你自己脑子里,Agent 拿不到。 写进 AGENTS.md 之后,它出错时能自报,你才看得见它的判断。
自测三问(点开对答案) 1. 为什么需求类错误在日志里没有痕迹? —— **因为日志记录的是「程序做了什么」,而需求缺失是「没规定要做什么」。** **没做的事不会留下报错,只会在断言处露一面。** 2. 四个误判方向里,为什么「代码错归成模型错」最贵? —— **因为它触发的动作是「重写」,而重写会扩大改动面。** **改动面一大,原来对的部分也进了 diff,判断成本翻倍。** 3. 补规则为什么救不回命中率? —— **因为规则是在同一份输入上做划分,而那一类信息压根不在输入里。** **划得再细,也只是把「看不清」改个名字。**

归因解决「错了怎么看」。下一节解决「什么东西不该让它看见」。出域是一条单向的坡。

相关笔记