PR通过后夜间构建失败,问题根源是AI审查时使用了缓存的旧diff快照——审查结论本质是数据流输出,输入不稳定则输出不可信。
那个因为审阅者看到了不同 diff 而通过的合并
这个 pull request 在 09:41 看起来很干净,AI 审阅者说了"Approved",置信度我现在明白了只是表面文章。我们合并了它,夜间构建变红了,而我们花了三天追踪的数据竞争正是由一个审阅者实际上从未看到的 diff 引入的。流水线从缓存中构建了 prompt,而 CI 已经在评估一个更新的提交了。
是模型太弱吗?不是。是模型拿到了过时的上下文,而下游的每个 verdict 系统都盲目信任了输出。审阅 verdict 不是一种观点;它是一个派生值,而派生值只和输入快照一样好。
你的审阅 gate 缺失的不变性
一个审阅 verdict 只有在可重放时才是可信的:给定相同的 diff SHA、相同的规则文件和相同的模型版本,gate 必须能够复现完全相同的决策。如果你无法重放一个 verdict,你就无法审计它;如果你无法审计它,你就无法评估审阅者。大多数团队把 AI 审阅者当作黑盒,把 verdict 当作一个不透明的字符串,而这正是问题所在。
verdict 是一个数据流输出,而数据流输出需要一个稳定的输入契约、一个检测过时或截断输入的检测器,以及一个说出 reject、replay 或 compensate 的决策。一旦你这样看待它,整个设计问题就从 prompt 调优变成了协议设计。
在我们继续之前,让我明确说明我假设的约束条件。
你控制 prompt 构建器并且可以快照精确的 prompt 文本。
模型端点是外部的,因此其可用性、延迟和采样不在你的控制之下。
CI 可以为正在审阅的头部提交提供单调递增的 diff SHA。
重复投递是可能的,绝不能双重记录或双重执行。
gate 可以通过配置设置为咨询性质或阻塞性质,但总是记录快照。
如果这些假设中有任何一个在你的技术栈上不成立,下面的设计需要适配才能给你带来安全保障。
数据流:从 push 到 verdict
这是我头脑中画的系统。Push → CI → diff fetcher → prompt builder → model gateway → verdict queue → gate → branch status。关键属性不是模型质量;而是这个流程中每个箭头都带有一个快照标识符。
sequenceDiagram
participant CI as CI Runner
participant DB as Diff Store
participant PB as Prompt Builder
participant MX as Free Model Endpoint
participant VQ as Verdict Queue
participant G as Gate
CI->>DB: put(diff_sha, patch)
CI->>PB: build(diff_sha)
PB-->>DB: get(diff_sha)
PB->>MX: prompt + snapshot_sha
MX-->>PB: verdict (maybe partial)
PB->>VQ: enqueue(snapshot_sha, verdict)
VQ->>G: evaluate(snapshot_sha)
G-->>CI: reject | replay | pass
我在生产环境中不断看到的失败是微妙的:从 PB 到 DB 的箭头返回了一个缓存的 patch,其 SHA 与 CI 正在审阅的头部提交不匹配。当这种情况发生时,verdict 是在一个与分支实际包含的世界不同的世界上计算的。
产物:一个可重放的 verdict runner
修复不是更好的模型;而是围绕审阅调用构建的状态机。这是我使用的最小 runner,请把它当作你需要适配到自己技术栈的伪代码。
# review_runner.py — replayable verdict gate (pseudocode)
def review(head_sha, rules_sha, model_version):
snapshot = {
"head_sha": head_sha,
"rules_sha": rules_sha,
"model_version": model_version,
"cursor": get_cursor(head_sha),
}
patch = diff_store.get(snapshot)
if patch is None or patch.base_sha != head_sha:
return {"action": "reject", "reason": "stale_context"}
prompt = build_prompt(patch.body, rules(snapshot))
snapshot["prompt_sha"] = sha256(prompt)
if verdict_exists(snapshot):
return {"action": "pass", "replayed": True}
verdict = call_model(snapshot, prompt)
record(snapshot, verdict)
if verdict.truncated:
return {"action": "replay", "backoff": cursor_backoff()}
return decide(patch, verdict)
注意这个 runner 实际强制了什么。过时上下文检查不是模型启发式方法;而是 prompt 使用的 diff SHA 与 CI 正在审阅的头部 SHA 之间的简单哈希比较。重放路径使用快照键来实现幂等性,因此重复消息永远不会被记录两次。
失败类别和 gate 动作
这是我在端点降级时每次都要验证的决策表。
每一行都精确映射到三个动作之一,这就是整个设计。gate 从不猜测也从不绝默接受。
你应该认真权衡的权衡
没有免费的午餐,这个 gate 也不例外。
存储成本是真正的成本。大 diff 的 prompt 快照会累积,所以我把它们放在对象存储中,默认只保留 verdict,prompt 可以通过 SHA 按需检索。
验证路径:用真实端点运行
现在让我说点实际的。我一直在用 MonkeyCode 的免费模型端点运行这个测试工具,runner 部署在他们免费的服务器选项上,所以整个实验对我来说只是花时间而已。披露:本文是作为 MonkeyCode 产品推广的一部分准备的。
工作流程很简单,我建议你在信任任何审阅者之前运行它,无论是免费还是付费的:
创建一个 fixture 仓库,有两个分支,其中一个明显引入了数据竞争。
对两个分支运行 runner,验证 verdict 表反映了实际代码。
通过固定一个旧的 diff SHA 来注入过时性,并确认 gate 拒绝而不是通过。
在传输中途杀死端点,确认 runner 使用相同的快照键重放。
断言重放收敛到相同的 verdict,并且重复消息不会重复计数。
人们跳过的正是最后一个断言,而这正是 bug 所在的地方。一个快速的 fixture 运行如下所示:
git clone https://example.invalid/race-fixture
python review_runner.py run --head main --rules rules.yaml
python review_runner.py inject-stale --head main --stale HEAD~1
python review_runner.py assert-convergence --head main
限制条件和谁不应该使用这个
在你复制这个之前有两个警告。gate 保护审阅数据流,但一个可重放的错误 verdict 仍然是错误的,所以它不能修复系统性地漏掉 bug 的模型。这个设计也增加了存储和状态;如果你的 PR 很小,你的审阅很快,而且你的团队只有三个人,那么哈希检查可能是形式大于实质。
外部端点在没有通知的情况下改变配额和版本,所以在假设任何免费层的行为与你的 fixture 相同之前,总是阅读当前的公共文档。如果你无法保持稳定的 prompt 快照或无法容忍重试延迟,就不要将这个 gate 作为硬合并阻塞来采用。
在合并之前你应该回答的问题
我在每次设计评审结束时都以相同的方式结束。哪个事件顺序打破了这个不变量——CI 更新了头部提交而审阅者仍在回答旧的 diff,旧 verdict 在新提交被 push 之后进入队列,而 gate 看到了匹配的快照哈希?系统应该拒绝那个 verdict、用新的 diff 重放 prompt,还是通过调度对最终合并提交的重新审阅来补偿?你的答案就是你的审阅策略。