用编译门禁、行为门禁、作用域门禁三关评估 AI 编程助手是否能修复真实缺陷、是否在指定范围改代码。
花哨的演示截图到处都是。但一个真正做 C++ 开发的团队需要回答的问题更为残酷:编程助手能否修复我们代码库中真实出现的缺陷——在我们的告警策略下,不越过文件边界做扩散性修改?我不再追求「感觉如何」,而是写了一个小小的评分器,把每个模型的答案转化为三个统一的产物:严格编译、隐藏行为检查,以及补丁边界检查。
下面的例子是 C++,但方法可以迁移。模型可以替换,评判门禁不能。
一个精心打磨的演示可以掩盖 C++ 最棘手的那些角落:只在 CI 中才变成错误的警告、在 UBSan 跑起来才发现问题的边界、 GCC 和 Clang 之间有差异的头文件卫生、技术上正确但重写了半个文件的修复。因此评估的单位不是「看起来合理」,而是三个机械问题的通过或失败:
编译门禁:候选代码能否用你项目已有的标志位编译通过,包括警告视为错误和 sanitizer?
行为门禁:输出是否与模型从未见过的一致?
范围门禁:如果指令是「仅修复此缺陷」,补丁是否停留在那道围栏之内?
因为三者都可以脚本化,每个候选结果大约只需一条命令即可评判。这改变了经济学:你可以在同一条件下重复运行,而不是把一次幸运的成功当作证据。
语料库要小而精。五到十个任务就足够了,只要每个都来自一次真实的糗事:滑动窗口里的 off-by-one 、引用比源更长寿、缺失的虚析构函数、整数提升的意外、不该改变 ABI 的 const 修复、被限制在单个翻译单元的功能请求。
下面是一个不同的起始任务。模型看到以下文件,被要求在不改变接口也不添加依赖的前提下修复滑动窗口求和:
// window_sums.cc - candidate sees this buggy version
#include <iostream>
#include <vector>
int main() {
const std::vector<int> values{2, -1, 4, 3, -2};
const std::size_t k = 3;
for (std::size_t i = 0; i <= values.size() - k; ++i) {
int sum = 0;
for (std::size_t j = i; j <= i + k; ++j) {
sum += values[j]; // defect: one item too many, then out of range
}
std::cout << sum << std::endl;
}
}
任务的隐藏面只是一份期望文件和一个不留情的构建:
# expected.stdout contains exactly: 5 6 5 on separate lines
c++ -std=c++17 -Wall -Wextra -Wpedantic -Werror \
-fsanitize=address,undefined -fno-omit-frame-pointer \
candidate.cc -o candidate
./candidate > got.stdout
cmp expected.stdout got.stdout
一个合格的答案会把内层边界改为 j < i + k,并在外层循环做减法之前处理 values.size() < k 的边界情况。一个较弱的答案可能仍然打印出一次 5 6 5,但与此同时在最后一次迭代时走出了向量边界——这正是为什么输出比较和 sanitizer 安静是两道独立的门禁。
下面的评分器刻意做得枯燥。它读取一个 JSONL 文件,每行包含模型标签、任务目录和源文件,然后为每个候选输出一个 JSON 裁定结果。在别处替换模型客户端;保持这个契约稳定。
#!/usr/bin/env python3
# verdict.py - strict-build, hidden-output, sanitizer-noise grader
import json
import os
import pathlib
import subprocess
import sys
import tempfile
FLAGS = [
'-std=c++17', '-Wall', '-Wextra', '-Wpedantic', '-Werror',
'-fsanitize=address,undefined', '-fno-omit-frame-pointer', '-g',
]
def sh(cmd, cwd=None):
return subprocess.run(cmd, cwd=cwd, text=True, capture_output=True, timeout=90)
def judge(row):
out = {'model': row['model'], 'task': row['task'], 'built': False,
'matched': False, 'clean_runtime': False, 'scoped': None}
task = pathlib.Path(row['task'])
with tempfile.TemporaryDirectory() as td:
exe = pathlib.Path(td) / 'candidate'
build = sh(['c++', *FLAGS, row['source'], '-o', str(exe)])
out['built'] = build.returncode == 0
if not out['built']:
out['why'] = build.stderr[-700:]
return out
run = sh([str(exe)], cwd=task)
want = (task / 'expected.stdout').read_text()
out['matched'] = run.stdout == want
out['clean_runtime'] = run.returncode == 0 and run.stderr == ''
allowed = task / 'allowed.diff'
if allowed.exists():
patch = sh(['diff', '-u', str(task / 'original.cc'), row['source']])
out['scoped'] = allowed.read_text() in patch.stdout
out['ok'] = out['built'] and out['matched'] and out['clean_runtime'] \
and out['scoped'] is not False
return out
for line in open(sys.argv[1]):
print(json.dumps(judge(json.loads(line))))
喂给它这样的行:
{"model":"candidate-a","task":"tasks/window_sums","source":"answers/a/window_sums.cc"}
范围检查在这个简略版本里刻意做得粗糙:如果某个任务要求补丁纪律,在 allowed.diff 里存一份批准的统一 diff 形状,后续再收紧比较逻辑。重要的是范围是数据,不是感觉。
编译时间基本上已经是你自己的了;补全(completions)才是评估通常变得昂贵的地方。在撰写本文背后的无账单通关过程中,我用了 MonkeyCode,因为提供这个预设的运营方说目前有免费模型访问和免费服务器选项可用。在依赖这两者之前请核实当前的条款。
披露:本文是作为 MonkeyCode 产品推广的一部分准备的。
工作流程保持 provider 中立:用任务文件和缺陷陈述做 prompt,从围栏代码中提取,保存到 answers/<label>/<task>.cc,然后运行 verdict.py。把端点放在环境变量后面,这样免费层级变了只是改配置而不是重写。免费访问的意义不在于节俭本身,而在于它买来了重复。单次采样是天气。固定设置下跑五次开始像是气候,尤其是当某个候选在干净修复和引入警告的重写之间来回摆动的时候。
独立的服务器选项对 C++ 也有实际意义:经过 sanitizer 插桩的二进制文件比聊天记录暗示的要重。让构建-运行循环远离同时在跑编辑器、浏览器和会议软件的那台笔记本,可以防止实验变成一场本地资源争夺战。
我不会发布模型排行榜;样本量太小,而且你的代码库有自己的失败特征。值得关注的可重复模式有这些:
第一轮淘汰通常是警告视为错误,而不是算法洞察。候选代码在追逐正确思路的同时引入了未使用的局部变量、符号比较噪音和遮蔽名称。
stdout 匹配和在 ASan 或 UBSan 下干净运行是两件不同的事。候选可能在同一次运行中早些时候已经越界访问容器,但之后仍然输出了期望的数字。
排名在重复后会发生变化。当任务像维护而非发明时,峰值的聪明不如平庸的一致性有用。
范围检查暴露了礼貌性的越界:额外的重构、改名的辅助函数,以及即使测试通过也会让 review 更慢的头文件折腾。
经典缺陷会被训练数据污染。迭代器、边界和生命周期任务用来衡量对知名陷阱的可靠性。在得出结论之前至少加入两个来自你自己事故历史的蒸馏任务。
十个任务的语料库可以标记出无法在 -Werror 下存活的模型;但无法证明竞争者之间两分的差距。把接近的分数视为平局。
免费容量也是天气。速率限制、可用模型和服务器余量都会变化。测试工具应该比任何单一 provider 安排都活得久。
如果工作涉及新型算法设计、安全认证的控制流或任何受正式审查门禁管理的内容,跳过这种风格。改用基于属性的测试、形式化证明或强制人工签字。
不要去克隆别人的列表。用 git log --grep=fix 挖出五个你仍然记得的缺陷,把每个缩减为一个包含隐藏期望输出的可运行目录,然后把你能低成本获取的候选接入评分器。如果需要一个零成本的地方来启动这个循环, MonkeyCode 当前的免费模型访问和免费服务器选项是一个可能的补全和算力来源;裁定脚本不关心答案从何而来。
更大的习惯才是关键:模型选择是一个有刷新按钮的实证问题。 gates 建造一次,当新助手或新免费层级出现时重新跑,让你的编译器、sanitizer 和补丁边界去和发布帖争论。