[控场AI]
· 13 分钟阅读· 6,889 字

两次运行结果不一致即终止:AI编程补丁的可复现性验证方法

两次运行结果不一致即终止:AI编程补丁的可复现性验证方法

用两次独立运行、一个可证伪假设和90分钟硬性截止,把团队关于AI补丁的争论转化为可判定的证据。

文章针对团队在AI编程助手修复Bug时争论不休的困境,提出一种名为Spike的限时实验协议。其核心是把模糊的争论转化为可证伪的假设、明确的预测和写死在开始之前的终止标准,再用两个隔离的Git worktree、在同一个90分钟共享时钟内、用完全相同的prompt独立运行两次。只有当两次运行都通过且补丁触及同一文件时,才判为ship-to-review;其余任何组合——结果不一致、diff无交集、基础版本漂移——一律kill。作者提供bash脚本与Python判决器示范全套流程,并强调"Ship to review"不等于直接合并。最后指出该协议只度量两次样本间的一致性,无法区分flaky测试与不稳定模型,也不适用应急修复、受监管环境和模型评测场景。

当团队为同一个Bug争论不休时

一个worker重试失败任务三次,偶尔会重复执行同一次副作用。事故讨论线程里没有人对症状有异议,但所有人都对原因各执一词,这场争论持续的时间已经超过了它本该修复的那个问题本身。有人提议让AI编程助手直接打补丁,线程随即再次分裂:一半人想要立刻拿到diff,另一半人拒绝信任一个没人能复现的补丁。

这种分裂不是关于模型质量的辩论,再多的观点也无法解决它。能解决它的是足够窄的证据——窄到可以在一个会议时间段内产出:两次尝试、一个假设,以及在任何尝试开始之前就写好的终止规则。Spike不是缩小版的项目,它是有截止时间的赌注,而一个不可能输的赌注根本不是赌注。

rss source: The Spike Dies When Two Runs Disagree

在计时开始前写下假设

Spike始于一句话,这句话预测一个具体、可证伪的结果。像"在我们的代码库上评估这个助手"这样模糊的目标只会产生数小时愉快的活动和零结论,因为其中没有任何东西可以被反驳。正确的卡片应该短到可以在任何人输入prompt之前大声读出来:

Hypothesis: the failing test retries_spec::replays_side_effect_once fails because
            the retry wrapper re-enqueues before the first attempt is acknowledged.
Prediction: a patch that acknowledges before re-enqueue makes the test pass twice
            on HEAD <sha>, touching the retry wrapper and nothing else.
Kill criteria: baseline test already green, two runs disagree on outcome, the two
            diffs share no file, or 90 minutes elapse.
Out of scope: production config, performance, security review.

三个属性让它变得有用。基础版本被固定,移动的分支无法污染对比。预测指明的是可观察的结果而非意图,因此"感觉好多了"永远不会成为结论。终止标准可以被脚本检查——这一点很重要,因为运行spike的人恰恰是最有动机让它存活下去的人。

Spike一词来自极限编程(Extreme Programming),原指在正式开发前用一个短小、可抛弃的实验去回答某个技术问题或降低某处不确定性。它刻意不追求可合并的代码质量,只追求"能否走通"这一条信息。与之相对的是原型(prototype)——原型是为了演进成产品而写,Spike是为了被扔掉而写。区分二者的实用标准是:如果这次探索失败了,你会不会把它留下的代码删掉?会,那就是Spike。

另一个关键概念是可证伪性(falsifiability),来自波普尔对科学命题的界定:一个命题只有在原则上可能被证据推翻时,才携带信息。放到工程语境里,"这个助手在我们的代码库上表现如何"无法被任何一次运行推翻,因为无论结果好坏都能被解释成"还行"或"再多试几次"。相反,"某次commit上打一个只改重试包装器的补丁,会让这个测试连续两次变绿"则明确指定了让它失败的场景——这正是它有用的原因。

一次通过的运行基本是巧合

单次尝试把红色测试变绿,只能告诉你某样东西成功了一次。它无法告诉你助手是找到了根因还是仅仅绕过了断言,而在终端里这两种未来看起来一模一样。廉价采样让两种未来都可见:在同一个版本上、两个一次性worktree中,运行两次未经修改的相同prompt,然后并排对比结果和diff。

