🛠️ 第10章 场景二:修复 bug、补测试、做回归
📄 一句话概括

范围要写进任务里,不是写在心里。 detector 跑在提交之前,比跑在 review 里便宜一个量级。 新建文件的越界最隐蔽:不加 git add,它根本不出现在 diff 里。

📝 本章和前面几章的关系

上一篇把 bug 说清楚了。这一篇加一道闸:说清楚只准改哪儿。 9.4 讲过改动的三个数字,那是「改完怎么交」。 这一篇讲的是「改之前怎么锁」,以及「锁完怎么量」。 两篇不重复:一个管输出,一个管输入。 Demo 造三个版本:越界重构八个文件、只在范围内改两个、范围外多建一个。 再用二十行检测器把越界逐个点名。

一、把范围写进任务

「顺手」这件事,Agent 和人犯的是同一个错,但原因不同。 人顺手是因为看见了不舒服; Agent 顺手是因为它读到的上下文里,那些东西和目标是连在一起的——它不区分「相关」和「该动」。 所以任务描述里要有一句显式的话:只允许改哪几个文件。 这句话的作用不是「限制能力」,是「给它一个判据」—— 它看到一个变量名不好,本来会改;有了这句话,它会记下来。

任务写法 典型结果
修一下空名字导致的 500 顺手改了八个文件
修 500,只改 handler.py 和它的测试 改了两个
同上,加一句「其它发现记到 issue」 两个,另有三条待办

第三行那个「记到 issue」很关键。 不给出路,它就只能二选一:要么改,要么忘。 给了出路,它会把发现攒下来,而这份清单本身就是下一轮任务的输入。

二、用 diff 数字兜底

光写进任务不够,还要有一道能自动跑的闸。 Demo 里那个检测器二十行:把允许的文件列成集合,拿 git diff --name-only 跟它比,多了就报。 二十行,跑在提交之前,比跑在 review 里便宜一个量级。 review 里发现越界,你得评论、等回复、来回一轮; 提交前发现,改一下就好。 Demo 里三个版本:A 改了八个文件、越界六个;B 两个、越界零;C 两个文件但越界一个。 C 版那个最值得看。 它只碰了两个文件,看起来很小,但其中一个是你没批准的新文件。 而如果检测器跑在 git add 之前,用裸 git diff,这个新文件根本不会出现在列表里—— 越界就这么漏过去了。 所以检测器有两个细节要写对:跑在 git add 之后,并且把「新增文件」和「修改文件」分开看。 新增文件的风险比修改更高——修改至少还在原来的调用链上,新增的没人知道它会被谁用。

三、发现该重构怎么办

发现 当次该做 什么时候做
这里应该抽个函数 记一笔,不动 换一个提交
这个变量名太糟 记一笔,不动 换一个提交
这行会崩 当场修 就是这次

判据只有一条:这件事跟「修好这个 bug」是不是同一件。 是,就做;不是,就记。 第三行是唯一的例外。 你路过一个会崩的地方,看见了不修,下次还得再来一轮,而且下次来的人可能不知道你也看见过。 但要在提交信息里明说「顺手修了另一个问题」,别让它伪装成这次修复的一部分。 「记一笔」要真记。 写进 issue 或者代码里的 TODO,别记在脑子里—— 脑子里那张单子从来不会变短,也从来没被谁看见过。 这条规矩对 Agent 同样适用。 任务里写清「其它发现列成清单交回来,不要动手改」,它就会给你一份清单;不写,它就会替你做决定。

四、动手 Demo:约束修改范围:越界的另一种查法

同一个 bug,三个版本各改一遍:越界重构、只在范围内改、范围外多建一个文件。 然后拿一份声明的范围当尺子,逐个点名越界。 三个版本都是真 git 仓库、真 git diff --name-only,文件列表是命令输出,不是我列的。

SCOPE = {"svc/handler.py", "tests/test_handler.py"}

# 必须在 git add 之后,否则新建文件不出现
changed = git(root, "diff", "--cached", "--name-only").split("\n")
extra = [f for f in changed if f and f not in SCOPE]

print("改了 %d 个,越界 %d 个" % (len([f for f in changed if f]), len(extra)))
for f in extra:
    print("  越界:", f)

实跑输出:

