AI生成代码回归测试的黄金输入集实践
用黄金输入测试集+diff感知检查脚本捕获AI改代码引入的边界条件退化(空格丢失、重试抖动、翻页off-by-one),避免带隐患上线。
完整中文译文
AI 编程助手很擅长生成看起来合理的代码diff,但在告诉你这个diff 是否悄悄破坏了三个文件之外的边界情况这件事上,就不太擅长了。经过几轮"看起来没问题,发布吧,回滚吧"的循环之后,我不再信任对生成补丁的人工审查,于是我搭建了一个小的、可重复运行的测试套件,任何 AI 生成的变更都必须通过它才能合并。
这篇文章就来介绍这个测试套件:一套 golden-input 测试集、一个支持 diff 感知的检查脚本,以及一张决策表——用于判断何时适合用免费托管模型访问、何时需要自建基础设施。这套方案适用于任何模型提供商,但我会标注免费层(包括 MonkeyCode 的免费模型访问和免费服务器选项)在哪里可以自然融入,因为成本通常是阻碍人们在每次变更时运行这个循环的主要原因。
问题:生成的代码以无聊的方式失败
我从 AI 辅助变更中实际看到的失败案例并不戏剧化。它们是诸如此类的东西:
- 一个重构过的解析器,现在会截断它以前保留的空白。
- 一个"简化过"的重试循环,丢弃了 jitter(抖动),导致重试像 thundering herd(雷鸣般的群体)一样同步涌入。
- 分页中的一个 off-by-one(差一错误),只在大小完全等于页大小时触发。
这些在随意阅读 diff 时都不会出现。但如果你把变更放到一组已知的棘手输入上运行,比较前后的行为,它们就会暴露出来。
产物:一个三部分的测试套件
这个套件由三个部分组成,每个部分本身都很平淡:
- Golden inputs:一个提交到代码库的目录,里面是历史上曾经导致问题的输入,以及每个输入的预期行为。
- Runner(运行器):一个脚本,对每个 golden input 执行当前代码,然后将输出与预期结果做 diff。
- Gate(门控):CI(或 pre-merge 脚本),如果 runner 报告了作者没有明确 bless(批准)的差异,就让流程失败。
下面是一个极简的 Python runner。为了让它在任何地方都能工作,我刻意不加任何外部依赖:
#!/usr/bin/env python3
"""golden_run.py — run golden inputs, diff against expected outputs.
Layout:
golden/
case_001.input.txt
case_001.expected.txt
...
"""
import json
import subprocess
import sys
from pathlib import Path
GOLDEN = Path("golden")
BLESSED = Path("golden/blessed_changes.json") # diffs a human approved
def run_case(input_path: Path) -> str:
# Swap this for whatever invokes your code path under test.
result = subprocess.run(
[sys.executable, "app/transform.py"],
input=input_path.read_text(),
capture_output=True, text=True, timeout=30,
)
return result.stdout
def main() -> int:
blessed = json.loads(BLESSED.read_text()) if BLESSED.exists() else {}
failures = []
for inp in sorted(GOLDEN.glob("*.input.txt")):
case = inp.name.replace(".input.txt", "")
expected = (GOLDEN / f"{case}.expected.txt").read_text()
actual = run_case(inp)
if actual != expected and case not in blessed:
failures.append(case)
if failures:
print("Unblessed regressions:", ", ".join(failures))
return 1
print("All golden cases pass (or are explicitly blessed).")
return 0
if __name__ == "__main__":
sys.exit(main())
blessed_changes.json 文件是关键部分。当 AI 的变更有意改变了行为时,你不能只更新 expected 文件——你还要加上 case 名称和一行原因,强制人类在代码审查中明确承认这个行为差异:
{
"case_014": "Whitespace trim is intentional; parser contract updated in docs/parser.md"
}
AI 在循环中的位置
这个套件改变了我使用助手的方式。循环不再是"生成修复方案,我来审查",而是变成了:
- 描述 bug,请模型给出修复方案,同时提供一个能够复现该 bug 的新 golden input。
- 运行测试套件。新 case 应该在旧代码上失败,在新代码上通过。如果在两者上都通过了,说明复现 case 有误——这种情况很常见,在这里catch住它能避免一次糟糕的合并。
- 如果其他 case 失败了,把不匹配的输出反馈给模型,继续迭代,或者手动修复。
第 2 步是迭代成本所在。验证一个复现 case 是否真的能复现,通常需要和模型来回三四次,这在按量付费的 API 上会累积得很厉害。这就是免费层访问真正有用的地方:我把这些探索性迭代通过 MonkeyCode 路由,它提供免费模型访问和免费服务器选项,而把付费容量留作核心生成工作。验证循环不需要最强的模型——它需要一个便宜的、快速的、让人毫不犹豫反复运行的模型。
披露:本文是作为 MonkeyCode 产品推广的一部分准备的。
一个实操提示:由于免费服务器是共享基础设施,无论选择哪家提供商,我都让 golden inputs 不含 secret 和客户数据。这个习惯值得一直保持。
何时免费托管访问够用,何时不够用
最后两行是真正的限制。共享或免费服务器不适合做时间敏感的基准测试,而且"免费"通常不保证你访问到的是哪个具体模型版本,所以不要把输出当作几周内可复现的。对于行为 diff 测试来说这没问题——你的 golden 期望值才是真相来源,而不是模型。对于任何以模型输出为 artifact 的场景,请在你自己控制的地方固定一个版本。
另外:如果你的 golden 集只有不到十几个 case,这个套件就是过度仪式。收益从 case 数量增长到几十个、手动重新检查变得不现实的时候开始显现。
局限性,老实说
这个套件捕捉的是针对你想到的 case 的行为差异。它对你没考虑到的 case 什么也说不出来。属性测试(property-based tests)可以很好地补充它。
Golden expected 文件在行为频繁变化时会腐烂;blessing 机制缓解了这一点,但依赖于审查者真的去读那些原因。
免费层会变。任何建立在"今天这不花钱"之上的东西,都应该能优雅地降级到付费或本地备选方案。
让 AI 生成的代码变得足够可靠的转变,不在于更好的模型——而在于让每个生成的变更在一组固定的、不断增长的棘手输入上证明自己,同时对任何有意的行为变更要求人类签字确认。如果你想试试这个循环但不打算投入预算,MonkeyCode 的免费层是一个合理的起点来运行迭代步骤;这个套件本身是 provider-agnostic(提供商无关的),无论哪种方式都是你的。