Agent 工程对照
← 返回框架对照

循环 · 第 2 章

主循环形态

「继续做下一步」这个决定,由什么结构做出?

openJiuwen 给循环设了迭代上限,其余三家不设上限、靠外部条件退出;四家都在循环顶部处理用户插话。

循环有没有上限

有界迭代预算

无界循环,靠外部条件退出

场景建议

编辑判断

按你的需求,倾向哪种做法

借鉴的是那一家的设计,不等于选用那个框架。

场景

每次调用的最坏成本必须可控

只有它用 for range 加迭代上限,最坏情况是固定轮数,不靠外部条件兜底。

场景

用户要能在模型运行时插话

Codex 每轮先排空 input_queue;DeepSeek 把非 idle 相位的唤醒闩锁下来、收敛后重放,两者都把插话当一等问题。

场景

从零自建一个 Harness

内外两层 while 的分工写在注释里,和 openJiuwen 的 ReAct + Task Loop 语义一一对应,是这个结构最短的参照实现。

容易踩的坑

  • 四家都在循环顶部处理用户插话——「可打断」已是默认要求,事后再加会牵动整个循环结构。

四家怎么做

差异与源码

openJiuwen

有界迭代预算的 for 循环

唯一一个用固定迭代上限的实现:max_iterations 默认 5,循环体是 range 而不是 while。每轮开头先消费 force_finish(由 rails 在 AFTER_REACT_ITERATION 设置),让优雅中止落在迭代边界而不是打断执行到一半的一轮;随后注入 rails 放行的 steering 消息。真正的长任务由外层 Task Loop 承担,ReAct 只负责「一步做对」。

适合
单步推理要可预期、成本要封顶的场景;长任务另由外层 Task Loop 承担。
代价
默认上限很低(5 轮),真正的长任务必须再搭一层外循环,结构多一层。
Codex

无界 loop,回合内反复取用户插话

run_turn 内部是一个没有计数上限的 loop。每次进入循环先尝试排空 input_queue —— 也就是模型还在跑时用户从界面提交的消息。注释直白地写出了这里的张力:界面允许用户随时插话,但模型不一定接得住。退出靠 hook 返回、任务完成或取消令牌,而不是迭代次数。

适合
交互式编码:用户随时插话,模型跑的时候也要能接住新输入。
代价
没有计数上限,退出完全依赖 hook、完成信号和取消令牌,出错时容易空转。
DeepSeek Harness

idle / running / maintenance 三相状态机

循环本身只有一行:while (await this.turn()) {}。真正的复杂度在它外面的相位机——Agent 在 idle、running、maintenance 三种相位之间转换,唤醒(wake)在非 idle 相位会被闩锁下来等待收敛后重放。这让「模型正在跑的时候来了新消息」变成一个显式的状态转换问题,而不是循环体里的一段 if。

适合
需要把「运行中来了新消息」讲清楚的多入口系统,例如同时接 UI、SDK 与定时唤醒。
代价
复杂度搬到了相位机:要理解 idle / running / maintenance 的转换和唤醒闩锁才能改循环。
Pi

显式内外两层 while,注释直接写明分工

最容易读懂的一份:外层循环负责「Agent 本来要停了,但排队消息又来了」,内层循环负责「还有工具调用要处理」。openJiuwen 用 ReAct + Task Loop 两个模块表达的双层结构,Pi 用同一个函数里的两个 while 表达,语义几乎一一对应——这是双层循环属于问题本身、而非某家设计偏好的直接证据。

适合
想读懂、想自己改循环的团队;也适合作为自建 Harness 的起点。
代价
功能少:插话、停止这些策略要自己在两层 while 之间补。
横向读这一行

四家在同一个问题上分成两派:openJiuwen 用有界迭代预算(for range),其余三家都是无界循环,靠外部条件退出。但四家都在循环顶部处理同一件事——用户在模型跑的时候插话(steering / pending input),这说明「可打断」已经是主循环的默认要求,而不是附加功能。

设计空间分析

四家把「还要不要做下一步」放在不同的结构层级:openJiuwen 压成循环边界上的迭代上限,deepseek harness 挪到循环外的相位状态机,codex 交给外部的钩子与取消令牌,pi 用两层 while 分开问。

openJiuwen 是唯一用固定迭代上限的实现:ReAct 循环体是 range 而不是 while,max_iterations 默认 5,真正的长任务交给外层 Task Loop 承担,ReAct 只负责「一步做对」。循环体足够短,是因为它把「做多少步」这个问题挪到了循环外面。

把四格并排读,「继续还是停」这个决定被放在不同的结构层级里:openJiuwen 把它压缩成循环边界上的一个数字判断(for + max_iterations),每轮开头还要先检查 rails 在上一轮设置的 force_finish,让优雅中止落在迭代边界上;deepseek harness 把它挪到循环外面的相位机里,循环本身只剩 while (await this.turn()) {} 一行,复杂度在 idle/running/maintenance 三态转换和被闩锁、等收敛后重放的 wake 请求里;codex 干脆不在循环层面设判断,loop 本身没有计数上限,退出交给 hook 返回、任务完成或取消令牌;pi 用同一个函数里的两层 while 把这个问题拆成两半——外层问「是否还有排队消息」,内层问「是否还有工具调用要处理」。

编辑判断

四家的差异更像是对「Harness 该管多宽」的不同回答,而不是循环写法本身的优劣:把循环收窄成一步、把编排交给上层的做法(openJiuwen),扩展性更好但要求上层足够厚;把循环做成状态机的做法(deepseek harness),单体能力更强但可替换性更差;codex 和 pi 走的是另两条路——前者不设状态机,把继续/停止的判断权交给外部的 hook 和取消令牌,后者用双层 while 把两种「还要不要继续」的问题在同一个函数里分开问,不假外部结构。

还没有定论:有界迭代、相位状态机、hook 退出、双层 while 这四种结构在长任务和多轮打断场景下实际的鲁棒性差多少,目前没有可比的一手测试数据,以上是源码层面的结构比较。

这个问题为什么存在

第 2 章

Agent 循环

收集上下文 → 行动 → 检查 → 重复,直到满足停止条件