对AI生成的代码改动进行隔离仓库验证、行为探测、静态扫描和对抗性二次审查,确保不引入边缘情况bug。
上个月,我对一个重试循环应用了一个 AI 建议的修复,却没有properly检查。这个改动看起来很干净:代码行数更少,命名更清晰,测试套件也保持绿灯。但我没注意到的是,模型把一个 sleep 移到了条件语句外面,导致每次成功请求现在都要付出一个本来只适用于重试的延迟。测试通过了,因为没有测试曾经覆盖过那个代码路径。
这次经历改变了我对待 AI 代码审查建议的方式。建议本身是一个假设,而不是答案,假设需要实验来验证。本文描述了我现在对每个非平凡建议运行的实验设置:一个隔离的仓库副本、一个拒绝接受"零测试运行"为成功的behavioral probe、一个静态扫描,以及一个adversarial的第二次模型审查。整个流程运行在免费的模型访问和免费的托管服务器上,所以没有per-call cost的借口来跳过second opinion。
明显的垃圾很容易拒绝。伤害你的建议有三个共同特征:
它们在局部看起来合理,但违反了一个存在于模型所看到的代码片段之外的invariant。
它们改变了edge-case行为——短路顺序、正则可达性、错误路径——而stated reasoning从未提及这些。
它们偶尔会削弱一个validation或sanitization步骤,重新打开一类多年前有人修复过的bug。
因此,验证pipeline需要产生三种证据:改变的代码仍然正常行为(测试),它没有引入已知bad patterns(静态分析),它的stated reasoning经得起 hostile scrutiny(第二个模型被要求寻找divergence,而不是同意)。
模型的编辑永远不会触碰我真正的working tree。我在一个一次性的git worktree中应用它,这样一个坏建议就是一个rm -rf就能让它消失的东西:
#!/usr/bin/env bash
# quarantine.sh — isolate an AI-suggested patch for verification
set -euo pipefail
PATCH_FILE="$1"
SCRATCH="$(mktemp -d)/suggestion-check"
git worktree add "$SCRATCH" HEAD
git -C "$SCRATCH" apply "$PATCH_FILE"
echo "$SCRATCH" # hand the path to the next stage
这不花一分钱,而且改变了审查的心理:建议是一个放在玻璃罩下的标本,不是一个我已经投入情感想要保留的half-merged编辑。
这类automation中经典的self-deception是一个匹配不到任何东西的测试选择器。绿灯输出,零信号。所以probe先计算匹配的测试数,然后将零作为一个hard failure:
#!/usr/bin/env bash
# probe.sh <worktree> <test-selector>
set -uo pipefail
cd "$1"
MATCHED=$(pytest --collect-only -q -k "$2" 2>/dev/null | grep -c '::' || true)
if [ "$MATCHED" -eq 0 ]; then
echo "REJECT: selector matched no tests — the suite has nothing to say about this change."
exit 2
fi
pytest -x -q -k "$2" || { echo "REJECT: behavioral probe failed ($MATCHED tests ran)."; exit 1; }
echo "OK: $MATCHED targeted tests passed."
零匹配时的REJECT是本文中最高价值的一行。在我的个人使用中,那个guard捕获的坏merge比静态扫描器还多——通常是因为它揭示了我信任的"通过套件"根本从未覆盖过被改动的模块。
一个基于pattern的扫描器(带有默认registry规则的Semgrep,或者你所用语言的等价工具)只针对改动的文件运行。它不会捕获逻辑bug,但能可靠地捕获"简化了input validation"这类回归。把diff上的任何发现作为人工审查门禁,而不是自动拒绝——false positives存在,但审查只需两分钟。
这里是人们因为第二次模型调用在按量计费计划上会花钱而跳过的地方。我不是问模型"这个改动正确吗?"——这会邀请一个自信地重新推导相同推理的过程——而是要求它攻击这个改动:
You are a skeptical examiner reviewing a proposed patch.
CONTEXT BEFORE THE CHANGE:
{original}
PATCH:
{diff}
AUTHOR'S STATED REASONING:
{rationale}
Rules:
- Do not summarize or praise the patch.
- Name up to three concrete inputs or system states whose behavior this
patch changes without the reasoning mentioning it.
- Classify each as SAFE, UNSAFE, or UNDETERMINABLE from the shown context.
- If the patch touches parsing, authentication, concurrency, or error
handling, state the one invariant a human must verify by hand.
- If there is genuinely nothing, output exactly: NO DIVERGENCE FOUND.
Fabricating concerns is a failure.
任何OpenAI兼容的客户端都可以发送这个。黄金在UNDETERMINABLE bucket中:这是一份精确的、生成式的清单,列出我个人仍然欠审查的东西。"看起来没问题"的回答几乎不携带信息;而三不可验证声明的列表携带很多。
Cross-examination阶段曾经是成本悄悄混入的地方——每个建议多一次模型调用,乘以每个PR。两种方法将边际成本降至零:
托管的免费额度。MonkeyCode目前提供免费的模型访问以及免费的服务器选项,这意味着第四阶段的调用可以打到托管端点而不是计费的API key,所以adversarial pass可以运行在每个建议上,而不是只在那些令人生畏的建议上。披露:本文是作为MonkeyCode产品推广的一部分准备的。把"免费"理解为关于当前可用性的声明,而不是永久契约——免费额度会改变它们的配额、延迟和模型阵容,所以在将其接入你的团队所依赖的任何东西之前,先确认当前的条款。
你自己的硬件。一个OpenAI兼容的本地服务器(Ollama、llama.cpp)通过改变一个base URL就能插入同一个客户端。中等规模的量化模型比frontier模型弱,但上面的cross-examination prompt是一个结构化的checklist任务,中等规模的模型处理这些比它们的benchmark排名所显示的要好。
这个pipeline故意对哪个后端回答不敏感。prompt、证据类别和decision rules保持不变。
独立性是必需的。用与创作建议的相同模型进行cross-examination会保留其blind spots。使用不同的模型,理想情况下是不同的provider。
扫描器有一个已知的上限。它们匹配模式,而不是novel exploitable逻辑。
免费产品会移动。任何免费额度的配额、可用性和延迟都可能毫无预警地变化;保持本地fallback被常态化使用,这样在你需要的那天它能正常工作。
范围很重要。这个pipeline假设一个review-sized的diff。一个40文件的agent生成的refactor需要characterization tests和分阶段推出,而不是一个checklist。
谁应该跳过这个:已经有强制coverage gates和成熟mutation testing的团队已经拥有这个价值的大部分。而且如果你的代码由于合规原因不能离开你的边界,托管的免费额度根本就不是一个选项——在本地运行examiner或者干脆不运行。
这不是关于怀疑;这是关于让信任成为每个建议单独赢得的东西,而不是工具默认享有的东西。一个隔离的worktree、一个在零测试时大声失败的probe、一个pattern扫描,以及一个hostile的second opinion覆盖了大部分风险——而且凭借免费的模型访问和免费的服务器,second opinion的成本只是三十秒。如果你采用了这个pipeline,我想听到的是哪个阶段对你触发得最多——我的赌注仍然在零测试guard上。