AI 代码理解度检查 vs 自动化测试:各自何时更有效
测试和 linter 漏掉了 AI 生成代码的最大风险:你并不理解它。对比理解度检查门禁与自动化测试,看看各自何时更有效。

这种场景正变得越来越常见:你的编码 agent 写了四个文件、共 600 行代码,测试全部通过,linter 也没报错,于是你合并了。两周后线上出了问题,你盯着 diff 看,就像在看陌生人写的代码一样,因为从功能上说,确实是别人写的。问题不在代码本身,而在代码和你脑中的心智模型之间的断层。AI 代码理解度检查(在代码发布前让你针对 diff 答题的门禁)想要填补的正是这个断层。本周一个叫 Sphinx 的开源 Git hook 火了一把,它就是这么做的:在你的 push 生效之前,它读取 diff,然后问你一个问题:出错时代码会怎么运行?
作为前机器学习工程师,我的第一反应是怀疑:又一道门禁?又一套仪式?但越想我实际见到的 agent 生成代码的故障模式,就越觉得这个想法瞄准了测试在结构上根本做不到的事。所以我们来做一次实在的对比:理解度检查,对比你已经拥有的自动化安全网,也就是测试、linter、类型检查和 CI,并看看各自究竟何时更占优势。
测试和 Linter 真正验证的是什么
自动化检查只回答一个问题:代码的行为是否符合检查的预期?这是一个完全在机器内部闭合的循环。测试编码了某人对正确行为的认知,代码要么满足它,要么不满足,合并时完全不需要人的理解。这是一项优点,正是它让上百名工程师可以把代码合进同一个仓库,而无需互相阅读对方的工作。
问题出在这个模型背后的隐含假设上:合并代码的人对代码“应该”做什么有准确的心智模型,而且测试套件也反映了这一点。对于手写代码,这个假设大多成立,代码是你自己写的,你和它较过劲,哪里有坑你心里有数。但对于 agent 生成的代码,这个假设会悄无声息地失效。你描述了目标,agent 产出了实现,而测试通过,是因为测试也是 agent 写的。那个绿色的对勾现在证明的只是:agent 的代码符合 agent 的测试。你的理解从来不在这个循环里。
这和我在为什么 AI 基准测试漏掉了大部分真实工作中写过的陷阱是同一个:指标只能证明它所衡量的东西。测试套件衡量的是对某些人想到并编码过的用例的行为一致性。它无法说明重试逻辑会不会在 Stripe 超时时重复扣款,agent 是不是选错了端点,或者“修复”是不是只是用吞掉异常的方式掩盖了竞态条件。这些都是理解层面的失败,而它们会顺利通过 CI。
理解度检查能抓住而测试会漏掉的问题
理解度检查翻转了验证的方向。它不再问“代码能跑吗?”,而是问“发布这段代码的人,知道出错时它会怎么做吗?”Sphinx 给出的示例很典型:你的 webhook 处理器现在会在调用 charge() 之前先保存 Stripe 事件 ID。超时后重试会发生什么?如果你回答“客户会被扣两次”,那就错了,已保存的 ID 会让重试直接短路。但接着追问:如果 charge() 本身失败了,下一次重试会怎样?它会跳过扣款。客户永远不会被收费,而且没有任何测试会失败,因为这是一个看起来正确的幂等模式,只是用错了顺序。
注意这类 bug 的性质。它不是语法错误、类型错误,也不是断言失败。它是一种只在特定失败序列下才会暴露的设计缺陷,而在上线前以较低成本抓住它的唯一办法,就是让人去推理失败路径。测试本来可以抓住它,前提是有人写过“事件 ID 持久化之后 charge 失败”的测试。但会写这个测试的人,恰恰是理解了顺序风险的人;如果这个人存在,我们也就不会走到这一步了。理解度检查探测的,正是那种能写出缺失测试的理解。
还有第二个好处,它与其说是抓 bug,不如说是在扭转一种缓慢的技能流失。很多开发者都有过同样的体验:拿 agent 硬磨出一个能用的结果,消耗了真实的精力,却什么也没学到。你发布得越来越多,理解得越来越少,差距还在不断累积。推送时的一道小测验,至少会强迫你对 diff 的逻辑做一次浅层重构,这往往足以把“我批准过的代码”重新变成“我真正拥有的代码”。我之前说过AI 工具正在悄悄改变开发者的思维方式;这类门禁是最早一批朝相反方向推动的工具之一。
自动化测试何时胜出
别把人工门禁浪漫化了。测试在好几个领域都明显占优,假装不是这样,最终只会让一道答题环节成为你唯一的安全网。
- 大规模回归检测。没有人的心智模型能扛住一次 3 万行的重构。测试每次提交都能以几乎不计的成本重新验证成千上万个行为。理解度检查对于一个 diff 大概只能覆盖三个问题左右。
- 机械性正确性。差一错误、格式化字符串写错、时区处理出错,这些问题在时间压力下回答问题的人,远不如机器能穷举检查得可靠。
- 可重复性。测试永远以完全相同的方式运行。而一道测验题的质量,取决于生成它的模型,也取决于你周五下午五点的精神状态。
- 重构信心。当行为本不应改变时,测试是唯一有意义的检查。理解代码并不能帮你验证你没有改动它。
还有一类代码,理解度检查只会增加摩擦而没有价值:生成的样板代码、机械性迁移、依赖升级、配置修改。让我回答 Prettier 格式化做了什么,那是一种税,而不是门禁。这一领域的任何工具都需要为低风险 diff 给出好的答案,否则人们会绕开它。值得注意的是,Sphinx 预见到了这一点,提供了 sphinx yolo 逃生口,并记录每一次跳过。这份日志的意义比看上去更大:一个所有人都悄悄绕过的门禁,比没有门禁还糟。

