用免费模型一次生成可编译的目录树哈希工具,但差分测试发现三个真实缺陷。生成是便宜的,验证才是交付物——需要对抗性参考oracle而非仅靠代码审查。
结论先行:免费模型一次生成了一份可工作的 C++17 目录哈希器。草稿能编译、能运行,但仍然是错的。用标准系统工具做差分测试,在工具真正触碰生产缓存之前就发现了三个真实 bug。生成是廉价的部分。验证才是交付物。
我需要一个目录树的确定性哈希。用例是一个小型构建流水线的缓存失效:如果任何文件内容、名称或符号链接目标发生变化,缓存键必须改变。如果什么都没变,键必须在不同机器和不同 checkout 之间保持一致。
手写这个工具大约需要 200 行 std::filesystem 代码。happy path 很简单。风险存在于排序、符号链接和元数据泄露到哈希中。
我把任务变成了一个实验。MonkeyCode 的免费模型访问和免费服务器选项意味着模型在远程服务器上运行,而我把验证留在了自己的笔记本上。披露:本文是 MonkeyCode 产品推广的一部分。
计划:让模型写第一版,然后用参考预言机来证明或证伪它。
目标不是"一份能编译的工具"。目标是一份在我能生成的所有输入上都与参考实现匹配的工具。我用三句话写了合同:
第一步:提示词。我给了模型合同、C++17 标准和唯一约束:单文件,无标准库以外的依赖。
第二步:草稿。模型在一次响应中返回一个 .cpp 文件。第一次编译就成功了。这正是大多数工作流停止的时刻。这一次没有。
第三步:参考预言机。我没有逐行审查代码,而是构建了一个测试工具,比较工具与 shell 管道:
find "$tree" -printf '%P\0' | sort -z | while IFS= read -r -d '' f; do
if [ -L "$tree/$f" ]; then
printf 'L:%s:%s\0' "$f" "$(readlink "$tree/$f")"
elif [ -f "$tree/$f" ]; then
printf 'F:%s:%s\0' "$f" "$(sha256sum "$tree/$f" | cut -d' ' -f1)"
fi
done | sha256sum
管道将树规范化为排序后的记录流,然后对流进行哈希。它很慢。但它也是明确的。这正是一个预言机应该有的样子。
第四步:测试夹具生成器。一个小脚本创建了随机树:嵌套目录、空文件、不同目录中的重复名称、指向树内外的符号链接,以及内容相同的文件。我生成了 1,000 棵树。
第一次差分运行在 1,000 棵树中有 214 个失败。失败聚类为三个根本原因。
Bug 1 是微妙的一个。工具在一台机器上是确定性的,但在其他所有机器上都是错的。修复很小:
std::vector<std::filesystem::path> paths;
for (auto it = std::filesystem::recursive_directory_iterator(root);
it != std::filesystem::recursive_directory_iterator(); ++it) {
paths.push_back(it->path());
}
std::sort(paths.begin(), paths.end());
三个修复之后,工具在所有 1,000 棵树上与参考实现匹配。我用更深嵌套和更长路径运行了第二批 500 棵树。零不匹配。
时间:模型的草稿用了不到一分钟。三个修复花了大约一个小时,包括测试工具。测试工具还捕获了我自己测试生成器中的两个 bug。预言机不在乎是谁写了错误的代码。
能编译能运行是最低标准。模型立即通过了,但违反了三倍的实际合同。
参考预言机将审查转化为测量。我没有通过阅读发现 bug。测试工具指向了确切的失败输入,diff 告诉我是合同的哪一部分被打破了。
将生成与验证分离使得这个门槛难以绕过。模型在远程运行;预言机在本地运行。没有办法从"模型说它可以工作"直接跳到"它被合并了"。
这些 bug 并不奇特。它们就是我写在合同里的三句话。模型没有足够仔细地阅读它们。我也不会在第一遍就做到。
不要盲目复制这个工作流程。参考管道忽略 ACL、扩展属性和硬链接。我的工具也忽略它们,这是设计决定的。如果这些对你很重要,先写预言机再写工具。
免费模型也有盲点:它从未问过"确定性"在不同机器上意味着什么。它假设本地一致性。这个假设本身就是整个问题。
重要的产物不是生成的代码。而是那个 30 行的测试工具,它说"错了"并指向输入。如果你用免费模型端点尝试这个,在阅读输出之前先构建预言机。免费层足够运行实验;门槛才是让它有用的东西。