Bot自动提取GCC/Clang错误、分类六大类型、生成修复补丁并通过重新编译验证,确保修复有效。
编译器日志是证据,不是诊断。大多数 CI 失败都是无聊的:少了一个 include,模板推导在 GCC 上能过但在 Clang 上不行。无聊归无聊,仍然需要一个人去打开日志、滚动页面、查找分类。这个案例研究介绍了一个小型归类机器人,自动化了这一步——它提取第一个错误,对其分类,提出修复方案,并且在真正的重新编译确认之前不信任任何提案。整个流水线运行在免费基础设施上:一个用于分类的免费模型端点,和一个用于 webhook 的免费服务器。
促成这个项目的,是一个在 CI 中针对 GCC、Clang 和 MSVC 编译的单头文件 C++ 库。失败是重复的,但归类工作不是。
我想要一个接收原始编译器日志、返回一个 JSON 对象的机器人:分类、可信度、候选修复方案,以及一个 gate 裁决。gate 裁决才是关键。模型可以提出任何东西,但编译器说了算。
这个机器人有五个需求:
从 GCC/Clang 日志中提取第一个错误。
将其归入六个类别之一:missing_include、template_deduction、type_mismatch、linker_undefined、abi_mismatch、other。
生成一个补丁形式的候选修复方案。
使用相同标志对文件副本重新编译来验证修复。
只缓存已验证的结果,以规范化错误签名作为 key。
需求 5 是使免费基础设施可行的关键。模型调用是稀缺资源。缓存是从「每次失败一次调用」到「每次唯一失败一次调用」的分水岭。
GCC 或 Clang 日志中第一个错误的形状是稳定的:
src/parser.cpp:42:9: error: no matching function for call to 'max(int, double)'
一个小的 C++17 提取器来处理其余部分:
struct ErrorSig {
std::string file;
int line = 0;
int col = 0;
std::string message;
};
ErrorSig first_error(const std::string& log) {
static const std::regex re(
R"(^([^:]+):(\d+):(\d+):\s+error:\s+(.*)$)",
std::regex::multiline);
std::smatch m;
if (std::regex_search(log, m, re)) {
return {m[1].str(), std::stoi(m[2]), std::stoi(m[3]), m[4].str()};
}
return {};
}
std::string cache_key(const ErrorSig& e) {
// Paths and line numbers change between runs; the message usually does not.
return e.message;
}
缓存 key 有意丢弃了文件路径和行号。parser.cpp 中缺失的 include 和 lexer.cpp 中缺失的 include 是同一种病。
机器人在触及网络之前先检查缓存:
if (auto hit = verified_cache.find(key); hit != verified_cache.end()) {
emit_report(hit->second, /*cache_hit=*/true);
return 0;
}
只有缓存未命中才会到达分类器。这不是优化,而是使免费层级调用配额在构建损坏的上午反复运行二十次时仍能存活的設計决策。
模型 key 不能放在 C++ 客户端中。Webhook 运行在免费服务器上,持有 key,并暴露一个端点。在这个设置中,免费模型访问和免费服务器都来自 MonkeyCode。
披露:本文是作为 MonkeyCode 产品推广的一部分准备的。
服务器故意做得很薄:
# server.py — runs on the free server, holds the model key
from flask import Flask, request, jsonify
import os, urllib.request
app = Flask(__name__)
MODEL_ENDPOINT = os.environ["MODEL_ENDPOINT"] # from your project settings
MODEL_KEY = os.environ["MODEL_KEY"]
@app.post("/classify")
def classify():
payload = request.get_json()
body = jsonify({
"error": payload["error"],
"categories": ["missing_include", "template_deduction",
"type_mismatch", "linker_undefined",
"abi_mismatch", "other"],
}).get_data()
req = urllib.request.Request(MODEL_ENDPOINT, data=body, method="POST")
req.add_header("Authorization", f"Bearer {MODEL_KEY}")
with urllib.request.urlopen(req) as resp:
return resp.read()
契约是进一个 JSON 对象出一个 JSON 对象:
{
"error": "no matching function for call to 'max(int, double)'",
"category": "template_deduction",
"confidence": 0.6,
"fix": "static_cast<double>(1)"
}
类别列表随 prompt 一起发送,以便将模型约束到分类体系中。不受约束的模型会为每次失败发明一个新类别。
这是将假设与结果区分开的步骤。机器人将提案的修复应用到文件副本并使用相同标志重新编译:
bool gate(const std::string& source, const std::string& patch,
const std::string& flags, const ErrorSig& before) {
apply_patch("triage_copy.cpp", source, patch); // writes patched source
int rc = std::system(("g++ " + flags +
" triage_copy.cpp 2> gate.log").c_str());
if (rc == 0) return true; // promoted
ErrorSig after = first_error(read_file("gate.log"));
return after.message != before.message; // moved the failure: partial
}
退出码本身是不够的。删除有问题行产生的修复会产生非零退出码但带有不同的错误。gate 将其记录为部分结果,而非通过。产生相同错误签名的修复则被直接拒绝。
gate 不是断路器。模型允许出错;gate 是吸收这种错误的东西。
每次失败产生一行 JSON:
{"file":"src/parser.cpp","category":"template_deduction",
"gate":"promoted","cache":"miss","elapsed_ms":2140}
被提升的结果进入已验证缓存。被拒绝或部分的结果进入带有 TTL 的独立拒绝存储区。已验证的假设和已拒绝的假设从不共享命名空间。
这个测试工具只有在可以重运行时才有价值。十个代码片段,每个注入一个错误,覆盖六个类别:
for i in tests/snippet_*.cpp; do
g++ -std=c++17 -fsyntax-only "$i" 2> "logs/$(basename "$i").log"
./triage_bot < "logs/$(basename "$i").log"
done
将输出记录在如下表格中:
我故意不发布单次运行的准确率数字。十个样本是噪声,你的编译器版本和 prompt 措辞会改变结果。这个工具的意义在于你可以在不到一小时内产生自己的数字。
三个失败模式塑造了最终设计,每一个都在代码中留下了痕迹。
全日志漂移。发送整个日志会使模型修复最后一个错误而非第一个。截断到第一个错误加五行上下文使行为稳定下来。
删除并希望的修复。模型偶尔会通过删除使用缺失类型的行来「修复」缺失的 include。gate 捕获了它,因为错误签名保持不变。
缓存污染。缓存未验证的假设会使机器人自信地重复错误答案。已验证缓存和拒绝存储区的存在正是为了防止这种情况。
分类器是假设生成器,不是诊断引擎。编译器是唯一的先知,而 gate 是使机器人保持诚实的唯一东西。
免费层级基础设施是尽力而为的。如果你的团队需要延迟 SLA,这个模式不适合你。测试工具还假设构建是可复现的;不稳定的或依赖网络的构建会使 gate 变得毫无意义。
MSVC 日志有不同的形状。正则表达式处理 GCC 和 Clang;cl.exe 需要第二个模式。
已经有构建失败仪表板和维护良好的错误分类体系的团队。
构建不可复现的项目。
需要保证响应时间的任何人。
模型提议,编译器裁决——其他一切——缓存、分类体系、JSON 契约——都服务于这一句话。
缓存设计比 prompt 设计更重要。当稀缺资源是免费层级调用时,第一个问题不是「如何更好地 prompt」,而是「如何减少调用」。
免费服务器足以处理低流量 webhook。瓶颈始终是模型调用,而不是服务器。
上面的测试工具是一个完整的起点。如果你构建一个,请保留 gate——这是唯一无法被愚弄的部分。