三个数字:改了几个文件、增删多少行、切成几段(hunk)。 最小变更不是「改得少」,是「目标改动之外一行都没有」。 同一个 bug,三种改法:最少九行,顺手重构四十三行,其中三十四行与 bug 无关。
上一篇讲改之前。这一篇讲改完之后,别人怎么看。
为什么「审查友好」值得单独成篇?
因为 AI 改代码的成本降下来之后,瓶颈从「写」移到了「看」。
一个人一天能看多少 diff 是有上限的,而这个上限决定了团队能并行多少事。
Demo 同一个 bug 修三遍:顺手重构、只修这个、修加抽函数。
三版都用真 git diff 取数字,再按「每行二十秒」估审查成本。
一、改动的三个数字
三个数字都能从 git diff --numstat 和 -U0 直接拿到。
| 数字 | 从哪拿 | 说明什么 |
|---|---|---|
| 文件数 | numstat 的行数 | 审查要切换几次上下文 |
| 增删行 | numstat 两列之和 | 要读多少行 |
| hunk 数 | -U0 里 @@ 的个数 | 改动散不散 |
第三个数字最容易被忽略,但它最能说明问题。 同样改二十行,集中在两段和散在十段,审查成本差好几倍—— 因为每开一段,审查者都要重新建立「这段在干什么」的上下文。 Demo 里 A 版的文件数是八、hunk 数是十三,行数四十三。 三个数一起看,你就知道这份 diff 大概要看多久。 单看行数会漏掉「散」这个维度。 这三个数是给人用的,不是给机器用的。 它们回答的问题是「这份改动值不值得现在看」—— 如果文件数远大于改动意图,答案通常是「先拆开再说」。
二、一次只做一件事
「最小变更」容易被理解成「改得越少越好」,这是错的。 改得少但没解决问题,等于零。 它真正的意思是:目标改动之外,一行都不多。 Demo 的 A 版是最好的反例。 它没有一行是错的——函数改名、加类型注解、把配置抽成类,每一件都是好事。 但它把这个 diff 从「这个 bug 修好了吗」变成了「这四十三行里哪几行是在修 bug」。 审查者要重新做一遍定位,而那本来是你该做的事。 所以「顺手」这两个字是成本转移。 你顺手省了五分钟,审查者多花二十分钟——而且他很可能漏掉真正的 bug,因为它在噪音里。 正确做法是把它拆成两个提交: 先一个只修 bug 的(九行),再一个只做重构的(三十四行)。 第一个能被快速确认,第二个可以慢慢看。 两个提交加起来还是四十三行,但审查成本差一个量级。
三、提交信息怎么写
提交信息是 diff 的目录。 好的提交信息让审查者能只读它、不看 diff,就判断「要不要细看」。
| 写法 | 审查者拿到什么 |
|---|---|
| fix bug | 什么都没有 |
| fix: 空名字返回 400 | 知道改了什么 |
| 标题加两行正文 | 知道改了什么、为什么、怎么验 |
第二行正文讲「为什么」,第三行讲「怎么验」。
这两句是给未来的:半年后有人 git blame 到这一行,他要的不是「改了什么」(diff 里有),
是「当时为什么这么改」。
AI 写的提交信息常见毛病是「复述 diff」。
「修改 handle 函数,增加空字符串判断」——这句等于没说。
有用的版本会说清「check 只校类型不校空串,空串要进 save 才炸」。
这是 diff 里看不出来的因果。
所以给 Agent 的任务里要写清「提交信息里必须有为什么」。
不然它默认只复述改了什么——因为它看得见 diff,看不见你的推理过程。
四、动手 Demo:最小变更的三个数字
同一个 bug——空名字导致 KeyError——用三种改法各修一遍,每版都真跑 git diff 取文件数、
增删行、hunk 数。
再按「每行二十秒」的粗模型估审查成本。
这个模型是粗的,但三版之间的相对关系不粗。
def stat(s):
nf = add = dele = 0
for ln in s.split("\n"):
p = ln.split("\t")
if len(p) == 3 and p[0].isdigit():
nf += 1
add += int(p[0])
dele += int(p[1])
return nf, add, dele
def hunks(s):
return len([x for x in s.split("\n") if x.startswith("@@")])
实跑输出:
--------------------------------------------------------------------
最小变更的三个数字
--------------------------------------------------------------------
同一个 bug,三种改法,各自跑一遍 git diff 取真数字。
bug 是:handle({"name": " "}) 抛 KeyError。check 只校类型不校空串,空名字被送进 save 才炸。
① 先复现
--------------------------------------------------------------------
退出码 1,最后一行:KeyError: 'name'
! 这就是 500 的来源:一个只由空格组成的名字,在 store 那层才被发现。
| 改法 | 文件 | 增 | 删 | hunk | 非目标占比 |
|---|---:|---:|---:|---:|---:|
| A 顺手重构 | 8 | 33 | 10 | 13 | 79% |
| B 只修这个 bug | 2 | 7 | 2 | 2 | 0% |
| C 修 bug 加抽函数 | 4 | 14 | 4 | 5 | 50% |
三个数字就是这三列:改了几个文件、增删多少行、切成几段(hunk)。
! B 那组是基准——为了修这个 bug,最少要动 9 行。A 动了 43 行,其中 34 行跟这个 bug 无关。
A 版没有一行是错的,但它把「这个 bug 修好了吗」变成了「这些改动里哪几行是在修 bug」。
审查者要重新做一遍定位。
② 审查者要花多少时间
--------------------------------------------------------------------
| 改法 | 要读行数 | 粗估分钟 | 主要成本 |
|---|---:|---:|---|
| A 顺手重构 | 43 | 14 | 找哪几行在修 bug |
| B 只修这个 bug | 9 | 3 | 确认两处改动 |
| C 修 bug 加抽函数 | 18 | 6 | 多读一个文件 |
按每行二十秒的粗模型算。真实情况更糟:改动散在 8 个文件里,来回切换的成本比读数本身还高。
③ 提交信息怎么写
--------------------------------------------------------------------
| 写法 | 审查者能拿到什么 |
|---|---|
| fix bug | 什么都没有 |
| fix: 空名字返回 400 | 知道改了什么 |
| fix: 加两行正文 | 知道改了什么、为什么、怎么验 |
[提交信息示例]
| fix: 空名字返回 400 而不是 500
|
| check() 只校类型,空串要进 save 才炸。
| 改为先 strip,空则返回 400。
| 顺带补一条测试 test_blank_name。
第三行那种写法,让后来人能只读提交信息就决定「要不要看 diff」。
三组数摆在一起:最少九行,顺手重构四十三行,半顺手十八行。 非目标占比从零升到七成九——这就是「顺手」的代价被量化之后的样子。 重点在「B 是基准」这件事上。 为了修这个 bug,最少要动九行,这是任务本身的下限。 超出这个下限的部分,全是审查者要多承担的。 所以「最小变更」有一个可算的定义:贴着九行往下砍。 A 版那张表里,文件数是八、hunk 数是十三。 这两个数比行数更能说明「散」——十三段改动散在八个文件里,审查者要来回切十三次上下文。 最后那张提交信息表。 第三行那种写法(标题加两行正文)不是格式好看,是让审查者能「只读正文就决定要不要看 diff」。 这件事省下来的时间,可能比把 diff 写干净还多。
30 秒实验
- 翻一个你最近的提交,数三个数:文件、行、hunk。 文件数超过三而改动意图只有一件事的,就是可以拆的。
- 找一个「顺手改」的提交,试着把它拆成两个: 一个只做目标改动,一个只做顺手的事。看第二个还能不能独立成立。
- 把「每行二十秒」换成你自己的实际速度,算一下你上周审查花了多少分钟。 这个数就是「顺手改」向你收的费。
常见误区
- 「最小变更就是改得越少越好。」 改得少但没解决问题等于零。 它的意思是「目标改动之外一行都不多」。
- 「顺手重构是提高代码质量,应该鼓励。」 重构没错,混在 bug 修复里错。 拆成两个提交,质量照样提高,审查成本降一个量级。
- 「AI 生成的提交信息都差不多,改不改无所谓。」 差很多。 默认它只会复述 diff,而「为什么」是 diff 里没有的那部分。
自测三问(点开对答案)
1. 三个数字分别说明什么? —— **文件数说要切换几次上下文,增删行说要读多少,hunk 数说改动散不散。** 2. 为什么说「顺手」是成本转移? —— **你省五分钟,审查者多花二十分钟,还可能漏掉真正的改动。** **更糟的是它把定位工作推给了别人。** 3. 「最小变更」的可算定义是什么? —— **贴着任务本身的下限砍。** **Demo 里那个下限是九行,超出部分全是额外成本。**最后一篇讲最难的场景:遗留项目。它多一条铁律——先补验证,再补文档。
- ☐ (本篇完,下一篇 9.5 遗留项目的 Harness 工程)