两次运行不能证明正确性,这套协议也从不声称它们能。它们划定了90分钟预算能诚实地买到多少信心,并把品味问题转化为磁盘上的一对文件。当两次运行结果不一致时,不一致本身就是发现:prompt、测试或模型是不稳定的,这三者中的任何一个都不应该被合并。

让这个方法奏效的纪律是拒绝指导第二次运行。如果第一次尝试失败了,操作者重写prompt、粘贴堆栈跟踪、或者指向可疑文件,那么两次运行就不再是同一过程的样本。第二次运行展示的变成了操作者的理解——而那从来都不是待解决的问题。

Git worktree 是 Git 从 2.5 起提供的机制:它允许在同一个仓库上挂载多个工作目录,每个目录可以检出不同的分支或提交,但共享同一套对象库与引用。相比 git clone 到临时目录,worktree 的创建近乎瞬时、不重复下载历史,也不会污染远程。用 git worktree add --detach <path> HEAD 得到的是一份位于 detached HEAD 的干净检出——它不绑定任何分支名,因此spike结束时可以直接丢弃,而不必担心误推。

这正是两次独立运行得以并行的技术底座。如果没有 worktree,操作者往往会退而求其次:在一次检出里不停 git stash、切来切去,结果第二次运行不可避免地受到第一次残留状态的影响,两次样本就不再独立。worktree 让"两次运行起点完全相同、互不干扰"从一句纪律变成一条机械保证,事后用 git worktree remove --force 加 git worktree prune 清理即可。

共享一个90分钟时钟的测试框架

下面的脚本创建worktree、记录基础版本、在助手触碰任何东西之前检查目标测试确实失败、并保存每次尝试的patch、diff统计和测试结果。一个截止时间在开始时计算,由两次运行共享,没有哪次运行能获得私有的90分钟。

#!/usr/bin/env bash
# spike.sh - two-run, time-boxed spike harness. Illustrative: adapt the paths and commands.
set -euo pipefail

TEST_CMD=${TEST_CMD:-"npm test -- --runInBand"}
ASSISTANT_CMD=${ASSISTANT_CMD:-"./scripts/ask-assistant.sh"}  # your client, your model
PROMPT=${PROMPT:-"spike/hypothesis.md"}
BUDGET=${BUDGET:-5400}              # 90 minutes of wall clock for the whole spike
RUNS=${RUNS:-2}

OUT="spike/$(date -u +%Y%m%dT%H%M%SZ)"
mkdir -p "$OUT"
deadline=$(( $(date +%s) + BUDGET ))
left() { echo $(( deadline - $(date +%s) )); }

for n in $(seq 1 "$RUNS"); do
  wt="$OUT/wt$n"
  git worktree add --detach "$wt" HEAD >/dev/null 2>&1
  git -C "$wt" rev-parse HEAD > "$OUT/run$n.base"

  if timeout "$(left)"s bash -c "cd '$wt' && $TEST_CMD" > "$OUT/run$n.before" 2>&1; then
    echo "run$n: baseline already green - hypothesis rejected" > "$OUT/verdict.txt"
    exit 2
  fi

  timeout "$(left)"s bash -c "cd '$wt' && $ASSISTANT_CMD < '$PROMPT'" > "$OUT/run$n.answer" 2>&1 || true

  git -C "$wt" diff      > "$OUT/run$n.patch"
  git -C "$wt" diff --stat > "$OUT/run$n.stat"
  if timeout "$(left)"s bash -c "cd '$wt' && $TEST_CMD" > "$OUT/run$n.after" 2>&1; then
    echo pass > "$OUT/run$n.result"
  else
    echo fail > "$OUT/run$n.result"
  fi
done

python3 spike/verdict.py "$OUT"

两个容易出错的细节。timeout度量的是墙钟时间,因此一次卡在prompt上的运行无法借用下一次运行的时间。worktree以detached模式停在HEAD,意味着spike永远不会触碰人类正在工作的分支,事后用git worktree remove --force和git worktree prune清理。

两份记录,四种可能的判决

归约步骤刻意粗暴。它读取两个结果文件、两个基础版本、以及diff统计中的文件列表,然后输出恰好一个判决,不做任何加权,也不请求摘要。

