Agent Loop 的难点并不在于“让模型调用工具”,而在于如何处理真实运行时的边界:模型会不会反复使用相同参数调用同一个工具?输出被截断后,工具参数还可信吗?什么时候应当停止?模型服务失败后,重试由谁负责?

本文基于以下 release 源码,比较 Pi、OpenCode 与 Codex 这三个开源的Coding Agent在四个关键控制面上的取舍:

  • Codex:0.148.0,tag rust-v0.148.0
  • Pi:v0.84.2
  • OpenCode:v1.18.18

本文只根据以上版本的源码做出结论。

一、整体差异:三种把“保险丝”放在不同层的方式

比较维度 Pi v0.84.2 OpenCode v1.18.18 Codex 0.148.0
重复 Tool / 死循环 核心 Loop 不判断,预留 hooks 给 Harness 实现 内建 doom_loop:相同工具、相同输入连续 3 次会触发 permission 没有看到同类重复调用检测,更依赖 Runtime 生命周期控制
输出截断 不执行可能不完整的 Tool Call,让模型重新生成 保存 provider 的 finish reason;未见专门的截断恢复状态机 重点处理 stream 中断后的重连与重新 sampling
停止条件 Loop 提供基本退出边界,业务策略交给上层 以 Session 是否进入终态为中心 以当前 Turn 是否仍需要 follow-up 为中心
服务失败重试 Agent 层统一重试,默认 3 次 独立 Retry Policy,默认最多 5 次 HTTP request retry 与 stream retry 两层分离

注:Codex 将一次模型生成称为 sampling;在 Pi、Claude Code 等项目中,同一层概念通常表现为一次模型请求或一次 assistant response。

先给出一个简要判断:

  • Pi 更强调可组合性。核心 Loop 尽量少替开发者定义 policy。Pi相当于提供了一个基础框架,这个框架的可扩展性比较强,大家可以基于这个框架做更多定制化的Agent Runtime。
  • OpenCode 更强调显式控制。重复调用、步骤上限、重试与结束状态都被建模为可见规则。
  • Codex 更强调长生命周期运行。上下文、取消、恢复和重试被吸收到更厚的 Runtime 中。

二、重复 Tool 调用:谁来判断“这是不是死循环”?

这里的死循环,指模型持续产生相同的 Tool Call,尤其是反复以相同参数调用同一个工具,且这种重复没有推动任务向前。

2.1 Pi:核心 Loop 只定义生命周期

Pi 的核心实现位于:

packages/agent/src/agent-loop.ts

runLoop() 负责把模型响应、工具执行、下一轮准备和 follow-up 消息串联起来。不过,它没有在核心路径里维护“工具名 + 参数指纹”“重复调用次数”“最近 N 次工具调用”或“是否取得进展”等状态。

这意味着,Pi 不会在 core 层直接判定“同一个工具调用三次就是风险”。它把这个判断交给上层 Harness,同时提供了足够的扩展点:

  • beforeToolCall:可在调用前阻断工具。
  • afterToolCall:可观察调用结果。
  • shouldStopAfterTurn:可在当前 turn 后结束 Loop。
  • Tool Result 中的 terminate: true:可结束当前工具批次。

这是一种刻意的设计取舍。重复调用并不总是错误,轮询后台任务状态就是典型例子。Pi 的立场更接近:核心提供可控的生命周期,上层应用自己决定什么算死循环(OpenClaw小龙虾就是这么做的)。

2.2 OpenCode:为重复调用提供显式 guard

OpenCode 的相关实现位于:

packages/opencode/src/session/processor.ts

它定义了一个很清晰的阈值:

const DOOM_LOOP_THRESHOLD = 3

当模型即将产生 Tool Call 时,OpenCode 会检查当前 assistant message 最近三个工具 parts 是否使用了相同工具和相同输入。命中后,它不会直接杀掉任务,而是发出:

permission: "doom_loop"

也就是说,OpenCode 将“连续三次相同 Tool + 相同 Input”视为一个需要权限层处理的风险信号。

这个机制的边界也很清楚:它检测的是调用模式,不判断工具结果是否带来新的信息;并且在 v1.18.18 中,它读取的是当前 assistant message 的 parts,不是跨多个 turn 的完整语义轨迹。因此,这是一个低成本、易理解的 heuristic,而不是通用的任务进展判断。

2.3 Codex:没有看到通用的重复调用阈值

在 Codex rust-v0.148.0 的 Runtime 主路径中,没有看到与 OpenCode 类似的“同工具、同参数达到 N 次后拦截”机制。

turn.rs 的重心是 sampling、工具 Runtime、pending input、取消、Context budget、compaction 和 stream retry。换言之,Codex 更关心“一个 Turn 如何在复杂边界下可靠地持续运行”,而不是用一个通用阈值定义死循环。