理解度检查何时胜出
理解度检查恰恰在人是最薄弱环节的地方胜出,而在 agent 辅助开发中,这种情况比我们愿意承认的要多得多。
- 失败路径推理。重试、超时、部分写入、分布式事务,这些代码路径只有在其他环节已经出错时才会执行。Agent 处理顺利路径还算不错,但对失败路径却相当随意。
- 高爆炸半径的 diff。支付、鉴权、数据迁移,以及任何涉及需要抵御灾难的系统的改动。这里答错的代价远超两分钟的答题摩擦。
- 你没写、也几乎没审过的代码。非开发同事提交的 1500 行 PR,或者 agent 通宵自动生成的分支。有位从业者报告说,他会用一句“针对这个 PR 考考我”的提示词,就是为了在结构化审查这类代码时保持专注。
- 入职培训与技能保持。如果你的团队发布 agent 代码的速度远超任何人学习业务领域的速度,那么理解度门禁就是现有最便宜的强制手段。
共同点在于:当 diff 影响重大、而人的心智模型又不可靠时,理解度检查才物有所值。这是一个特定的细分场景,而不是通用方案。
门禁本身的失效模式
如果不把这种做法可能出错的各种方式如实列出,那就是在误导你,因为怀疑者的观点确有道理。
首先是确认疲劳。我们以前就经历过:cookie 弹窗、“你确定吗?”对话框、大家都会狂点通过的安全问题。一个每次 push 都触发的门禁,会训练人们快速作答而非认真思考,不出一个月,这道测验就沦为仪式表演。缓解办法是划定范围:只在受保护分支和高风险路径上运行,而不是对功能分支的每次提交都运行。
其次是题目质量。测验由 LLM 读取 diff 生成,这意味着它会继承 LLM 的失效模式:问题浅显、“正确答案”本身有误、只盯着明显的改动而漏掉细微的那个。有位审查者指出,他们的版本有助于组织代码审查,但没能抓住错误的决策,比如调用了错误的端点。一道糟糕的题目比没有题目更糟,因为它为一种并未经过检验的理解盖上了认证章。这是真实的局限,我认为它无法被完全解决;这也正是为什么在这类工具中,agent 框架设计往往比模型选择更重要。
第三点,这一点让我笑了出来,因为它确实是真的:终究会有人把 agent 指向这道门禁,让它替自己答题。工具的设计已经考虑到了这一点:你的答案永远不会传给编码 agent,密钥也保持封闭。但没有什么能阻止一个铁了心的开发者把题目粘贴到聊天窗口里。到那时,这个工具衡量的就只是人愿不愿意作弊,而这是任何 hook 都修不好的文化问题。
理解度门禁并不验证代码。它验证的是:一个掌握上下文、并承担责任的人,确实认真参与了审查,而这正是 agent 工作流悄然移除的东西。
把门禁融入真实工作流
如果你想尝试,有两种实际的做法。低摩擦的路径是在你现有的 agent 里用基于提示词的 skill:“在我开 PR 之前,针对当前分支考考我。”有几个人确实这样做了,并表示它对组织审查很有帮助,但要注意,它依赖于你选择去运行它,而这恰恰会在你最匆忙的时候失效。
强制执行的路径是 Git hook。Sphinx 一分钟内就能装好:先运行 npm install -g sphinx-understand,再执行 sphinx install,它会同时接入 Git 和你的编码 agent,并询问应由哪个模型进行审查。它支持 Claude Code、Codex、Cursor 等工具,也支持纯 Git,并且可以接入任何兼容 OpenAI 的端点。如果你想自己动手,骨架就是一个 pre-push hook:
#!/usr/bin/env bash
# .git/hooks/pre-push — quiz gate for protected branches
protected_branch="main"
current_branch=$(git rev-parse --abbrev-ref HEAD)
if [ "$current_branch" != "$protected_branch" ]; then
exit 0 # only gate the branches that matter
fi
diff=$(git diff origin/main...HEAD)
if [ -z "$diff" ]; then
exit 0
fi
# Send the diff to your model of choice, get a scenario question,
# check the answer. Pseudocode for the core loop:
question=$(quiz_generate "$diff")
echo "$question"
read -r answer
if ! quiz_verify "$diff" "$question" "$answer"; then
echo "Not quite — here's why. Push blocked; try again."
exit 1
fi
有两个设计决策比代码本身更重要。门禁应该作用于分支,而不是每次 push:在 main 和发布分支的 push 上触发,最好也在“请求审查”时触发,因为在问题代码消耗队友的注意力之前把它拦下来,可以说是价值最高的位置。另外,记录每一次绕过。逃生口是必要的;而审计日志才是让它保持诚实的东西。
还要考虑它与人工审查的关系。我写过每一层代码审查都会让团队变慢,这一点我仍然坚持,所以不要把它叠加在所有环节之上。用它来替代第一道、也是最浅的一轮审查,即“作者到底知不知道这段代码在做什么”的那一轮。这正是理解度检查真正能自动化的部分。
我的建议
测试和理解度检查并不是竞争关系,它们验证的对象不同。测试验证代码;理解度检查验证为代码负责的人。agent 辅助开发打破了两者等同的隐含假设,这些门禁是一种务实的补丁,而不是噱头。
我的建议是:把测试套件作为首要安全网,这里的任何东西都不能替代它;然后只在受保护分支和高风险路径上,有选择地加入理解度门禁,并记录逃生口的使用。如果你的大部分代码都是手写的,你并不需要它;门禁的价值与你没写的那部分 diff 占比成正比。而如果这个测验开始让你觉得是个必须绕开的苦差事,那也是一个信号:它说明你交付代码的速度快到,连两分钟的停顿都显得昂贵。好好想想这件事。这个工具论点中令人不适的版本才是正确的:你可以外包思考,但理解必须留在自己手里,因为凌晨三点,当重试循环在无人买单地扣款时,值班的不是那个 agent,是你。