--------------------------------------------------------------------
约束修改范围:越界的另一种查法
--------------------------------------------------------------------

  上一篇把 bug 说清楚了。这一篇把范围也写清楚,然后拿它当尺子量 diff。
  任务里先声明范围:只允许改 svc/handler.py 和 tests/test_handler.py。

  ① 三个版本,各自改动
  --------------------------------------------------------------------
| 版本 | 改了 | 范围内 | 越界 | 越界的文件 |
|---|---:|---:|---:|---|
| A | 8 | 2 | 6 | docs/CONVENTIONS.md、svc/routes.py、svc/store.py… |
| B | 2 | 2 | 0 | — |
| C | 2 | 1 | 1 | svc/guard.py |

  A 越界 6 个,C 越界 1 个,B 越界 0 个。
  ! C 版那个越界很值得看:它是新建的文件。
  检测器必须跑在 git add 之后——直接 git diff 不含未跟踪文件,新文件根本不出现,越界就漏了。

  ② 检测器二十行
  --------------------------------------------------------------------

  [越界检测 · 本沙箱真跑]
  | SCOPE = {"svc/handler.py", "tests/test_handler.py"}
  | changed = git(root, "diff", "--cached", "--name-only")
  | extra = [f for f in changed if f not in SCOPE]
  | if extra:
  |     print("越界:", extra)
  跑在提交之前,比跑在 review 里便宜一个量级。


  ③ 发现该重构怎么办
  --------------------------------------------------------------------
| 发现 | 当次该做 | 什么时候做 |
|---|---|---|
| 这里应该抽个函数 | 记一笔,不动 | 换一个提交 |
| 这个变量名太糟 | 记一笔,不动 | 换一个提交 |
| 这行会崩 | 当场修 | 就是这次 |

  判据只有一条:这件事跟「修好这个 bug」是不是同一件。
  第三行是例外——一个会崩的 bug 你看见了不修,下次还得再来一轮。
  但要在提交信息里明说是顺手修的另一个问题。
  ! 「记一笔」要真记。
  写进 issue 或 TODO,不然它永远只是「记在脑子里」,而脑子里那张单子从来不会变短。

A 版改了八个文件,越界六个;B 版两个,零;C 版两个,越界一个。 三个版本的 bug 修复部分完全一样——差别只在「多做了什么」。 A 版没有一处改动是错的。 函数改名、加类型别名、补一份命名约定文档,都是好事。 但它把「这次改了什么」这件事变得无法用一句话说清—— 而「能用一句话说清」恰恰是提交信息的前提。 C 版是最像样的一次越界:只多了十行,还是个好抽象。 但它暴露了检测器的一个盲区—— 如果跑在 git add 之前,这个新文件根本不在 git diff 里,越界就查不出来。 所以检测器的顺序是:add、再 diff、再比范围。 这三步倒过来,闸门就漏了。

30 秒实验

  1. 给你手上的项目写一份 SCOPE 例子:随便挑一个任务,写下「只准改哪几个文件」。 写不出来,说明这个任务还没拆到能派出去的程度。
  2. 跑一次 git diff --name-only,数一下上一次提交改了几个文件。 超过三而任务只有一件的,就是可以拆的。
  3. 在你的 AGENTS.md 里加一句「其它发现记清单,不要动手」。 下一次任务看它有没有真的交回一份清单。

常见误区

  1. 「限得太死会影响 Agent 发挥。」 限制的是「改哪儿」,不是「怎么改」。 范围内的解法它随便发挥,范围外的它记下来就好。
  2. 「越界改了也没坏处,反正测试会挂。」 测试只覆盖测过的地方。 顺手改的那八处,可能一处测试都没有。
  3. 「diff 数字看一眼就行,不用脚本。」 人看一眼会漏,尤其是新建文件。 二十行脚本跑一千次都不累,你看一千次会累。
自测三问(点开对答案) 1. 为什么要把范围写进任务里? —— **因为「相关」和「该动」是两件事。** **Agent 分不清,你不给它判据,它就会替你做决定。** 2. 检测器为什么必须跑在 `git add` 之后? —— **因为裸 `git diff` 不含未跟踪文件。** **新建文件的越界最隐蔽,也最危险。** 3. 哪种「顺手」是允许的? —— **只有「这行会崩」这一种。** **而且要写明是顺手修的另一件事,别伪装成这次修复的一部分。**

下一篇讲测试本身:覆盖率百分之百的测试,可能一条 bug 都抓不住。

相关笔记