用 Git 二分定位 Agent 引入的回归
示例用一个三行测试脚本驱动 git bisect run,在 16 次提交中经过 4 轮测试定位首个错误提交。说明了成功、失败及跳过提交的退出码约定,并建议将测试脚本放在提交历史之外。
AI 深度解读
Agent 连续提交了 16 次代码,原本应返回 5 的 add 2 3 却变成了 -1。作者没有逐个翻 diff,而是给 git bisect 一个正常版本、一个异常版本和三行测试脚本,运行 4 次便找到了植入缺陷的「commit 11」。
这篇短文的价值,在于把回归排查变成了可重复执行的流程:先写清楚什么叫错,再让 Git 缩小范围。对于经常让 AI 连续修改代码的程序员,这个顺序值得借鉴。
16 次提交,为什么只测了 4 次
git bisect 用二分查找定位首次引入问题的提交。你提供一个已知正常的提交和一个已知异常的提交,它就在两者之间选择版本测试,再根据结果缩小搜索区间。
作者构造的案例里,Agent 一共提交了 16 次,缺陷被放在「commit 11」中。自动测试执行了 4 次,最终找到的正是这个提交。
这不是模型理解代码的能力,而是 Git 利用提交历史做搜索。
提交数量增加时,二分查找所需的测试次数增长较慢。不过,4 次是这个案例的结果,不是所有 16 次提交历史都能照搬的保证。 历史结构、可测试性,以及问题是否曾被修复后再次引入,都会影响排查。
真正要先写好的,是那个退出码
自动定位的前提,是把异常变成机器能判断的条件。这里的条件非常小:执行 add 2 3,检查结果是否等于 5。
下面这段命令创建测试脚本、启动定位,并在结束后返回起始位置:
printf '#!/usr/bin/env bash\nsource ./calc.sh\n[ "$(add 2 3)" = 5 ]\n' > ../test.sh && chmod +x ../test.sh
git bisect start HEAD <known-good-commit>
git bisect run ../test.sh
git bisect reset # go back to where you started
最关键的是脚本中的 [ "$(add 2 3)" = 5 ]:条件成立时返回 0,不成立时返回非零值,Git 据此判断当前提交。
运行前,要把 <known-good-commit> 替换成已确认正常的提交。命令里的 HEAD 则作为已知异常的一端。
原文列出的脚本退出码规则是:
- 返回
0,表示当前版本正常。 - 返回
1到127,但不包括125,表示当前版本异常。 - 返回
125,表示当前提交无法测试,应跳过。 - 其他退出码会中止二分过程。
「无法测试」和「测出了缺陷」必须分清。某个中间提交无法构建,并不能证明它引入了正在排查的问题。作者建议遇到这种情况返回 125;上面的三行脚本没有实现构建检测,接入真实项目时需要自己补上。
测试脚本的位置也有讲究。作者把它放在仓库外的 ../test.sh,避免切换旧提交时,测试脚本也跟着变动。原文也允许使用未被 Git 跟踪的脚本。
我的判断
我赞同作者提出的排查顺序:先写一个能复现问题的检查,再运行 git bisect run。相比直接让 Agent 阅读大量 diff,这种方法能先把搜索范围收敛到具体提交,也留下了可重复验证的依据。
但原文只展示了一个人为植入缺陷的 Shell 算例,没有提供真实项目或与人工排查的对照数据。「比先读 diff 更合适」可以作为实践建议,不能据此认定它在所有任务里都更快。
一般团队还要满足几个前提:正常端点确实正常,异常端点稳定复现,测试在历史版本上仍然有效。如果失败来自偶发网络问题、环境变化,或者缺陷在区间内反复出现和消失,定位结果就需要额外核查。
它能帮你找到首次被测试判为异常的提交,原因仍要靠代码分析确认。 找到「commit 11」之后,阅读这个提交的 diff 才是下一步。
能拿来做什么
- Agent 一轮修改后出现回归,先把具体异常写成一个独立检查,确认它在正常版本通过、在当前版本失败,再启动二分定位。
- 排查 Node 或前端逻辑回归时,优先选能稳定复现问题的小测试,让它通过退出码报告结果,避免只输出日志却始终返回成功。
- 把定位脚本放在仓库外或保持未跟踪;对确实无法构建、无法执行检查的中间提交返回
125,结束后执行git bisect reset。