循环工程实验,使用独立验证器
范围: 这不是一个比较代理框架的基准测试,也没有声称这个控制器“更聪明”。它声称的东西更窄,也更站得住脚:故障隔离是一个真实的、可衡量的架构属性。
这是一种模式,而不是产品: 循环工程是一种控制流模式,而不是特定的框架。它的概念是用一个迭代系统来替代单个巨大的提示,该系统观察状态并采取行动以实现目标。
为验证而构建: 为了证明这个机制而不是盲目相信,我构建了一个微小的、确定性的目标导向控制器实现。它需要零次LLM调用,并且零外部依赖。
可衡量的故障隔离: 当线性的一次性管道在其第一个未解决的障碍上完全停止时,这个控制器将故障隔离到包含它们的特定分支。在300个随机种子上,控制器平均完成了10.3个独立分支中的3.3个,而线性基线仅为0.4个。
彻底的透明度: 我实际上在信任这些数字之前发现并修复了自己基准逻辑中的一个真实错误,并且我正在详细说明那个错误,而不是隐藏它。
1、无缘无故停止的管道
几个月前,我看到一个任务处理管道在第40步的第3步崩溃了。
第3步需要一个我还没有设置的配置值。由于这个单一的缺失值,下游的所有内容都死了。这包括三十多个与缺失配置完全无关的步骤。
工作本身并非不可能。系统崩溃只是因为管道没有概念上的能力来跳过一个损坏的分支并继续处理其他分支。
这不是提示问题。为模型重写指令无法修复它,因为模型从来不是出错的部分。故障发生在控制代码中。负责在事情出错时决定下一步做什么的逻辑没有做出任何决定。它只是退出了。
我写这篇文章是为了解决完全高于提示的层面。这是代理系统的一部分,管理状态并确定在步骤成功、失败或卡住后的下一步行动。在AI工程圈中,人们称之为循环工程。
在你继续阅读之前,我想完全精确地说明我做了什么和没有做什么。精确是这篇文章的重点。
- 我没有构建新的代理框架。
- 我没有对LLM进行基准测试。
相反,我构建了一个小型的、确定性的、经过彻底测试的软件来证明一个架构声明。我将向你展示代码,揭露我在信任数据之前必须在我的基准逻辑中捕获的错误,并列出确切的数字。
我不期望你盲目相信这个机制。我希望你检查我的工作。
完整代码: https://github.com/Emmimal/loop-engine/
2、“循环工程”的含义以及术语的来源
如果你花时间在AI工程的Twitter、Substack或LinkedIn上,你可能见过循环工程这个短语。我想精确说明它的起源,而不是将其视为环境知识。错误归属一个快速发展的术语正是你侵蚀技术文章信任的方式。
谷歌工程师Addy Osmani在2026年6月发表的一篇文章中命名并构建了这个术语。他基于开发者Peter Steinberger的一句话,Peter认为从业者应该停止直接提示编码代理,而是设计提示它们的循环。大约在同一时间,Anthropic的Claude Code负责人Boris Cherny提出了类似的观点,他自己的工作流程从编写提示转变为编写编写提示的循环。Osmani的文章将循环工程定义为位于他单独称为工具工程之上的一个层次,工具工程涉及单个代理运行的环境。其他从业者的文章将其扩展为更完整的堆栈,从提示工程到上下文工程到工具工程到循环工程,每一层都包装了前一层。
我们还应该认可一个直接的技术前身。工程师Geoffrey Huntley在2025年7月描述了在简单的while循环中运行编码代理,这种方法被称为Ralph技术。核心思想,反复向代理提供相同的目标直到它满足书面规范,是完全相同模式的更简单祖先。
Osmani的文章描述了自运行循环的五个构建块,自动化、工作树、技能、插件和连接器,以及子代理,加上外部状态作为第六个,其他解释者更深入地介绍了这种架构。
我在本文中所做的要窄得多。循环工程是总体模式。接下来的内容是该模式的一个小部分的确定性实现,构建它是为了让你可以直接检查机制,而不是盲目相信。
3、为什么我在不调用单个LLM的情况下构建这个
我读到的每一篇关于循环工程的解释都假设LLM位于循环内部,在每一轮做出决策。对于像Claude Code这样的产品来说,这是一个合理的假设,它们确实将模型放在决策循环的中心。
但如果你想从自己的角度评估架构,这就会产生问题。如果循环的行为完全取决于模型决定做什么,你就永远无法完全将良好的循环设计与仅仅在特定运行中幸运的模型分开。当你需要判断时,非确定性是正确的工具。当你想要验证关于控制流的声明时,它是错误的工具。
所以我构建了完全相反的东西。我编写了一个目标导向的控制循环,其中简单的Python规则处理每个决策,而不是模型调用。它不需要API密钥,不花费令牌,并且在使用相同种子时在运行之间引入零方差。
如果这种架构相对于线性的一次性管道提供了真正的优势,那么即使if语句代表每一步的智能,这种优势也应该显现出来。如果它没有在这里显现出来,用LLM装饰相同的控制流也不会使底层的声明更真实。它只会使声明更难检查。
在本文的剩余部分,请记住这个特定的区别:
- 循环工程 是总体设计哲学。你用一个观察状态、采取行动并迭代以实现目标的系统来替代单个大型提示。
- 我下面描述的目标导向控制器 是该哲学的一个具体、确定性的实现。我构建它是为了测试关于故障隔离的一个特定声明。
将这两个概念混为一谈会将关于这个主题的文章变成无法证伪的思想文章。我宁愿给你可以实际运行的代码。
4、我没有比较什么,为什么这是诚实的范围
在我分解架构之前,我想先说明这篇文章没有进行的比较。任何使用过LangGraph、CrewAI或AutoGen的人都会合理地想知道我的代码与这些框架相比如何。
让我明确一点。我没有将一个代理框架与另一个进行比较。我没有将GPT-5与Claude进行比较,我也没有评估推理质量,因为我的代码中没有任何东西真正进行推理。相反,我正在隔离一个单一的架构属性。我想看看当工作流的一部分遇到它无法立即解决的障碍时,其余部分会发生什么。
线性执行器代表大多数一次性管道的默认形状,当遇到错误时它只是停止。目标导向控制器的行为不同。即使一个完全确定性的控制器内部没有模型,它也会重新路由图中它目前无法前进的部分,并继续处理它能处理的部分。
这是一个狭窄的、可检查的声明,也是我在这篇文章中提出的唯一声明。
5、架构:一个状态机,而不是序列
我构建的系统由两部分组成。首先,它有一个环境,只是一个具有依赖关系的任务图。其次,它有一个控制器,是在每次迭代期间决定每个任务做什么的逻辑。我有意让环境变得无趣。它是一个简单的有向无环图,仅此而已。控制器是本文的真正主题。
我为这个设置设计了一个特定的状态机。每个任务在每次迭代中都经历这个周期,直到整个图达到稳定的最终状态:
每个任务在每次迭代中独立地落入其当前状态所需的任何分支。这个过程持续进行,直到整个图达到终端状态,其中每个任务要么完成,要么永久失败,要么可证明死锁。
这个设置不需要固定的步骤序列。这是我的实现与线性管道之间的实际机械差异。值得花一点时间理解这个概念,因为它是我在本文后面展示的基准结果的整个来源。
一个特定的设计细节比它最初看起来重要得多。当任务需要它还没有的资源时,控制器必须区分两种情况。它需要知道资源是否仍在解析中,需要在下次迭代中重新检查,或者该资源无论你问多少次都不会解析。
我稍后会回到这一点。弄错这个区别正是差点让我失去有效基准的错误。
6、真正的推理引擎插入的位置
要执行任务,控制器调用 task.action(task, context)。在这个实现中,我将该调用编写为一个普通的、确定性的Python函数。
那个特定的调用是接缝。如果你将该函数替换为具有完全相同签名的LLM调用,你可以传递任务及其上下文,返回成功或失败状态,并让控制器中的每一行其他代码保持完全不变。
观察、检索、询问、执行、修订或阻止的控制流循环不知道也不关心是硬编码规则还是前沿模型做出最终决策。控制流和推理引擎之间的这种清晰分离正是我试图提出的架构论点。架构本身完全与模型无关。
7、构建值得测试的环境
只有当你给控制循环真正的障碍去导航时,它才有趣。为了实现这一点,我生成了合成任务图并为其设置了严格可重复性的种子。我将这些图构建为独立分支,最终馈送到少量集成任务中。这种设置模拟了现实世界的生产管道,其中几个不相关的工作最终必须组合在一起。
我随机为每个任务分配了六种不同行为之一。我使用大致相等的几率进行这些分配,但我将分布严重加权到干净的情况。
设计上,大约四分之一的任务是永久的死胡同。我是故意这样做的。我不想为了让我的控制器看起来更好而将牌堆向容易的问题倾斜。
如果有的话,这个故障率是非常激进的。真正的生产管道可能没有四分之一的步骤被永久阻止。我稍后会回到这一点,因为这对于正确解释最终结果至关重要。
我比较的基线
我的比较点是标准的线性执行器。它以有效的依赖顺序遍历任务,每个任务只尝试一次,并在第一次遇到无法立即解决的障碍时完全停止。它不允许重试、绕过阻止分支的重新路由,或者对仍然可以继续的独立工作给予部分信用。
我想明确一点细节。线性执行器确实以有效的拓扑顺序运行任务,而不是原始的插入顺序。这个选择并不意味着基线很聪明。它代表了此比较有意义的绝对最低要求。
没有拓扑排序,线性执行器几乎会在任何图的第一个任务上失败,仅仅因为它的依赖项恰好在文件中列出得更晚。那将测试列表排序运气而不是线性执行。超过该基线底线,执行器不会得到任何额外的东西。这是完全缺乏控制流的代码的诚实、未防御的默认行为。
8、差点使整个基准无效的错误
我想详细说明这个错误。捕获这个特定的错误正是你可以信任的基准与一个悄悄对你撒谎的基准之间的区别。
我对资源查找的第一个实现返回一个简单的布尔值。如果资源可用,它返回 True,如果不可用,返回 False。问题在于 False 做了两个完全不同的工作。它意味着尚未可用,但在下次迭代中再次检查,对于一个在几次轮询后会解析的资源。它也意味着永远不会存在,不要费心再次检查,对于一个永久缺失的资源。控制器无法仅从返回值中区分这两种情况。
这个缺陷有一个主要后果。一个等待缓慢但最终可用资源的任务将在 需要资源 状态下一两轮没有任何可见的状态变化。我原来的终止逻辑会过早触发。该逻辑在一次完整的传递中如果没有状态变化就声明整个图死锁。结果,它错误地将一个仍在合法进展的任务标记为永久卡住。
我发现了这一点,因为我的测试套件包含了一个针对这种场景的用例,测试失败了。为了修复它,我将资源和决策查找改为返回三状态信号而不是二进制布尔值:RESOLVED、PENDING 或 MISSING。
只有 MISSING 状态证明永久阻止。PENDING 状态意味着继续等待。有了这个更改,循环的迭代预算,而不是启发式猜测,最终决定何时放弃。
然后我编写了一个回归测试和一个专门的健全性检查,专门确认这类错误不会再次发生。我在正常的迭代预算下运行每个基准配置,在预算大四十倍的情况下再次运行,并确认标记为永久死锁的任务集在两次中保持不变。如果更大的预算改变了结果,你仍然在猜测某些东西而不是证明它。在九种不同的配置中,结果从未改变。
我包含这一点是因为这是我能向你展示的关于这个基准如何赢得信任权的最诚实的事情。在你需要三个状态的地方发布一个布尔值是一个容易、安静的错误。它产生的数字看起来完全合理,直到你根据基本事实检查它们。
9、在信任任何数字之前对基准进行健全性检查
我读到的大多数基准文章,包括我自己的一些早期文章,都止步于“这里是数字”这个短语。我希望这个特定的基准能经受住怀疑的阅读。在锁定任何数字之前,我针对基准自身的假设运行了五个特定的检查。
1. 注入的故障模式是否与预期的设计率匹配? 我在200个种子和4,800个任务模式分配中汇总了数据。六种注入行为中的每一种都在其预期频率的半个百分点内。两种永久阻止模式合计正好是25.2%的分配,这完美验证了我25.0%的预期目标。
2. 当任务死锁时,我能追溯每个下游故障到真实的注入阻止器吗? 我查看了一个代表性运行,其中我直接注入了11个任务作为永久阻止器。我看到9个额外的任务在它们的下游死锁。我成功地将这9个中的每一个都通过依赖图追溯到原始11个中的一个。我发现了零个无法解释的死锁。
3. 检索、询问和修订事件是否按预期比例发生? 我实际上发现了一个第二个、较小的问题,值得披露。我这个检查的第一个版本期望注入的慢速资源任务数量与我观察到的成功检索数量完全相等。测试失败了。解释结果是完全正确且不起眼的。少数慢速资源、可回答决策和不稳定任务位于另一个首先死锁的任务的下游。控制器根本没有到达它们。我验证了级联阻止完全解释了每一个短缺,而不是控制器错过了它应该完成的任务。之后,检查干净地通过了。
4. 给控制器更大的迭代预算是否会改变它标记为死锁的任务? 不会。我在50次迭代与2,000次迭代下测试了九种不同的配置。这作为我之前描述的布尔与三态错误的直接回归测试。
5. 结果是否稳定,还是我只是得到了一个幸运的样本? 我鼓励你最重视这个检查,因为大多数基准完全跳过它。我在300个随机种子上进行了测试。
目标导向控制器完成率: 平均值 = 47.7 标准差 = 11.8 最小值 = 11.6 最大值 = 74.4
线性基线完成率: 平均值 = 2.1 标准差 = 2.7 最小值 = 0.0 最大值 = 14.0
方差是真实的,你应该花一点时间思考。控制器的绝对最差种子完成了11.6%的任务。这个数字并没有击败线性基线的绝对最佳种子14.0%。
10、结果,以真正重要的指标为主
我想纠正一下我差点如何框定这个基准。我的第一本能是引导原始任务完成率。在九个配置的样本中,控制器达到了46.7%的完成率,而线性基线为8.3%。在完整的300次种子运行中,这些数字变为47.7%对2.1%。
这些是真实的数字,但引导它们恰恰邀请了我试图避免的那种夸大解释。它诱使你说类似控制器好20倍的话,而这并不是这个机制实际证明的。
真正隔离架构声明的指标是完全完成的独立分支数量:
完全完成的分支(9个种子配置的平均值)
目标导向控制器 ################ 3.3 / 10.3
线性基线 ## 0.4 / 10.3
控制器并不是因为它解决了不可能的任务而更好。它更好是因为它拒绝让一个不可能的任务阻止每个其他可能的任务。这就是整个贡献,尽可能精确地表述。我宁愿你记住这句话,而不是任何单一的百分比。
以下是迭代的情况,使用一个具有10个分支、每个分支运行4个任务的代表性配置:
| 迭代 | 完成 | 阻止 | 等待 | 检索 | 询问 | 修订 |
|---|---|---|---|---|---|---|
| 1 | 6 | 12 | 25 | 0 | 3 | 2 |
| 2 | 12 | 15 | 16 | 2 | 1 | 1 |
| 3 | 19 | 17 | 7 | 0 | 0 | 1 |
| 4 | 20 | 17 | 6 | 2 | 0 | 0 |
| 5 | 22 | 20 | 1 | 1 | 0 | 0 |
| 6 | 23 | 20 | 0 | 0 | 0 | 0 |
观察 等待 列从25排空到0,而 完成 从6攀升到23。注意 检索 和 询问 在控制器解决资源或决策的精确迭代中出现峰值,这些资源或决策一直阻止着一个分支。
线性执行器总共只产生一行。在我测试的九个配置中,它在五个配置中在第一个分支的第二个或第三个任务上停止,在另外四个配置中在第二个分支的第一个任务上停止,在任何单次运行中都从未达到大多数分支,无论它们是否本可以干净地成功。
我想精确说明最后一点,因为它解释了数字中的大部分差距。这个差距的大小部分是我的场景设计的一个属性,它强制执行一个硬停止和零进一步尝试。这不是声称控制器普遍更有能力。
控制器明显做的是将灾难性的、全有或全无的故障转换为部分的、隔离的故障。这代表了一个真实的、有用的工程属性。这绝对不是与称系统更聪明相同的声明。
11、这个基准没有显示什么
为了避免过度推销这些结果,让我明确一点。控制器没有完成100%的任务,它也不应该完成。永久缺失的资源和真正无法回答的决策在两个系统中都未解决。没有控制流模式,无论是确定性的还是模型驱动的,都无法检索不存在的资源或回答没有可用答案的问题。在典型的运行中,大约一半的任务因为这个原因而未解决。
这也不是声称确定性控制循环对于真实的代理系统是足够的。大多数生产代理循环需要模型或人类在某个步骤做出判断。这正是为什么我的架构将该步骤隔离为一个单一的、可交换的函数调用,而不是将特定的推理策略烘焙到控制流本身中。
最后,这不是整个循环工程的基准。该术语涵盖了调度、持久内存、验证器-生成器分离以及这个小系统根本没有触及的其他几个关注点。我在这里演示的是一个单一的属性,我故意隔离它,以便你可以检查它。
12、如何判断你自己的管道是否需要这种模式
如果你正在构建代理管道并想知道这是否适用于你,这是我使用的诚实清单,基于构建这个系统教给我的东西。
如果你匹配这些迹象,你可能有一个线性执行器问题:
- 管道中任何地方的一个失败步骤都会导致其后的每个步骤失败,包括与失败步骤零依赖的步骤。
- 你的重试逻辑是一种重新尝试整个管道的方法,而不是一个识别哪个特定步骤需要另一次尝试的系统。
- 在部分失败后,你无法回答这个工作的哪些独立部分实际完成的问题,因为你的日志只告诉你执行停止的位置。
- 向管道添加一个新的独立工作分支意味着你必须修改每个现有分支的故障处理,而不是新分支直接插入相同的控制循环。
如果你的情况看起来像这样,你可以安全地跳过目标导向控制器:
- 你的管道完全是顺序的。 如果每个步骤都真正依赖于它前面的步骤,你就没有独立分支。当没有东西需要隔离时,故障隔离是无用的。
- 工程开销太高。 如果从头重新运行一个短管道很便宜,就不要构建和维护一个复杂的状态机。如果你有一个五步骤的工作在不到一秒内完成,坚持使用具有简单顶级重试的线性执行器。它只是更可维护。
- 你真正的瓶颈是判断质量。 这种模式不会修复特定步骤的准确性差或决策错误。它只管理步骤失败之后发生的事情。那是推理问题,而不是控制流问题。没有多少聪明的循环设计会让你的模型更聪明。
如果你真的想构建这个,三个特定的设计选择将决定系统的成败。如果这些错了,它们绝对会在生产中咬你。
以下是它们按潜在损害排序的方式:
- 永远不要从行为推断状态。 你必须在数据层面直接区分“仍在工作”和“永久卡住”。就绪检查的布尔返回值是一个绝对的陷阱。如果单个
False既意味着临时延迟又意味着永久失败,你的系统就无法做出明智的选择。使用显式的第三种状态。 - 不要依赖终止启发式。 很容易假设上次传递中没有任何变化,所以永远不会有任何变化。这是错的。它在你需要它工作时恰恰失败:轮询、退避或等待外部API。你的循环需要跟踪硬状态,而不是时间猜测。
- 隔离推理步骤。 将决定下一步行动的任何内容包装在一个单一的、可交换的接口后面。核心控制流不应该关心它是与Python规则、LLM调用还是人类回调对话。保持这个边界完全清晰是我能够单独对架构进行基准测试的唯一原因。
原文链接: Context Engineering Isn't Enough — A Loop Engineering Experiment With No LLM Inside the Loop
汇智网翻译整理,转载请标明出处