你让它加一个“导出 CSV”的功能。回来时 git status 列了十七个文件:新模块、改过的路由、两个配置文件、几个快照测试,还有一个你根本没提过的工具函数。

此刻你缺的不是读代码的耐心,是两样东西:一个能退回去的落点,和一个能当场判断“改坏了没有”的信号。这篇把这两样凑齐:用提交作为退回的落点,用一条检查命令作为判定的信号,用隔离分支兜住整条路线作废的情况。三样拼在一起才叫安全网,缺一样就只是心理安慰。

为什么“一次改十七个文件”是工程风险

两个退化,都比“看着累”严重。

一是审查退化成模式匹配。上一篇审 AI 生成的代码的基本动作是先看这次改了什么、再核对它对项目的假设,而这取决于 diff 的大小:改动一旦超出你能同时握在手里的范围,审查就变成“扫一眼觉得说得通”。

二更关键:回滚的最小单位被放大了。 git 能退回去的粒度就是提交。十七个文件压成一个提交,出问题时 git revert 会把这次的全部改动一起撤掉,包括本来做对的部分。想定位是哪一处改坏了?git bisect 能二分出“问题就在这次提交”,但你打开它,面对的仍是功能、测试、重构、格式化、依赖升级混在一起的一堆改动,根因还是没定位出来 35。

所以提交粒度直接决定了出问题时的修复成本。

提交:把“能退回的工作状态”切成几份

判断粒度只有一句话:这次提交里如果有一处是错的,你愿意把剩下的部分一起丢掉吗? 愿意,粒度就对了;不愿意,说明它塞了两件以上的事。

按这个标准,“加导出功能”“顺手重排 import”“升级一个依赖”“格式化整个文件”必须分开;接口定义和它的实现可以放一起,因为那是一件概念。这种提交有个通行名字,叫 原子提交(atomic commit):一个提交只表达一个能独立解释、独立回滚的意图,并且在这个点上代码能跑、能过检查 24。

比粒度更容易漏的是顺序:先绿,再提交。

理由是一条推理链。安全网的全部价值在于“我退回去的那个状态一定是个好状态”。提交里混着没验证的中间态,回退就只是从一种坏法换到另一种坏法。经历过一两次,人就不敢退了——改成在原地继续打补丁,错误一层层叠在同一个未提交的堆上。所以有条实践规则很直接:未提交的改动不得堆叠,检查没过就先修好,别在坏状态上加新改动 4。坏状态上叠三层,最后你要面对三个互相影响的错误同时爆发。

一条检查命令:把“改坏没有”变成一个退出码

把“这个状态是好状态”变成机器能回答的问题:把项目里所有自动检查收进一条命令,只记这一个入口。

1check:
2	ruff check .
3	mypy src
4	pytest -q

(make 只是给几条命令起名字的老办法,make check 就是把这三行依次跑一遍。用什么工具不重要,重点是它是 一条 命令。)

第一,用退出码说话:跑完,0 就是通过,非 0 就是失败。这不只是给你省事,更是给 AI 一个能读的信号。官方实践文档把这条列在最前:有东西可跑,它就能做、跑检查、读结果、自己迭代到通过;没有,“看起来做完了”就是它唯一能拿到的信号,于是你变成验证回路本身,每个错误都等你发现 1。命令散在你脑子里,它就会漏跑一个,而漏跑的那个往往正好是能发现问题的。

第二,先把基线跑绿。动手前在没改动的原状态上跑一次。这一步不能省,因为失败有两种:这次引入的,和本来就有的。基线是红的,你就无法区分,AI 也会把已有的红当背景继续往上加代码。基线红时,先记下哪几条在失败,再开工。

第三,便宜的放前面:静态检查和类型检查几秒出结果,单元测试次之,集成测试最慢,失败得越早越省事。

但记住它管到哪儿:它只回答“有没有违反我已写下的检查”。 它不回答需求是否实现,也不回答这些测试本身会不会变红——后者是上一篇用变异检查撞坏代码解决的问题。一条烂测试组成的检查命令,跑一万次也全绿。

分支:给“整条路线不要了”留退路

提交解决“退一步”,分支解决另一步:这次实验整体方向就错了。

在 main 上小步提交,做到第七步发现方案走不通,你要么留下一串提交(再产生一串 revert,顺序有依赖,可能冲突),要么重写历史。一开始就在 ai/csv-export 分支上做,收场只有两步:

1git switch main
2git branch -D ai/csv-export

代价是零,历史里也不留痕迹。提交是路上的存档点,分支是这条路本身,互不替代。

顺带一条硬规矩:AI 不该有直接推 main 的权限。分支保护、必须过状态检查才能合并,防的不是人,是“一次没看就混进去了” 5。

接起来:一块任务一个循环

绘制中

发任务时就说清只做一块。“这次只加导出函数和它的测试,不动路由、不动配置、不格式化无关文件。改完先跑 make check,把输出贴回来,先别提交。”需求那篇讲的边界,在这里变成可检验的边界:git diff --stat 列出的文件,就是它实际动过的地方。

提交权留在你手上。 让它在循环里反复跑检查、自己修,这是效率来源;但“这算做完、可以存档了”是个判断,得你确认。提交信息写清为什么这么改,而不是“AI 修改”。

红了就退,不要叠。 检查没过时最容易犯的错是接着喊“还是红,你再改改”,在一个未知状态上叠第二层改动。此时回到上一个已知良好的点(git restore . 丢掉未提交的改动),再重新提要求。这和报错那篇的停手判据是同一条:现场变复杂了就停。

已经压成大提交也能拆。 用 git reset --soft HEAD~1 把提交拆开(改动都还在暂存区),再用 git add -p 一块块挑着提交;也可以让它整理提交历史,但你要核对最终内容与整理前一致 2。只在没有 push 出去的提交上这么做。

三个让安全网变成心理安慰的地方

把全绿当验收通过。 检查只核对断言里写了的东西,绿色不说明功能对。另外本地全绿和 CI 全绿不是一回事,同一把尺要在两处都量。

把提交切得太碎。 每个小步都提交,历史成了流水账,bisect 找到的提交本身没有语义,等于没定位。判据还是那句:能不能独立解释、独立回滚。

把工具自带的回退当成 git。 有些助手提供 checkpoint、/rewind,能撤销它自己改过的文件,但官方文档写得很明确:它只覆盖通过文件编辑工具做的改动,用 Bash 命令或外部进程产生的影响不在其中,不能替代 git 1。

三样东西回答的是同一个问题的两面:每一步都要有一个已知良好的落点,和一个能自动判定的信号。 前者靠提交和分支,后者靠那条命令。少了任一半,你就只能盯着十七个文件的 diff,祈祷没漏看。