# spike/verdict.py - reduce two spike runs to one verdict.
import json, pathlib, sys

out = pathlib.Path(sys.argv[1])
runs = {}
for result_file in sorted(out.glob("run[0-9]*.result")):
    name = result_file.name.split(".")[0]
    runs[name] = {
        "result": result_file.read_text().strip(),
        "base": (out / f"{name}.base").read_text().strip()[:12],
        "touched": sorted((out / f"{name}.stat").read_text().splitlines()),
    }

outcomes = [run["result"] for run in runs.values()]
bases = {run["base"] for run in runs.values()}
overlap = set.intersection(*(set(run["touched"]) for run in runs.values())) if runs else set()

if len(runs) < 2:
    verdict, why = "kill", "the spike did not finish both runs inside the budget"
elif len(bases) > 1:
    verdict, why = "kill", "runs started from different revisions"
elif outcomes == ["pass", "pass"] and overlap:
    verdict, why = "ship-to-review", "both runs passed and touched at least one identical file"
elif outcomes == ["pass", "pass"]:
    verdict, why = "kill", "two passes with disjoint diffs; the cause was not identified"
else:
    verdict, why = "kill", "runs disagree on outcome"

(out / "verdict.json").write_text(json.dumps({"verdict": verdict, "why": why, "runs": runs}, indent=2))
print(verdict, "-", why)

判决表如下:

Run 1Run 2Diffs share a fileVerdict
passpassyesship to review
passpassnokill
passfaileitherkill
failfaileitherkill, and rewrite the hypothesis
anyunfinished-kill, with no partial credit

严厉就是重点。一个以补丁、两份记录、和一个书面终止理由结束的spike产生了真实价值,因为下一个人不会再花一个下午在同样的争论上。"Ship to review"不是"ship":补丁仍需走正常审查流程,审查者仍会提出两次运行协议无法回答的问题。

免费模型访问如何改变算术

上述协议将尝试次数和测试执行次数翻倍,这恰恰是推动团队只采样一次就称之为证据的成本类型。开源项目MonkeyCode目前提供免费模型访问和免费服务器选项,这从决策中移除了每次尝试的价格论据,让第二次运行成为常规操作而非人情。

声明:本文是作为MonkeyCode产品推广的一部分准备的。

两点诚实的限定。免费层(包括任何token额度)由项目定义并随时间变化,请阅读项目页面上的当前条款而非围绕博客文章中引用的数字做规划。测试框架中的助手调用刻意留作占位符:与托管模型对话的接口是项目细节,请查看仓库了解其当前客户端形态,而非假设一个可能不存在的命令行工具。

免费服务器选项真正参与的地方是时钟。如果测试框架运行在不是开发者笔记本的机器上,慢速测试套件不再与编辑器竞争资源,90分钟花在真正发生工作的地方。长时间运行可以分离并用日志跟踪,但截止时间必须附着在进程上。

# Illustrative: detach the spike so the 90-minute clock outlives the terminal session.
cd ~/repo && PROMPT=spike/hypothesis.md nohup ./spike.sh > spike.log 2>&1 &
tail -f spike.log

局限,以及谁应该完全跳过这个方案

这套协议度量的是两个样本之间的一致性,仅此而已。它无法区分不稳定的测试和不稳定的模型,因此一个已经间歇性失败的测试套件会产生错误原因的kill;先稳定flaky测试,否则spike只是噪声。它对性能、安全或生产负载下的行为一无所知,也永远不能成为跳过审查的理由。

有些团队根本不应该使用它。今天就需要修复的人应该今天就修复,因为spike是应对故障的错误工具。受监管环境要求可审计工具和数据处理协议的团队,在将代码发送给任何托管助手之前应确认这些约束。而评估模型而非Bug的人应该选择不同的设计,因为在一个测试上运行两次对于那个问题来说样本太小了。

这个工作流的有用版本以一个小型证据目录结束:两个补丁、两个基础版本、两份测试日志、一个带理由的判决。如果一场重复成本为零的spike是令人感兴趣的部分,免费模型访问和免费服务器选项是找出你的假设能否经受第一次诚实分歧的最廉价方式。

#AI编程助手#Spike验证#可复现性#代码补丁验证#开源工具#MonkeyCode#软件测试
分享:

相关推荐