这也符合 Coding Agent 的实际需求。原始分析中提到,Codex 的 awaiter agent instruction 明确允许重复调用工具直到后台任务完成;如果采用简单的重复检测,可能会误伤这类正当工作流。

2.4 本节小结

Pi 把死循环 policy 留给 Harness;OpenCode 直接实现了显式的重复模式检测;Codex 没有看到通用 detector,而是以 Runtime 生命周期控制承接风险。

三、输出被截断:不完整的 Tool Call 还能执行吗?

输出因长度达到上限而结束,和生成过程中 stream 中断,是两个不同问题。Pi、OpenCode 与 Codex 在这里分别优先保护工具安全、记录完成状态与恢复流式连接。

3.1 Pi:宁可失败,也不执行可疑的工具参数

Pi 会明确识别:

message.stopReason === "length"

当输出因 Token limit 被截断,而消息中又带有 Tool Call 时,Pi 不会正常执行这批调用,而是将其标记为失败。

这样做的原因是,流式工具参数可能经由 JSON salvage parser 形成“形式上能解析、实际却缺字段”的对象。执行这类 Tool Call 可能带来不可靠的副作用。

Pi 的恢复策略是:

  1. 不执行可能被截断的 Tool Call。
  2. 将“参数可能不完整,请重新发送完整调用”的 observation 返回模型。
  3. 保持 terminate: false,让模型在下一轮重新生成工具调用。

它不是从文字断点继续续写,而是先保护工具执行边界,再让模型生成一份完整请求。

3.2 OpenCode:保留 finish reason,但未见专门的 length recovery

OpenCode 会将 provider 的 value.reason 保存到 assistantMessage.finish,并记录 step-finish。Session Loop 会根据这些状态判断当前消息是否已完成。

不过,在 v1.18.18 的主路径中,没有看到专门针对 finish === "length" 的恢复状态机,例如自动要求模型从断点继续、提高输出上限,或将截断的 Tool Call 统一判为无效后重发。

因此,准确结论是:OpenCode 会保存并建模 provider 的 finish reason,但至少在该版本的 Session Loop 中,没有看到像 Pi 一样针对 token-truncated Tool Call 的专门策略。 这不等于它完全“不支持截断”。

3.3 Codex:重点处理 stream failure

Codex 更需要区分“输出正常到达上限”和“模型尚未完成、stream 却中断”两种情况。

对 stream failure,源码中的策略更明确:

DEFAULT_STREAM_MAX_RETRIES: u64 = 5;
DEFAULT_STREAM_IDLE_TIMEOUT_MS: u64 = 300_000;

也就是说,流断开后最多会重连或重新 sampling 五次;连续五分钟没有 stream activity,则视为 stream lost。

从已经检查的 0.148.0 代码看,没有足够依据说 Codex 具备 Pi 式的“length 后让所有 Tool Call 失效,再让模型重发”的路径。Codex 在这一维度更明显的设计重点,是把 stream error 视为可重试的 Runtime error。

3.4 本节小结

Pi 的原则是“不执行可能被截断的工具参数”;OpenCode 会保存结束原因,但没有看到专门的 length 恢复逻辑;Codex 则更偏向恢复异常中断的流式生成。

四、停止条件:什么时候才算一个任务真正完成?

模型当前消息没有 Tool Call,并不必然表示整个 Agent 任务已经结束。三者的区别在于:停止判断放在 Loop、Session,还是 Turn Runtime。

4.1 Pi:提供清晰的 Loop 边界

Pi 的 core Loop 主要在以下情况下停止:

  • 模型返回 erroraborted
  • shouldStopAfterTurn hook 要求结束。
  • 当前工具批次通过 terminate: true 满足终止条件。
  • 没有待执行 Tool Call,也没有 steering 或 follow-up 消息。

Pi 的职责是提供可预测的生命周期边界;“任务是否已经足够完成”“是否达到业务上限”等更复杂的 policy,仍由上层 Harness 决定。

4.2 OpenCode:由 Session 是否进入终态决定

OpenCode 的停止判断更像状态机。它会检查最新 assistant 是否已有 finish、是否不是 tool-calls、是否没有剩余工具调用,以及这条消息是否对应最新 user request。满足这些条件后,Session Loop 才认为用户得到了完成的响应。

除了正常完成,它还处理以下明确路径:

  • 达到 agent.steps,通过 MAX_STEPS_PROMPT 引导最后一轮。
  • processor 返回 "stop"
  • structured output 完成。
  • content-filter 导致当前处理结束。
  • 用户 Interrupt 被转换为 AbortError,并 finalize 当前 assistant。

因此,OpenCode 不是简单地以“没有 Tool Call”作为停止条件,而是以 Session 的状态转换作为最终依据。

