文章提出用固定输入和手写参考实现,分别在 -O0、-O2 下验证 AI 生成的 C++ 补丁,捕捉有符号溢出等导致的结果差异。另用提示词哈希锁定测试版本,避免后续修改悄然改变评估条件。
这种故障很常见,却藏在一份全绿的日志里。评分器接受了一份 C++ patch,因为 debug 二进制打印出了 18432。随后,用同一份源码构建的 release 版本却打印出了 18416。两次运行之间,没有人改过 prompt。优化器遇到了一次有符号加法,而这次加法的结果已经超出了可表示范围。
这种差异就是一次静默回归。只在一组编译参数下捕获一个基准输出,发现不了它。
本文介绍一种用于 AI C++ patch 的优化差异门禁(opt-split gate):一份冻结的输入数据文件、一个人工编写的 oracle,以及分别以 -O0 和 -O2 构建的待测实现。当 oracle 保持稳定、待测实现却出现差异时,门禁判定失败。通过锁定 prompt 哈希,可以防止后续草稿在没有记录修改的情况下改变测试。
生成的 C++ 放进一个只打印数字的驱动程序里,往往看起来已经完成了。驱动程序和 patch 共用一个翻译单元。编译时没有警告,或者根本没启用警告。构建使用本地默认的优化配置,而这套配置往往更接近 -O0,而不是软件包实际发布时使用的参数。
有符号整数溢出、读取未初始化的标量,以及依赖求值顺序,都可能在这套环境下蒙混过关。换一组编译参数,有时会让运算保持在 oracle 要求的范围内,有时则会让问题暴露出来。opt-split gate 无法证明不存在 undefined behavior。它捕获的是一种检测成本更低的不一致:debug 和 release 输出不同,而经过检查的 oracle 两次都打印出完全相同的字节。
一次运行由四项输入定义,没有一项是仪表盘截图。prompt.txt 是生成草稿这一步唯一可以读取的文字说明。prompt.pin 保存该文件在上一次获准修改措辞后的 SHA-256。tape.txt 列出有符号整数,每行一个 token,以 END 结束。
oracle.cpp 由人编写。candidate.cpp 是待测 patch,必须导出与 oracle 相同的函数签名。契约的范围保持狭窄,让评分只针对数值,而不是文字表述。
// contract.hpp
#pragma once
#include <cstdint>
#include <optional>
std::optional<std::int64_t> fold_checked(const std::int64_t* data, std::size_t n);
返回整数表示精确的总和。std::nullopt 表示拒绝这次折叠求和。待测实现的评分依据是 oracle 的判定,而不是放在 prompt 里的某个基准字符串。
prompt 变了,测试就变了,即使输入数据的每个字节都没有变化。锁定检查会拒绝对发生漂移的测试进行评分。它不会自作聪明地忽略空白差异。
python3 - << 'PY'
import hashlib, pathlib, sys
prompt = pathlib.Path("prompt.txt").read_bytes()
digest = hashlib.sha256(prompt).hexdigest()
pin = pathlib.Path("prompt.pin")
if not pin.exists():
pin.write_text(digest + "\n", encoding="ascii")
print("pinned", digest)
sys.exit(0)
expected = pin.read_text(encoding="ascii").strip()
if digest != expected:
print("prompt pin mismatch")
print("expected", expected)
print("actual ", digest)
sys.exit(2)
print("prompt pin ok", digest)
PY
出现不匹配时,应视为测试框架失败,而不是 patch 失败。只有在提交中引用了新措辞,才能更新 prompt.pin。夜间任务不得为了让结果变绿而重写锁定值。
驱动程序读取 tape.txt,调用 fold_checked,然后打印 REJECT 或一个十进制整数。它不包含任何预期答案。如果模型能看到驱动程序里的基准输出,往往会直接复述它,而这样的复述在任何优化级别下都能匹配。
// tape_driver.cpp
#include "contract.hpp"
#include <fstream>
#include <iostream>
#include <string>
#include <vector>
int main() {
std::ifstream in("tape.txt");
std::vector<std::int64_t> data;
std::string tok;
while (in >> tok) {
if (tok == "END") {
break;
}
data.push_back(std::stoll(tok));
}
std::optional<std::int64_t> got = fold_checked(data.data(), data.size());
if (!got) {
std::cout << "REJECT\n";
return 0;
}
std::cout << *got << "\n";
return 0;
}
先准备三份输入数据,分别保存在独立文件中。一份的总和可以用 64 位整数表示;一份的总和超过有符号 64 位整数的上限;还有一份只包含 END,表示空区间。
在本地编译器生成四个二进制文件之前,下面的示例都只是尚未执行的测试夹具。这种方法不依赖托管运行环境。
10
20
30
END
9223372036854775807
1
END
保持语言标准、警告选项和 include 路径不变,只修改优化参数。在生成第一个目标文件之前记录编译器身份,让后续的绿色结果能够对应到具体工具链。
mkdir -p out
g++ --version | head -n 1 | tee out/compiler.txt
CXXFLAGS="-std=c++20 -Wall -Wextra -Werror -I."
g++ $CXXFLAGS -O0 -o oracle_o0 oracle.cpp tape_driver.cpp
g++ $CXXFLAGS -O2 -o oracle_o2 oracle.cpp tape_driver.cpp
g++ $CXXFLAGS -O0 -o cand_o0 candidate.cpp tape_driver.cpp
g++ $CXXFLAGS -O2 -o cand_o2 candidate.cpp tape_driver.cpp
如果待测实现编译失败,就给出编译失败的评分结果,并停止矩阵评估。不要比较不完整的二进制产物。如果 oracle 编译失败,说明门禁配置有误,这个结果不属于 patch 缺陷。
oracle 本身应该简单、平实。下面的实现是为测试夹具提出的方案,并不声称它由模型生成,也不代表已经测得的结果。
#include "contract.hpp"
#include <cstdint>
#include <limits>
#include <optional>
std::optional<std::int64_t> fold_checked(const std::int64_t* data, std::size_t n) {
__int128 acc = 0;
for (std::size_t i = 0; i < n; ++i) {
acc += data[i];
if (acc > std::numeric_limits<std::int64_t>::max()) {
return std::nullopt;
}
if (acc < std::numeric_limits<std::int64_t>::min()) {
return std::nullopt;
}
}
return static_cast<std::int64_t>(acc);
}
在只包含所选 tape.txt 的目录中运行每个二进制文件。捕获 stdout 的原始字节,不要去除首尾空白,也不要让文本经过会改写换行的 shell。
./oracle_o0 > out/oracle_o0.txt
./oracle_o2 > out/oracle_o2.txt
./cand_o0 > out/cand_o0.txt
./cand_o2 > out/cand_o2.txt
cmp -s out/oracle_o0.txt out/oracle_o2.txt
使用 cmp 比较文件。少一个换行也算不匹配。如果用允许空格差异的数字解析器,就会掩盖这个门禁要捕获的漂移。
第三行就是静默回归:patch 在 debug 构建中满足契约,却在 -O2 下破坏了契约,或者改变了行为。把四份 stdout 文件与 prompt 哈希保存在一起。后续任务即使只有 cand_o2 的结果发生变化,也属于回归,哪怕 debug 输出仍然匹配之前的示例。
对每份输入数据重复执行矩阵评估。只有所有输入数据都通过,patch 才能通过门禁。只要出现一次优化差异失败,整次运行就失败。不要把各行结果取平均,变成宽松的评分。平均值会把只在 release 中出现的故障,埋在三个看起来没问题的 debug 匹配结果之下。
披露:本文是 MonkeyCode 产品推广活动的一部分。
这个矩阵不需要托管模型。本地编辑器可以把 candidate.cpp 放进目录树,四条 g++ 命令仍然适用。只有当团队需要草稿生成步骤或远程构建平台时,MonkeyCode 才会参与。它是一个开源项目。运营方的资料提到免费模型访问和免费服务器选项。这两项都属于可用性声明,因此应在运行当天查看项目的最新文档,确认它们是否仍然可用。
让草稿生成过程保持盲测。它可以读取 prompt.txt 和 contract.hpp,但不能读取 oracle.cpp、输入数据文件或之前的 stdout。把 oracle 展示给模型,会让评分变成一次抄写练习;抄写会在两个优化级别下都通过,却失去了评估的意义。
不要把 token 配额、机器规格或租用时长写死在 prompt.pin 或评分器中。这些数字会变化,过时的上限可能比发布它的套餐存续得更久。如果账户当天无法启动构建,就让任务以失败结束,并保留原有的锁定值。
免费服务器在确实可用时,适合执行这四组编译和运行命令。通过或失败的判定规则,应该保留在评审者能用笔记本查看 diff 的脚本里。清晰的分工是:本地检查锁定值,远程编译,本地评分。平台返回 stdout 文件和编译器版本行,再由评审者持有的矩阵脚本给出评分。
已经维护 prompt.pin 的团队,可以在阅读当天项目文档中的限制后,把冻结的 prompt 发送给当前可用的免费模型,并在免费服务器上运行这个 opt-split 矩阵。即使平台离线,评分脚本也应该无需修改就能继续运行。
-O0 和 -O2 下结果一致,只能作为筛查,不能作为证明。undefined behavior 可能在两组参数下都存在,也可能在两组参数下都不表现出来。Sanitizer 构建应放在独立门禁中。这个矩阵也不考虑吞吐量。patch 可能通过所有输入数据,却仍然因成本过高而无法发布,因此应使用另一套测试夹具评估成本。
如果规范允许在 fast-math 参数下出现差异,或者契约本身是近似的、真正的 oracle 是容差,就跳过这种方法。如果 oracle 和待测实现本来就应该拒绝不同的边界情况,也应跳过。这种不匹配属于规范争议,而不是回归。
对于无法作为一个额外翻译单元构建的 patch,例如 plugin ABI 或生成的 lexer,也应跳过。示例驱动程序使用 std::stoll,遇到损坏的输入数据时会抛出异常。这属于测试框架错误,运行应在写入任何 patch 评分之前失败。
__int128 累加器是 GCC 和 Clang 的扩展,不是可移植的 C++。如果工具链不支持它,就把 oracle 替换成用标准 C++ 编写的带检查加法实现。将这一选择与编译器版本行一起锁定记录。否则,两台机器会对 oracle 得出不同结果,矩阵却会把问题归咎于 patch。
每次运行记录都需要包含 prompt 哈希、编译器版本字符串、四条编译参数命令,以及 stdout 哈希。缺少这些字段,却只有一条绿色的 debug 输出,就会让单个总和蒙混过评审。release 构建从未接受评分,因此也没人有文件可以打开检查。
opt-split gate 让这份文件成为必需品:相同的输入数据、两个优化级别,以及一个不随 prompt 一起提供的人工 oracle。当 release 的总和发生变化,而 oracle 没有变化时,patch 就不能发布。
如果要采取进一步行动,可以考虑屏蔽此人和/或举报滥用行为。