一次模型调用是一锤子买卖:文本进、文本出、结束。而当你把这次调用包进一个循环里——调用模型、执行它要做的事、把结果喂回去、再调一次——直到任务完成,这就成了 Agent。那个循环正是 Agent 探索、行动、自我纠错的全部发生之处,把它设计好本身就是一门学问:Loop Engineering(循环工程)。当前沿模型的原始能力趋同、"context engineering(上下文工程)"成了"喂什么给模型"的热词之后,循环工程是再下一层——决定何时思考、何时行动、何时停止、出错怎么办的那套控制流。本文是 Harness 工程的姊妹篇:harness 是整辆车,这篇讲的是发动机的燃烧循环。
- 循环把一锤子模型变成 Agent。 调用 → 行动 → 观测 → 重复,这个循环是探索、行动、自我纠错发生的地方。
- 停止是最难的部分。 知道任务何时真正完成——既不早停、也不空转到天荒地老——是核心的控制流问题。
- 每个循环都要有预算。 步数、token、时间、成本的硬上限不可省:它们是兜底,防止一个犯迷糊的 Agent 无限烧资源。
- 验证闭合循环。 跑测试/构建、把失败喂回去,才让循环收敛到正确答案,而不是干转。
- 恢复是工程的大头。 工具报错、输出格式坏、Agent 反复重试一个失败动作("死循环"),都需要重试、死循环检测、逃生出口。
- 拓扑是设计选择: 单循环、反思(行动 → 批评 → 重试)、计划-执行、编排器 + 子 Agent,各自适配不同任务。
- 循环靠实证调优 —— 停止条件、预算、上下文处理的小改动会大幅摆动真实成功率,所以你用评测和轨迹分析来量。
循环工程是设计 Agent 控制循环的学问:plan-act-observe 循环、决定何时算完成的停止条件、约束它的步数/token/时间/成本预算、把它锚在环境里的验证、处理错误并打破死循环的恢复逻辑,以及适配任务的拓扑(单循环、反思、计划-执行、子 Agent)。因为模型在商品化,把这个循环工程做得多好,越来越是"能完成任务的 Agent"和"瞎扑腾的 Agent"之间的分水岭。
循环到底是什么
抛开框架名字,每个 Agent 都是同样的五行:维护一份滚动状态(对话,加上目前观测到的一切),在它上面调用模型,如果模型要用工具就执行那个工具、把结果追加进状态、再调一次模型。重复,直到模型说完成或你撞上限。就这样——循环很小。难的是你必须围着这五行做的所有决定。
state = [system, tools, goal]
steps, spent = 0, 0
while not done(state) and steps < MAX_STEPS and spent < BUDGET: # ← 停止 + 预算
reply = model(state) # 唯一的 LLM 调用
if reply.tool_calls:
for call in reply.tool_calls:
obs = execute(call) # ← 报错?超时?恢复
state.append(obs)
else:
if verify(reply): return reply # ← 它真的对吗?
state.append("验证失败:…") # ← 把失败喂回去,继续
state = compact(state) # ← 跨迭代的上下文
steps, spent = steps + 1, spent + reply.cost
return give_up(state) # ← 优雅失败 / 升级上报
每条注释都标着一个循环工程决定:done 何时为真、预算多大、execute 失败怎么恢复、verify 查什么、怎么 compact 状态以塞进窗口、以及从底部掉出来时怎么办。本文剩下的部分就是这些决定。
为什么循环如今成了产品本身
有几年的叙事是"模型更强者胜"。这正在变平:前沿模型在趋同,同一个模型放进两个不同循环里,在真实任务上的成功率能天差地别。循环控制着模型每一轮看到什么、给它几次尝试、它会不会发现自己的错、以及它是优雅收场还是烧光你的预算。所以差异点沿着栈往上移了:先是上下文工程(喂什么给模型),现在是 循环工程(怎么把重复调用编排成可靠的工作)。那些"就是感觉更能干"的编码 Agent,通常是循环工程得更好,而不是有什么秘密模型。
控制流:计划、行动、观测
循环体的经典写法是一个三拍循环,常被称为 ReAct(reason + act):模型推理下一步该干嘛,发出一个动作(工具调用),harness 返回一个观测,这个观测回到上下文里供下一次推理用。威力在于反馈:Agent 不是一上来就预测整个解,而是一次走一个有据的步子,根据每个动作揭示的东西去适应。
第一个真正的设计选择就在这里:每轮做多少。 每步一个工具调用最简单、最好恢复,但慢、啰嗦。允许模型一次请求好几个工具调用(比如并行读)能砍延迟和 token 成本——代价是其中一个失败时错误处理更难。多数生产循环允许批量的只读调用,但把任何改状态的操作串行化,这样一个失败是局部的。
停止条件 —— 最难的部分
任何循环里都有一个貌似简单实则最难的问题:"我们完成了吗?"停太早,Agent 交回一个做了一半的任务;不停,它要么永远转,要么把一个本来不错的答案一直"改进"到坏掉。没有单一正确答案——你要组合几个信号:
- 模型自报完成 —— 模型不再调工具、给出最终答案。必要但不充分:模型动不动就提前宣布胜利。
- 验证门控 —— 只有检查通过(测试绿、构建成功、输出符合 schema)"完成"才算数。这是最强信号,因为它锚在环境里,而不是模型的意见。
- 目标/退出标准 —— 一个显式谓词("函数存在且所有测试通过"),由循环来求值,而不是信一段话。
- 预算耗尽 —— 不管状态如何的硬停;是兜底,不是目标。
绝不让模型成为"完成"的唯一裁判。模型宣布成功是一个停止请求,不是完成的证明——任务只要有验证检查,就用它来门控。"它说它做完了"是 Agent 版的"在我机器上能编译"。
预算:步数、token、时间、成本
循环里的 LLM 默认是个无底洞——一个犯迷糊的 Agent 会乐呵呵地走 200 步、烧着钱原地打转。预算是让循环能无人值守安全运行的硬上限,一个认真的循环会同时盯几个:
| 预算 | 限什么 / 为什么 |
|---|---|
| 步数 / 迭代数 | 最简单的兜底——限制单个任务能做多少次模型调用,卡住的 Agent 会停而不是空转。 |
| token 预算 | 限制总上下文 + 生成 token;最真实的成本代理,因为花费随 token 而非轮数走。 |
| 墙钟 / 超时 | 限制单个工具调用(挂死的命令)和整个任务;保护对延迟敏感的调用方。 |
| 美元成本 | 每任务或每用户的上限;你实际计费的依据,也是运维关心的限。 |
微妙之处不在于有预算,而在于快撞上时怎么办。好的循环会优雅降级:步数预算见底时,它可以从"探索"切到"收尾"("你还剩 2 步——总结你发现的然后停"),而不是思考到一半被一刀切。预算不只是个开关;它是循环能去推理的一个信号。
验证:闭合循环
没有验证的循环只是在迭代;有验证的循环才会收敛。把真 Agent 和花哨自动补全分得最开的那个特性,就是把"跑一遍工作的结果"喂回循环:跑测试、linter、构建、查询——把失败变成一个观测,让模型去推理并修正。这把 Agent 锚在环境里,而不是它自己(常常是错的)那个"代码是对的"的信念。
验证让循环的迭代有意义:每一轮不是重新掷一次骰子,而是朝着一个被检验过的结果迈一步。这一步的质量取决于循环工程的选择——跑什么、怎么把一份 5000 行的失败日志缩成关键那几行、以及部分通过何时"够好"。一个弱模型配一个紧凑的"验证-重试"循环,常常打败一个没验证的强模型。(评估循环到底有没有收敛,本身是一门学问——见 LLM 应用的评测。)
错误恢复与死循环
Agent 会以单次调用永远不会的方式失败,生产循环里大部分工程就是这些恢复路径。大头有:
- 工具报错 —— 命令失败或文件不在。修法是把一个可读、可操作的错误当作观测返回("文件未找到:你是不是想要 X?"),而不是一坨原始堆栈——错误文本是模型据以行动的反馈。
- 输出格式坏 —— 模型给工具调用吐了非法 JSON。用解析错误重新提示(可选配约束解码),而不是让循环崩掉。
- 死循环(doom loop) —— Agent 一遍遍重复同一个失败动作,坚信这次会成。这是 Agent 的标志性失败,需要显式的死循环检测。
死循环检测就是察觉重复——同样的工具调用配同样的参数,或同样的错误 N 次——然后打破这个模式:注入一句提醒("这招已经失败两次了;换个法子"),升级到更强的模型,或停。没有它,预算是你唯一的保护,而你会把整个预算浪费在同一个错误上。两者配对——早早检测重复、用预算兜底——是标准的双保险。
recent = []
def guard(action, result):
sig = (action.name, action.args, result.error) # 给这次尝试做指纹
recent.append(sig)
if recent[-3:].count(sig) >= 3: # 同一失败动作 3 次
return "卡住了:这反复失败了——换策略或停止"
return None # 否则正常继续
循环拓扑
朴素的 while 循环是基线,但"循环工程"越来越意味着为任务挑对循环的形状。四种常见拓扑:
- 单循环 —— 默认的 ReAct 循环。最适合开放式任务,下一步取决于上一个观测;最易构建和调试。
- 反思 —— 产出答案后,一个独立的批评步骤找毛病并触发修订。加了一拍自审,能抓住第一遍漏掉的错误,代价是额外的模型调用。
- 计划-执行 —— Agent 先起草一个计划,再执行各步(每步可能是自己的小循环)。让长任务保持连贯,也给你一个在花钱执行前先检视计划的检查点。
- 编排器 + 子 Agent —— 一个主循环把有边界的活(搜代码库、审 diff)委派给有自己干净上下文的新子 Agent,后者只回一个结论。这限制了任一上下文窗口要装多少,也使能并行——大任务 Agent 背后多是这个拓扑。
跨迭代的上下文
每次循环迭代都把观测追加进状态,所以长任务必然朝着上下文窗口的天花板膨胀。管理这种增长是核心的循环工程活——循环得在甩掉噪声的同时,把目标和关键事实留在视野里。标准做法是压缩(compaction):当状态逼近窗口时,总结或丢弃更早的轮次,保留目标、计划和近期结果。相关手法还有:把细节卸到一个 Agent 可按需重读的外部便签上,以及上面那个子 Agent 模式——它通过把重活发给子进程,让主循环的上下文保持精简。这事做砸了,Agent 会在任务中途"忘了"自己在干嘛;做对了,它能在一个问题上工作得比任何单个窗口都久。
用实证调循环
你没法靠直觉调循环,因为真正重要的改动——不同的停止条件、更紧的预算、何时压缩、要不要加一拍反思——会以出人意料的幅度相互作用、摆动真实成功率。认真的循环工作是实证的:把 Agent 跑在基准任务上,量成功率、步数、成本,并检视轨迹(完整的调用与观测序列)看运行在哪出错——提前停、死循环、预算爆掉。然后改一处、再量。循环是一个你对着数据来调的系统,就像你调任何控制系统一样。
被要求"设计一个 Agent"时,别急着跳到模型。先讲循环:"它是一个调用-行动-观测的循环;有意思的决定是停止条件、预算、循环内验证、以及从死循环里恢复。"这个定调表明你懂 Agent 的可靠性到底从哪来。
坑与取舍
- 信模型自报的"完成"。 没有验证门,Agent 凭自己一句话就停、交回做了一半的活。用真实检查门控。
- 没有死循环检测。 只靠预算意味着一个死循环会把整个预算浪费在一个错误上;早早检测重复。
- 预算没有优雅降级。 思考到一半被硬切,浪费了已经做的功;让循环在接近上限时收尾。
- 拓扑过度工程。 反思和多 Agent 增加模型调用、延迟、成本;很多任务用一个紧凑单循环更好。只在更简单的循环明显失败时才加结构。
- 上下文腐烂。 跳过压缩,窗口会被陈旧观测填满,质量悄悄下降;每次迭代都管理状态。
- 盲调。 没有轨迹分析,你会修错地方——你看到成功率低,却不知道是提前停、死循环、还是恢复差。
一个 Agent 就是一个循环,而循环工程是把它设计好的学问。模型供给推理;循环供给把推理变成可靠工作的控制流——决定何时行动、何时停止、花多少、怎么验证、怎么恢复。随着模型商品化,这套控制流越来越是产品本身:同一个模型在精心设计的循环里能完成任务,在马虎的循环里就一直扑腾到预算耗尽。
"循环"是什么? 包住 LLM 调用的控制循环——调用 → 行动 → 观测 → 重复——把一锤子模型变成会探索、自我纠错的 Agent。
循环最难的部分? 停止条件:知道任务何时真正完成,既不早停也不空转;用验证而不是模型的话来门控。
Agent 为什么要预算? 循环里的 LLM 无限花钱;步数/token/时间/成本的硬上限是兜底,最好在接近上限时优雅收尾。
什么是死循环、怎么停? Agent 反复一个失败动作;检测重复(同样调用/错误 N 次)并打破模式——提醒、升级或停——用预算兜底。
循环靠什么收敛而不空转? 循环内验证——跑测试/构建、把失败喂回去,把每次迭代锚在环境里。
何时用子 Agent? 任务大或上下文变乱时——把有边界的活委派给有干净上下文的新子 Agent、合并结果,还能并行。