4.3 Codex:一个 Turn 可以包含多次 sampling 与 compaction

Codex 的一个用户 Turn 可能经历多轮内部循环:模型生成、工具执行、再次生成、必要时 compact 后继续。因此,一个 Turn 的结束不等于一次 sampling 的结束。

它的核心判断是 needs_follow_up:只要模型仍需要后续处理,或仍存在 pending input,当前 Turn 就不算完成。Token/Context 压力也未必导致停止,Runtime 可以先 compact 再继续。

取消令牌、Turn error、hooks 与 compaction 失败等,则是其他可能结束 Turn 的路径。与 OpenCode 的 Session 状态机相比,Codex 更像围绕 Runtime lifecycle 推进和收尾。

4.4 本节小结

Pi 以 Loop 边界为主;OpenCode 以 Session 终态为主;Codex 以 Turn 是否仍需 follow-up 为主。三者不适合硬套为同一组固定数量的“退出类型”。

五、LLM API 失败:重试放在 Agent、Session 还是 Runtime?

重试策略不仅决定重试几次,也决定失败信息在哪一层可见、谁可以取消等待、谁可以统一执行策略。

5.1 Pi:Agent 层集中重试

Pi 的 Coding Agent settings 中包含:

retry.enabled = true
retry.maxRetries = 3
retry.baseDelayMs = 2000
retry.provider.maxRetries = 0

默认的 Agent-level 退避节奏是 2s、4s、8s,同时 Provider SDK 默认不自行重试。

这种设计让 Provider failure 回到 Agent layer,再由 Agent 决定是否重试,而不是让 SDK 因较长的 Retry-After 在内部静默等待。Provider 层仍支持 timeoutMsmaxRetriesmaxRetryDelayMs;其中 maxRetryDelayMs 默认 60 秒。

5.2 OpenCode:独立、显式的统一 Retry Policy

OpenCode 的重试逻辑集中在:

packages/opencode/src/session/retry.ts

它对 429、常见 5xx、服务过载、连接被拒绝或丢失、ECONNRESETETIMEDOUT,以及 request/response/stream timeout 等情况都有分类。

其默认策略是 2 秒起步的指数退避、2 倍 backoff、25% jitter,最多重试 5 次;如果服务端返回 Retry-AfterRetry-After-Ms,则优先使用服务端建议。processor 通过 Effect.retry(SessionRetry.policy(...)) 采用这一策略。

三者之中,OpenCode 的 retry policy 最集中,也最容易直接阅读、调整和复核。

5.3 Codex:请求失败与流中断分两层处理

Codex 把重试拆为两个范围。

第一层是 HTTP request retry。默认配置的核心边界是:

DEFAULT_REQUEST_MAX_RETRIES = 4
retry_429: false
retry_5xx: true
retry_transport: true

也就是说,在这个版本中,5xx 与 transport/connection failure 会进入普通 request retry,而直接 HTTP 429 不会走这条普通策略。retry_429: false 是重要的版本事实,不能简单套用其他 Coding Agent 中“429 必然指数重试”的判断。

第二层是 stream retry。流建立后如果中断,最多会进行 5 次 stream retry;如果五分钟没有 stream activity,则认为流已失效。

这种分层意味着:请求还未建立成功,与流已经建立但在生成中断开,由不同的机制负责。

六、结语:同一个 Agent Loop 问题,三种不同的责任边界

问题 Pi OpenCode Codex
模型乱循环 Harness 自定义 policy doom_loop heuristic Runtime lifecycle,未见通用重复检测
输出截断 拒绝执行不完整 Tool Call 保存 finish reason 重点处理 stream/error retry
Agent 何时停止 Loop boundary 与 hooks Session 到达终态 Turn 不再需要 follow-up
LLM 服务失败 Agent-level bounded retry 统一 Retry Policy request retry + stream retry

从功能清单看,很容易得到“谁有、谁没有”的扁平结论;但源码真正值得比较的,是每个项目把控制策略放在了哪一层。

Pi 的价值在于可组合:核心不抢占 Harness 的业务判断。OpenCode 的价值在于显式:重复调用、步骤数、状态和重试都形成可见规则。Codex 的价值在于 Runtime:它将上下文、恢复、取消与持续执行统一进 Turn 生命周期。

所以,Agent Loop 的差异最终不在于“是否有一个 while 循环”,而在于系统如何划分继续、恢复、收尾与停止的责任边界。

Logo

葡萄城是专业的软件开发技术和低代码平台提供商,聚焦软件开发技术,以“赋能开发者”为使命,致力于通过表格控件、低代码和BI等各类软件开发工具和服务,一站式满足开发者需求,帮助企业提升开发效率并创新开发模式。

更多推荐