循环工程实验发现反馈循环和验证器可能悄然失效
要点速览
- •该实验评估了两个循环工程组件:一个使用真实测试失败的反馈循环,以及一个将代码生成与最终判断分离的 maker/checker 设置。
- •第一次测试运行显示真实反馈没有带来收益,因为隐藏测试工具只返回裸断言错误,没有提供有用的失败细节。
- •在向测试工具加入失败输入、期望输出和实际结果后,真实反馈解决了一个通用重试循环无法解决的问题。
- •测试运行型验证器误接受了 8 个错误候选中的 3 个,在该测量比较中高于两个基于意见的检查器。
- •最终组合系统的改进主要来自反馈循环,而验证器产生了一次误接受,只有隐藏测试评分才将其捕获。

最后更新于 2026 年 7 月 27 日,作者为编辑团队。最初发表于 Towards AI。
“我的工作就是编写循环。”
这句话出自 Boris Cherny,他在 Anthropic 负责 Claude Code。Cherny 曾表示,他已经不再直接提示 Claude,而是把时间花在设计那些替他提示 Claude 的循环上 [1]。这句话以及几条类似评论,推动了今年一波关于循环工程的解释性文章 [1][2]。读完其中六篇后,我自己搭建了一个。
更准确地说,我搭建了几乎每篇解释文章都会描述、但很少有人真正端到端运行的两个组件。第一个是“运行直到完成”的循环:在失败后,它不是让模型再猜一次,而是把模型自身真实的测试失败反馈给它。第二个是 maker/checker 设置:编写代码的模型不能成为判断代码是否正确的最终裁判。
这种区分很重要,因为编码代理工作流通常依赖同样的分离:一个系统提出变更,另一个工具、测试套件或模型决定该变更是否可接受。如果反馈通道或验证器很弱,循环看起来可以实现自动化,却未必真正更安全。
我从零实现了这两部分,约 600 行 Python,将其接入 claude-opus-4-8,并用 MBPP+ [3] 进行评估。MBPP+ 是 EvalPlus 的小型 Python 编程任务基准,因此适合在不引入仓库级复杂性的情况下隔离观察循环行为。本文讨论的所有实验总成本不到两美元。第二个组件——常被视为更安全的一半,因为它“确实运行测试”,而不是相信模型自己的说法——在我的测量中给出了那些解释文章没有提醒过的结果。
简而言之:循环工程的两个核心部分连接起来很简单,也很容易在不知不觉中出错。我的“真实反馈”循环一开始看起来与随机重试没有区别,直到我发现了自己测试工具中的一个错误。我的“安全”测试运行型验证器,其误接受率高于一个仅仅询问模型自信程度的检查器。构建循环只是容易的 20%。
将循环接到空信号上
关于循环工程的许多文章止步于接线图。它们列出触发器、可验证目标、工具、状态和停止规则——五个方框,中间用箭头连接。其隐含信息是:只要这些方框连上了,循环就会工作。
这就像安装了烟雾报警器,然后因为它装在天花板上并接好了线,就宣布房子安全,却没有检查里面是否有可用电池。
我在自己的“真实反馈”循环中正好遇到了这种失败。它连接的是实际测试输出,而不是通用的重试提示。从理论上说,它应该明显优于只收到“那是错的,再试一次”这条消息的循环。但我的第一次运行显示并非如此。
给评分器评分的评分器
在动循环本身之前,我先构建了其他一切都依赖的组件:一个评分器,它在隔离的子进程中运行候选代码,设置硬性超时,并根据隐藏测试进行评分。
在它能给自己评分之前,我并不信任这个评分器。给它一个已知正确的解法,它必须通过。给它一个已知错误的解法,它必须失败并附上断言错误。给它一个无限循环,它必须被超时机制终止,而不是永远挂起。
随后,我用 75 个 MBPP+ 参考解法验证了整个流水线。75 个全部通过。只有在那之后,我才相信循环产生的任何数字。
看起来正确但其实并非如此的循环
循环本身非常简单。它生成一个解法,对其评分,并在失败时把真实 stderr 反馈回去——不是通用的“再试一次”,而是实际错误——最多尝试三次。
我还构建了一个对照组,因为我不想在没有基线的情况下相信一个标题数字。对照组使用完全相同的循环,但把真实错误替换为一条通用指令:“那是错的,写一个不同的解法。”如果真实反馈不能明显优于这一组,那么接线中就有东西坏了。
在第一次运行的 35 个问题中,三个分支的结果完全相同。这不是循环有效的证据,而是一个长得像循环的危险信号。
我没有盯着总体指标,而是检查失败案例,并找到了问题:MBPP+ 的隐藏测试工具以裸 AssertionError 失败。它没有包含失败输入、期望值或实际值。因此,“真实反馈”在信息量上与“再试一次”相同,因为模型没有收到任何可据以行动的内容。
我为测试工具加入了检测记录,使其报告失败输入、期望输出以及代码实际返回的值。在同样 35 个问题上的第二次运行中,真实反馈解决了一个通用重试组无法解决的问题,整个运行额外消耗约 2,500 个输入 token。
循环本身并没有坏。它连接到的信号是空的。只有对照组暴露了这一点;单看标题指标并不会发现。
验证器以理论未预料的方式失败
循环需要停止规则,而“模型说它完成了”并不是停止规则。因此,我构建了一个检查器,它根据规格说明编写自己的测试。它从不查看隐藏测试,也不查看它正在评判的解法代码。随后它运行这些测试。策略是只有全部通过才接受,默认拒绝。
我将该验证器与三个更弱的检查器进行了比较,样本是我的循环产生的 41 个候选解法:其中 33 个正确、8 个错误。测量指标是误接受率,即每个检查器让实际已损坏的代码通过的频率。
| 检查器 | 误接受 | 误拒绝 |
|---|---|---|
| 全部信任 | 8/8 — 100% | 0/33 — 0% |
| 询问模型是否有信心 | 2/8 — 25% | 4/33 — 12% |
| 第二个模型阅读代码 | 2/8 — 25% | 5/33 — 15% |
| 编写测试并运行 | 3/8 — 38% | 1/33 — 3% |
我原以为运行测试的检查器会在误接受方面直接胜出。但它没有。它放行错误代码的比例高于任一基于意见的检查器。
原因比数字本身更重要。全部八个错误候选都来自三个规格说明确实含糊的问题。检查器和修复器是同一个模型在阅读同一句含糊的描述。因此,检查器自己编写的测试编码了错误代码已经包含的同一种误读,使错误代码能够干净地通过这些测试。
基于意见的检查器在这场误接受比较中“获胜”,主要是因为它们总体上更谨慎。也正是这种谨慎,导致它们误拒绝正确代码的次数多出四到五倍。
运行测试并不能自动免除误接受。它是一种不同类型的证据:它包含具体输入、期望值和实际值,而不是一种笼统印象。正因如此,它 3% 的误拒绝率才可以作为真正的关口使用。一个拒绝 15% 正确工作的检查器,可能在阻止一次糟糕合并之前,就把工作流淹没在重试中。
将两个部分结合起来
最终组合会先运行反馈循环。如果失败,它会采样新的候选,并让验证器——而不是模型的自信程度——决定提交什么。如果没有任何候选越过门槛,它还包含一条明确的放弃路径。
我在 MBPP+ 的一个留出切片上评估了该设置;在构建上述任何组件时,我都没有使用过这部分数据。评分独立于验证器的任何决定,针对隐藏测试完成。
几乎所有工作都是循环完成的:它增加了 14.2 分,恢复了六个单次生成失败中的五个,并需要约 10 次额外 API 调用。验证器阶段只触发了一次,发生在循环无法解决的那个问题上。它采样到的第一个候选通过了自己编写的测试,但仍然未通过隐藏测试。
这是一次真实发生的误接受,与前表预测的失败模式一致。它之所以被抓住,只是因为运行器会用验证器从未见过的真实依据来给提交评分。如果验证器自己的判断就是最终结论,这个 bug 就会被发布。
该阶段的总成本为 45 次调用,约十三美分。
这种方法在哪里会失效
当目标确实可测试时,这种方法有效:例如带有隐藏测试用例的函数、要么验证通过要么不通过的 schema,或循环无法通过话术绕过的判定器。当规格说明本身含糊时,它无效,因为同模型检查器可能继承生成器的同一种误读。在这种情况下,修复办法未必是更聪明的检查器,而是更清晰的规格说明,或来自完全不同模型家族的独立判定器。
此外,我只在小型、独立的 MBPP+ 函数上测试了这一点。该实验没有处理带有跨文件依赖的大型代码库。我也没有构建或测试用于并行运行多个循环的 worktree 隔离。这是一个真实问题,但不是本实验测量的问题。
该循环也停在“已验证”处。它不会决定是自动应用变更,还是升级给人类处理。一旦系统触及任何具有真实写入权限的内容,这就会变成一个独立且更困难的问题。
从这里开始
从评分器开始。在编写任何循环逻辑之前,先写自测试——已知正确、已知错误和无限循环。这个五分钟脚本是防止虚构标题数字的主要保护。
对于哪怕只有少量单元测试的代码库,下一步连接反馈循环,并从第一天起就同时构建通用重试对照组。在看到它战胜“再试一次”之前,不要相信所谓改进。
然后构建第二个检查器,并将它与第一个进行比较。当误接受表与理论预期不一致时——这种情况可能会发生——其原因会说明为什么循环比其底层模型更重要。
参考资料
[1] Rohan Mistry, “Prompt Engineering Is Dead. Loop Engineering Is Here.,” Towards AI, July 2026.
[2] Mehmet Özel, “Loop Engineering for AI Agents : Building Verifiable, Self-Correcting Coding Workflows,” Towards AI, June 2026.
[3] EvalPlus, “MBPP+ Dataset,” Hugging Face Datasets.