先用 sanitizer(-Warray-bounds、UB sanitizer)配置构建,AI 生成的补丁过不了编译器警告即打回,比单元测试更早发现问题。
单元测试证明了你的 Agent 想要实现什么;编译器警告则证明了语言规范要求什么。这两件事很少具有同等分量——所以,AI 生成的 C++ 补丁的第一个审查者应该是工具链本身。在你阅读任何一个 diff 之前,先把编译器配置好,让它拒绝未定义行为、隐式转换和可疑的指针运算。然后把同样的错误反馈给 Agent,你就会得到一次高效得多的对话。
看这样一个补丁:它重写了一个循环,改用 std::span 并用有符号偏移量对其进行索引。单元测试通过了,因为测试向量的长度恰好足够长,而你的调试构建也看不出任何异常。但当你用 -O2 -Warray-bounds 编译时,优化器发现有索引可能是负数,于是发出了测试覆盖完全没捕获到的警告。这种差异正是"编译器栅栏"的核心论点:静态分析能够捕获你的示例永远触及不到的情况。
所谓栅栏,是指你故意用两到三种配置来构建代码,并在每种配置中都将警告视为错误。第一个配置使用严格的警告集。第二个加上地址和未定义行为消毒器。第三个可选配置启用链接时优化和激进的内联,以暴露只在优化时才出现的警告。这听起来可能有点过度,但对于 Agent 生成的代码来说,这是最便宜的保险。
创建一个 CMake preset,把你不可妥协的标志都烘焙进去。对于 GCC 和 Clang,一个实用的起始集如下:
add_compile_options(
-Wall -Wextra -Wpedantic -Wconversion -Wshadow
-Wformat=2 -Wnull-dereference -Wmisleading-indentation
-Werror
)
if(CMAKE_CXX_COMPILER_ID MATCHES "Clang")
add_compile_options(-Wdocumentation -Wcomma -Wrange-loop-analysis)
endif()
set(CMAKE_CXX_FLAGS_SANITIZE
"-fsanitize=address,undefined -fno-sanitize-recover=all")
-fno-sanitize-recover=all 部分至关重要。启用恢复时,消毒器会打印一个运行时错误然后继续运行,程序可能以零退出码结束,从而扰乱 CI。不启用恢复时,第一次违规会以非零状态中止,这给你的 Agent 一个明确的信号:这个改动按现在的写法是不可接受的。
你还应该锁定编译器版本。如果你的本地机器用 GCC 13,而你的 CI 用 GCC 12,那么新版中的警告永远不会在流水线中出现。用容器或版本管理器来让这个栅栏可复现。目标是打造一个稳定的 oracle,而不是争论工具链的差异。
创建一个脚本,比如 fence.sh,用它来构建带有消毒标志的版本,然后通过 ctest 执行测试套件:
#!/usr/bin/env bash
set -euo pipefail
cmake -S . -B build-fence -DCMAKE_BUILD_TYPE=RelWithDebInfo \
-DCMAKE_CXX_FLAGS="$CMAKE_CXX_FLAGS_SANITIZE"
cmake --build build-fence --parallel
ctest --test-dir build-fence --output-on-failure
在任何人工审查之前运行这个脚本。当你的 Agent 提了一个补丁,你只需要调用一次 fence.sh。如果构建因为警告失败了,输出就是修正消息。如果测试在 ASan 下崩溃,栈追踪会直接指向症状所在,你可以把那个追踪作为下一次尝试的 prompt 反馈给 Agent。这个循环是机械的,但远比告诉 Agent"哪里有问题,请检查一下你的改动"要可靠得多。
有一点要注意:消毒器能检测到测试未覆盖的代码路径上的错误。所以消毒后的测试通过的价值取决于你的测试套件有多好。配合一些属性风格的测试使用,你能得到一个行为保持的坚实下界。
并非所有诊断信息都值得同等对待。下表将常见的编译器消息映射到它们所指示的 Agent 行为,这样你就可以快速地整理输出。
用这个表作为你自己策略的起点。重点不是机械地修复每个警告,而是把它们分类为"必须立刻修"、"必须理解"和"对这个补丁可以安全忽略"。如果每次迭代你都把警告输出作为上下文给 Agent,那些持续遇到同一类警告的 Agent 最终会学会避免它。
每次 Agent 补丁都要跑两个构建和一个消毒后的测试套件,会在按分钟计费的 runner 上消耗时间。这时一个免费服务器选项就有帮助了:你可以在一个小机器上专门用来在补丁到达时精确执行栅栏,而不用按分钟付费。免费模型访问处理后续的修复尝试,这意味着整个审查周期的成本接近于零。循环变得很简单:你的 Agent 提议一个改动,免费服务器用栅栏编译它,然后消毒器输出作为下一次提议的上下文返回。

披露:本文是 MonkeyCode 产品推广的一部分。
即使有免费资源,你也应该给每次运行设一个限制。用 timeout 600 ./fence.sh 和内存上限比如 ulimit -v 2097152,这样一个行为异常的补丁不会把机器搞挂。免费不等于无保护;意味着基础设施是可用的,但不是无限的。像对待任何生产系统一样对待你的免费服务器,给每个作业定义一个预算。
编译器在检测未定义行为、类型不匹配和明显的内存问题方面做得非常出色。但它们对算法复杂度、API 一致性和设计意图完全视而不见。一个补丁可能通过了所有警告标志,但仍然在热点路径上引入了一个二次方复杂度的循环,因为没有任何编译器标志能告诉你渐近复杂度。对于结构和可读性,你仍然需要人类判断。
消毒器也有运行时成本,通常对测试套件来说是 1.5 倍到 4 倍。对于大型项目,这可能使栅栏在每次提交时都太慢而无法运行。如果这成了问题,可以调度栅栏只在一个过滤后的测试子集上运行——覆盖补丁所涉及的模块——然后每天只跑一次完整套件。开销是有界限且可预测的,当审查 Agent 输出时,这比绝对速度更重要。
最后,警告在不同编译器和不同时的标准库版本之间存在差异。在 GCC 上干净编译的补丁可能在 Clang 上触发 -Wstringop-overflow,反之亦然。选择一个工具链用于栅栏并记录它。如果你的生产环境使用不同的工具链,给那个编译器加一个单独的配置,但保持它作为次要信号。
如果你的补丁只改动了 README 文本或构建配置,完整的编译器栅栏就过度了。同样,一个已经在大量警告下编译的代码库,如果你在全局打开 -Werror,会让你的 Agent 被遗留噪音淹没。渐进式引入栅栏:从一个新模块开始,然后随着警告数量下降再扩展。你也可以排除生成的代码和第三方依赖,但永远不要排除你的 Agent 写的代码。目的是强制 Agent 面对与人类贡献者相同的标准。
在你的下一个 Agent 补丁之前,先设置一个消毒器构建。编译器是唯一永远不会疲倦、永远不会假设上下文、永远不会为错误找借口的审查者。给它一个席位,你的审查过程就会变短,因为机器已经问了第一轮问题。