原文:https://mp.weixin.qq.com/s/GryGhL7SLCPwDQF0VjsPeg

Agent Loop 的基本流程并不复杂。

模型发起工具调用,Harness 执行后将结果返回给模型,模型再决定下一步。围绕这几步,就能搭建一个最简单的 Agent 循环。

但在 Claude Code、Codex 这样的实际产品中,还需要处理大量具体问题:工具如何并行执行、用户能否中途追加指令、权限检查放在哪个环节、上下文接近上限时如何压缩,以及整个循环什么时候结束。

不同 Harness 对这些问题的处理方式并不相同。

Claude Code、Codex、OpenCode、Pi、Oh My Pi 和 DeepSeek Harness 的 Agent Loop,从循环结构、工具执行、上下文管理到扩展机制,逐一梳理了六套系统的具体实现。

六套系统遵循相似的基本流程,但在循环的组织方式、状态管理和扩展机制上,各有不同的设计取舍。

Claude Code

Claude Code 的 agent loop 是一个单线程主循环:调用模型 → 有 tool_use 就执行 → 结果回填 → 再调用,直到模型给出不含工具调用的回复。

循环本身很简单,复杂度都放在工具、权限、hooks、自动压缩和子 agent 上。

循环结构

  • 官方把它叫 agentic loop:收集上下文 → 行动 → 验证,三个阶段交织进行。
  • 实现上就是一个 while 循环:模型输出里有 tool_use 就继续,没有就结束。
  • 一次“模型输出 + 执行工具 + 回填结果”称为一个 turn。
  • Agent SDK 暴露的就是同一个循环,CLI / 桌面 / IDE 共用。

一次模型调用

  • 每次请求都带上:系统提示、CLAUDE.md、工具定义、完整历史。
  • 不变的前缀走 prompt caching,降低重复成本。
  • effort 控制推理深度;thinking 块单独输出。
  • 每次响应返回后累计 cost,用来做预算检查。

工具执行

  • 同一轮多个调用:只读工具(Read / Glob / Grep、标注只读的 MCP)并发执行。
  • 改状态的工具(Edit / Write / Bash)串行执行。
  • 执行前过权限:PreToolUse hook → deny / allow 规则 → 权限模式 → 询问用户。
  • auto 模式用一个分类器模型审查命令和网络请求。

用户插话

  • 可以随时中断、换方向、补信息或让它换种做法。
  • 流式输入下,运行中发来的消息先排队,并入后续的模型调用。
  • 到 maxTurns 时仍在排队的消息不并入,而是另起一个新的 turn。

上下文压缩

  • 接近窗口上限自动压缩:先清旧工具输出,再摘要对话。
  • 流里发出 compact_boundary;PreCompact hook 可先归档全文。
  • 单个超大输出导致反复压缩时,几次后停止并报错,不会死循环。
  • 可在 CLAUDE.md 写明“压缩时要保留什么”。

停止条件与子 agent

  • 模型回复里没有 tool_use → 结束,返回 ResultMessage(含 cost / usage)。
  • maxTurns(只计工具轮)/ maxBudgetUsd 封顶,超了返回错误子类型。
  • Stop hook 可以 block 停止,让 Claude 接着干。
  • Agent 工具派生子 agent:全新上下文,只把最终回复交回父循环;预算含子 agent。

Agent Loop 运行流程图

核心循环伪代码

*按官方文档还原

messages = [system, CLAUDE_md, *history, user_input]    # UserPromptSubmit
while True:
    maybe_auto_compact(messages)                         # PreCompact: 清旧输出 → 摘要
    resp = claude.stream(messages, tools, effort)        # thinking / text / tool_use
    messages.append(resp)
    if not resp.tool_uses:
        if stop_hook(resp) == "block":                   # Stop hook 让它继续
            messages.append(stop_reason); continue
        return Result(resp.text, cost, usage)
    allowed = [c for c in resp.tool_uses if permitted(c)] # PreToolUse → 规则 → 模式 → 询问
    results = run_parallel([c for c in allowed if c.read_only])
    results += run_serial([c for c in allowed if not c.read_only])
    messages.append(user(tool_results=results))          # PostToolUse
    messages += drain_queued_user_messages()             # 运行中排队的插话
    if turns >= max_turns or cost >= max_budget:
        return Error("error_max_turns / error_max_budget_usd")

扩展点:在循环的哪里插手

  • UserPromptSubmit:提交输入前;可改写、补上下文或拦截。
  • PreToolUse:工具执行前;可拒绝、放行或改参数。
  • PermissionRequest:将弹权限框时;可代替用户决定。
  • PostToolUse:工具执行后;可给模型追加反馈。
  • PostToolBatch:一批工具全部完成后。
  • PreCompact / PostCompact:自动或手动压缩前后。
  • Stop:将要结束时;可 block 让它继续。
  • SubagentStart / SubagentStop:子 agent 起止。

设计取舍

亮点

  • 循环本身极简,复杂度都在工具、权限和 hooks 上。
  • 读并发、写串行,兼顾速度与安全。
  • Stop hook、预算、子 agent 让循环可以被外部编排。
  • Hook 事件量丰富,几乎每个阶段都能插手。

代价

  • 源码不开源,内部调度细节只能看文档。
  • 子 agent 只回传最终回复,父循环看不到中间过程。
  • 压缩是有损摘要,早期细节可能丢失。

Claude Code 的循环是“薄循环 + 厚外壳”:循环只管调模型和执行工具,安全与控制都交给权限系统和 hooks。

来源

[1] Claude Code — 工作原理
https://code.claude.com/docs/en/how-claude-code-works
[2] Claude Code — Agent Loop 官方文档
https://code.claude.com/docs/en/agent-sdk/agent-loop
[3] Claude Code — Hooks 参考文档
https://code.claude.com/docs/en/hooks

Codex

Codex(codex-rs)里,一次用户输入启动一个 turn,turn 内循环多次采样:每次采样是一次 Responses API 请求,加上执行这次响应里的全部工具调用。

循环条件是 needs_follow_up:模型还要继续,或者还有用户插话没并入。

循环结构

  • 客户端和内核通过 SQ / EQ 通信:提交 Op 进队列,内核推回 EventMsg。
  • run_turn() 里是一个 loop:并入插话 → 采样 → 执行工具 → 判断是否继续。
  • 一个 Thread 同一时间只有一个活跃 turn。
  • turn 开始前先跑 SessionStart / UserPromptSubmit hooks。

一次模型调用

  • build_prompt:历史 ResponseItem + 工具定义 + parallel_tool_calls: true。
  • 流式接收:文本、推理、计划更新实时推给客户端。
  • 断流按 stream_max_retries 自动重试。
  • turn_context 固定本轮的模型、沙箱、审批策略等设置。

工具执行

  • 边流边执行:OutputItemDone 一到就启动工具 future,不等整段响应结束。
  • FuturesOrdered 保证结果按调用顺序回填。
  • 并行闸门是读写锁:支持并行的工具拿读锁并发,其它拿写锁独占。
  • ToolOrchestrator:审批 → 选沙箱 → 执行 → 被沙箱拦截则升级重试(不重复审批)。

用户插话

  • turn 运行中可以 turn/steer:消息进 input_queue,不断开 turn。
  • 每次采样前取出 pending input 并入(先过 UserPromptSubmit hook)。
  • 还有没并入的输入也算 needs_follow_up,循环会继续。
  • turn/interrupt 中断当前 turn,并记录 turn_aborted。

上下文压缩

  • 采样前检查:超过 model_auto_compact_token_limit 先压缩。
  • 采样后如果还要继续且已到上限,原地压缩后接着跑(也支持远端压缩)。
  • 换成窗口更小的模型时,会先用旧模型压一遍。
  • 压缩结果写成 Compacted 记录,替换历史。

停止条件与多 agent

  • needs_follow_up 为假 → 运行 Stop hooks。
  • Stop hook 可以 block,并把续写提示作为新输入注入,循环继续。
  • 否则推送 TurnComplete 到 EQ,turn 结束。
  • 多 agent:子 agent 是独立 Thread,父 turn 通过工具收结果。

Agent Loop 运行流程图

核心循环伪代码

*简化自 codex-rs/core/src/session/turn.rs

async fn run_turn(sess, input) {
    run_hooks_and_record_inputs(&input).await;           // SessionStart / UserPromptSubmit
    run_pre_sampling_compact().await;                    // 超阈值先压缩
    loop {
        let pending = input_queue.get_pending_input();   // turn/steer 插话
        run_hooks_and_record_inputs(&pending).await;
        let r = run_sampling_request(build_prompt(history, tools)).await;
        // 流式事件里 OutputItemDone(FunctionCall) → in_flight.push_back(tool_future)
        // 工具:RwLock 读并行 / 写独占;审批 → 沙箱 → 执行 → 升级重试
        drain_in_flight().await;                          // 按顺序回填 FunctionCallOutput
        let follow = r.needs_follow_up || input_queue.has_pending_input();
        if follow && token_limit_reached { run_auto_compact().await; continue; }
        if !follow {
            match run_turn_stop_hooks().await {
                Block(prompt) => record(prompt),        // Stop hook 要求继续
                _ => break,
            }
        }
    }
    send_event(TurnComplete);                             // 推到 EQ
}

扩展点:在循环的哪里插手

  • SessionStart:会话开始 / 恢复 / 压缩后。
  • UserPromptSubmit:每条输入(含 steer)并入前。
  • PreToolUse:工具执行前;可拦截。
  • PermissionRequest:需要审批时。
  • PostToolUse:工具执行后。
  • Stop:turn 将结束;可 block 并给续写提示。

设计取舍

亮点

  • 工具边流边跑,长响应里的调用不必等到最后。
  • 沙箱 + 审批 + 升级重试统一在编排器里,安全模型最完整。
  • steer 并入当前 turn,插话不会打断节奏。
  • 压缩分采样前、采样中两个时机,长任务不容易爆窗口。

代价

  • 一个 Thread 同时只有一个活跃 turn。
  • 工具能否并行靠工具自报,默认保守(独占)。
  • 逻辑分散在 turn.rs / tasks / tools 多个模块,读起来较重。

Codex 的循环围绕“采样”组织:边流边执行工具,安全由沙箱编排器兜底,needs_follow_up 决定是否再采样。

来源

[1] OpenAI — Codex 官方仓库
https://github.com/openai/codex
[2] Codex — Agent Loop 主循环(turn.rs)
https://github.com/openai/codex/blob/main/codex-rs/core/src/session/turn.rs
[3] Codex — 工具并行执行(parallel.rs)
https://github.com/openai/codex/blob/main/codex-rs/core/src/tools/parallel.rs
[4] Codex — 工具执行编排(orchestrator.rs)
https://github.com/openai/codex/blob/main/codex-rs/core/src/tools/orchestrator.rs
[5] Codex — Hooks 运行时(hook_runtime.rs)
https://github.com/openai/codex/blob/main/codex-rs/core/src/hook_runtime.rs
[6] Codex — Hooks 模块
https://github.com/openai/codex/blob/main/codex-rs/hooks/src/lib.rs

OpenCode

OpenCode 的循环由 SessionRunner 驱动:从 SQLite 里的 inbox 取输入,按 step 循环;每个 step 是一次模型请求,有工具调用就再来一步。

工具在流里一出现就执行,每个 step 前后各抓一次 Git 快照,压缩、重试、续写都在 step 内部处理。(基于当前 dev 分支的 core 重写版)

循环结构

  • SessionRunner.drain:取一条输入,然后 while 按 step 推进。
  • 每个 step 一次模型请求;needsContinuation(有工具调用)就 step++ 再来。
  • runStep 内部还有一层循环,专门处理压缩、重试和续写。
  • 用 Effect 编写,状态变化通过事件总线推给各个客户端。

一次模型调用

  • 每个 step 重新加载上下文:instructions、agent、模型、工具、历史。
  • 超过 agent.steps 时追加 MAX_STEPS 提示,并设置 toolChoice: none。
  • 请求发出的同时并行抓 step-start 快照,不拖慢首字。
  • 最后一步也保留工具定义,以命中 provider 的前缀缓存。

工具执行

  • 流里一出现 tool-call 事件就执行(fiber 并发),不等整段响应结束。
  • provider 自己执行的工具(providerExecuted)跳过本地执行。
  • 权限规则 allow / ask / deny;ask 会弹窗,拒绝时抛 DeclinedError。
  • 参数没写完的调用不执行,回给模型“请重新发起”。

用户插话(Inbox)

  • inbox 每条都带 delivery:queue(等空闲)或 steer(运行中插入)。
  • 运行中只取 steer;到了空闲边界才取一条 queue 输入。
  • inbox 还能装控制项:compaction(手动压缩)、move(迁移会话位置)。
  • 进程重启后,先清理残留的压缩和工具调用,再继续。

压缩与失败恢复

  • 每个 step 前跑 compaction.compact(auto),压完重新加载再继续。
  • 请求报上下文溢出 → 以 overflow 为原因压缩一次后重来。
  • 可重试错误按策略等待;断流时合成一句“请从中断处继续”。
  • attempt 结果:Completed / Retry / Continue / Compacted / RecoverFull。

停止条件与子 agent

  • needsContinuation 为假(没有工具调用)→ 本次 drain 完成。
  • step 结算:结束快照 → patch 文件差异 → tokens / cost。
  • 达到 agent.steps 上限时,强制只输出文字总结。
  • 子 agent 以子 Session 运行,完成后把结果回传父会话。

Agent Loop 运行流程图

核心循环伪代码

*简化自 packages/core/src/session/runner/llm.ts、step.ts

drain(sessionID):
    settleStaleCompactions(); settleStaleToolCalls()      # 崩溃后的收尾
    whiletrue:
        next = advanceToStep()                             # 从 inbox 提升 queue / steer 输入
        if next != Ready: return next                      # Complete / Moved / Reloaded
        continuing = runStep(next.context, step); step += 1

runStep(ctx, step):
    whiletrue:                                            # 压缩 / 重试 / 续写循环
        if compaction.compact(reason="auto").completed: continue
        req = buildRequest(ctx, maxStepsPrompt if step >= agent.steps)
        outcome = attempt(req)                             # 流里 tool-call → 立即执行(并发)
        match outcome:                                     # 权限 allow / ask / deny
            Completed(needsContinuation) -> return needsContinuation
            Retry       -> retry.wait()
            Continue    -> retry.wait(); synthetic("请从中断处继续")
            Compacted   -> 上下文溢出,已压缩,重来

扩展点:在循环的哪里插手

  • tool · execute.before:工具执行前;可拒绝调用。
  • tool · execute.after:工具执行后;可改写输出。
  • permission · evaluate:权限判定时参与决策。
  • session · request 等:请求构造、压缩、标题生成等环节。
  • aisdk:改 AI SDK / provider 参数。
  • 事件总线(SSE):所有状态变化实时推给 TUI / 桌面 / IDE。

设计取舍

亮点

  • 工具边流边跑,step 状态全在数据库,崩溃后可恢复。
  • 每个 step 前后各一次 Git 快照,可精确回退到 step。
  • inbox 统一承载输入和控制项(压缩、迁移)。
  • step 上限到了强制总结,避免无限循环。

代价

  • 依赖 SQLite + 事件总线,比纯日志实现更重。
  • Effect 风格代码门槛较高。
  • core 正在重写(dev 分支),实现变化快。

OpenCode 把 agent loop 做成“数据库驱动的 step 机”:输入、状态、快照都落库,客户端只订阅事件。

来源

[1] opencode — 官方仓库
https://github.com/anomalyco/opencode
[2] opencode — SessionRunner 入口(index.ts)
https://github.com/anomalyco/opencode/blob/dev/packages/core/src/session/runner/index.ts
[3] opencode — 模型调用与循环处理(llm.ts)
https://github.com/anomalyco/opencode/blob/dev/packages/core/src/session/runner/llm.ts
[4] opencode — 输入队列(inbox.ts)
https://github.com/anomalyco/opencode/blob/dev/packages/core/src/session/inbox.ts
[5] opencode — 权限控制(permission.ts)
https://github.com/anomalyco/opencode/blob/dev/packages/core/src/permission.ts
[6] opencode — 插件包定义
https://github.com/anomalyco/opencode/blob/dev/packages/plugin/package.json

Pi

Pi 的 agent loop 是一个极简双层循环:内层在“还有工具调用或 steering 插话”时继续调模型,外层在要停时检查 follow-up。

循环只认几个回调(prepareNextTurn / prepareRequest / beforeToolCall / afterToolCall / finishTurn),压缩、重试、扩展全由外面的 AgentSession 挂上去。

循环结构

  • agentLoop() → runLoop:内层 while 处理工具和 steering,外层 while 处理 follow-up。
  • 内层条件:hasMoreToolCalls || pendingMessages.length > 0。
  • 全程用 AgentMessage,只在调模型那一刻 convertToLlm。
  • 对外是事件流:agent_start / turn / message / tool_execution_* / agent_end。

一次模型调用

  • prepareNextTurn:上一轮结束后可换上下文、模型、思考级别。
  • 注入消息前先算工具增删,写成 system 消息(toolsAdded / toolsRemoved)。
  • prepareRequest:AgentSession 从会话树投影上下文,虚拟模型在这里路由。
  • transformContext → convertToLlm → 流式调用。

工具执行

  • 默认 parallel:先逐个 prepare(校验 + beforeToolCall),再 Promise.all 并发执行。
  • 批次里只要有一个 executionMode: sequential 的工具,整批改为串行。
  • afterToolCall 可改写结果;结果按模型给出的顺序回填。
  • 所有结果都带 terminate 时,提前结束内层循环。

用户插话

  • steering:运行中插话,在每一轮之间注入(包括压缩等长准备之后再取一次)。
  • follow-up:agent 本来要停时才取,让外层再跑一圈。
  • 两个队列都可选 one-at-a-time(默认)或 all。
  • finishTurn 可返回 continue,不加新消息也再跑一轮。

压缩与失败恢复

  • stopReason 为 error / aborted:直接结束本次 run,不在循环里重试。
  • AgentSession 在 run 结束后:可重试错误 → 退避自动重试(maxRetries)。
  • 上下文溢出 → 压缩后重试;超阈值 → 压缩但不重试;轮间准备阶段也会检查阈值。
  • 输出被 length 截断:整批工具调用不执行,回错误让模型重发。

停止条件与子 agent

  • 没有工具调用、也没有 steering → 内层结束。
  • 没有 follow-up、也没有 continue 决定 → agent_end。
  • 扩展的 finishTurn 返回 end 可以随时提前结束。
  • 核心刻意不带子 agent 和 plan mode,需要就用扩展或 package 自己加。

Agent Loop 运行流程图

核心循环伪代码

*简化自 packages/agent/src/agent-loop.ts

let pending = await getSteeringMessages()
while (true) {                                      // 外层:follow-up
    let hasMoreToolCalls = true
    while (hasMoreToolCalls || pending.length > 0) {  // 内层:工具 + steering
        if (lastTurn) await prepareNextTurn(lastTurn) // 换模型 / 上下文、可压缩
        inject(declareToolChanges(ctx, pending)); pending = []
        await prepareRequest(ctx)                     // 会话投影 · 模型路由
        const msg = await streamAssistantResponse(ctx) // transformContext → convertToLlm
        if (msg.stopReason === 'error' || msg.stopReason === 'aborted') return end()
        const batch = msg.stopReason === 'length'
            ? failTruncatedToolCalls(msg)             // 截断可能截断,不执行
            : await executeToolCalls(msg)             // parallel / sequential
        hasMoreToolCalls = msg.toolCalls.length > 0 && !batch.terminate
        const d = await finishTurn(lastTurn = {msg, batch}) // end / continue
        if (d?.action === 'end') return end()
        pending = await getSteeringMessages()
    }
    const followUp = await getFollowUpMessages()
    if (followUp.length) { pending = followUp; continue }
    if (explicitContinuation) continue                // finishTurn 要求再跑一轮
    break
}
emit('agent_end')                                   // AgentSession: 重试 / 压缩

扩展点:在循环的哪里插手

  • prepareNextTurn:每轮之间;换上下文、模型、思考级别。
  • prepareRequest:请求前;投影上下文、路由模型。
  • transformContext / convertToLlm:改写上下文、转成模型消息。
  • beforeToolCall → tool_call:工具前;扩展可拦截、改参。
  • afterToolCall → tool_result:工具后;扩展可改写结果。
  • finishTurn → turn_end:每轮结束;扩展可 end / continue。
  • before_agent_start:run 开始前;可改系统提示、工具。
  • agent_before_settle:run 将停时;扩展可让它继续。

设计取舍

亮点

  • 循环不到一千行,双层结构一眼能看懂。
  • 所有策略都是回调,压缩、重试、路由都能替换。
  • 工具声明 sequential 就自动降级串行,并发安全由工具自报。
  • 截断的工具调用一律不执行,避免拿残缺参数做事。

代价

  • 重试和溢出恢复都在 run 结束后,失败要重跑整轮请求。
  • 没有内置权限审批、安全靠扩展自己写。
  • 不带子 agent、plan mode,复杂编排要自己搭。

Pi 把 agent loop 压到最小:循环只管“调模型 → 跑工具 → 收插话”,其余能力都通过回调和扩展挂上去。

来源

[1] Pi — 官方仓库
https://github.com/earendil-works/pi
[2] Pi — Agent Loop 核心实现(agent-loop.ts)
https://github.com/earendil-works/pi/blob/main/packages/agent/src/agent-loop.ts
[3] Pi — Agent 状态与调用(agent.ts)
https://github.com/earendil-works/pi/blob/main/packages/agent/src/agent.ts
[4] Pi — 会话管理(agent-session.ts)
https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/agent-session.ts
[5] Pi — 项目说明
https://github.com/earendil-works/pi/blob/main/README.md

Oh My Pi

Oh My Pi 继承 Pi 的双层循环:外层处理 follow-up,内层在“还有工具调用或插话”时持续调用模型。

它在上面加了工具并发分级、可中断工具、TTSR 流内规则、LoopGuards 纠偏和 advisor 旁路审查,重试与压缩由外面的 AgentSession 负责。

循环结构

  • agentLoop() → runLoop:外层 while 处理 follow-up,内层 while 处理工具和 steering。
  • 内层条件:hasMoreToolCalls || pendingMessages.length > 0。
  • 对外是事件流:agent_start / turn_start / message / tool_execution / turn_end / agent_end。
  • AgentSession 包在外面,负责持久化、重试、压缩、advisor 等。

一次模型调用

  • transformContext(扩展可改写)→ convertToLlm → 规范化成 provider 消息。
  • 支持的 provider 可以在流中途直接接收 live steering。
  • TTSR:流里命中规则就中止本次输出,注入规则后重来。
  • 设了 deadline 时超时即停,不再发下一次请求。

工具执行

  • 先 prepare:参数校验 + beforeToolCall 钩子(可改参 / 拒绝)。
  • concurrency:shared 并发执行,exclusive 等前面全部完成后独占。
  • afterToolCall 钩子处理结果;结果按批次顺序发出。
  • 找不到工具时,给模型提示相近的工具名。

用户插话

  • steering:运行中插话,在工具批次边界注入。
  • 可中断工具(wait 等)会被 steering 立刻打断;bash 等只收软信号,不会被杀。
  • asides:不打断的旁注(如 advisor 便签),随下一轮带上。
  • follow-up:等 agent 完全停下后再开新一轮。

失败恢复与纠偏

  • stopReason 为 error / aborted:为未完成的 toolCall 补占位结果。
  • turn-recovery:瞬时错误重试,额度受限时等待或切到备用模型链。
  • 自动压缩、自动重试在 AgentSession 层,turn 结束后检查。
  • LoopGuards:同一工具调用反复出现时,注入纠偏提示。

停止条件与子 agent

  • 没有 toolCall、也没有 steering → 内层结束。
  • 再查迟到的 steering、asides、follow-up,有就让外层再跑一圈。
  • 都没有 → agent_end。
  • task 工具派生子 agent,可隔离运行。

Agent Loop 运行流程图

核心循环伪代码

*简化自 packages/agent/src/agent-loop.ts

let pending = await getSteeringMessages()
while (true) {                                         // 外层:follow-up
    while (hasMoreToolCalls || pending.length > 0) {     // 内层:工具 + steering
        inject(pending); pending = []
        const ctx = convertToLlm(await transformContext(messages))
        const msg = await streamAssistantResponse(ctx)   // TTSR 命中 → 中止 + 注入规则 + 重来
        if (msg.stopReason === 'error' || msg.stopReason === 'aborted') {
            emitPlaceholderToolResults(msg); return      // AgentSession 负责重试
        }
        const prepared = await prepareToolCallDispatch(msg) // 校验 + beforeToolCall
        const results = await executeToolCalls(prepared)  // shared 并发 / exclusive 屏障
        emit('turn_end')                                  // 外层:LoopGuards、压缩、advisor
        hasMoreToolCalls = msg.toolCalls.length > 0
        pending = [...await getSteeringMessages(), ...asides()]
    }
    pending = [...lateSteering, ...asides(), ...await getFollowUpMessages()]
    if (pending.length === 0) break
}
emit('agent_end')

扩展点:在循环的哪里插手

  • transformContext:每次请求前改写上下文。
  • convertToLlm:把 AgentMessage 转成模型消息。
  • beforeToolCall:工具前;可改参、拒绝、附加上下文。
  • afterToolCall:工具后;可改写结果。
  • getSteering / FollowUp / Aside:三种插话队列的取数回调。
  • extensions / hooks runner:扩展可订阅全部 agent 事件。

设计取舍

亮点

  • 沿用 Pi 的双层循环,结构简单好懂。
  • 插话分软硬:等待类工具立刻打断,有副作用的工具不被硬杀。
  • TTSR、LoopGuards、advisor 把“纠偏”做进了循环。
  • shared / exclusive 让工具自己声明并发安全性。

代价

  • agent-loop.ts 约 4000 行,边界处理很多。
  • 很多能力在 AgentSession 层(约 1.3 万行),循环和会话耦合。
  • TTSR 中止重来会多花 token。

Oh My Pi 保留了 Pi“外层 follow-up + 内层工具”的简单骨架,再在流中、工具批次边界和 turn 结束三处加入纠偏能力。

来源

[1] oh my pi — 官方仓库
https://github.com/can1357/oh-my-pi
[2] oh my pi — Agent Loop 核心实现(agent-loop.ts)
https://github.com/can1357/oh-my-pi/blob/main/packages/agent/src/agent-loop.ts
[3] oh my pi — AgentSession 实现(agent-session.ts)
https://github.com/can1357/oh-my-pi/blob/main/packages/coding-agent/src/session/agent-session.ts
[4] oh my pi — 流式调用与会话机制说明
https://github.com/can1357/oh-my-pi/blob/main/docs/provider-streaming-internals.md

DeepSeek Harness

DeepSeek Harness(dsh)的 ReactLoopAgent 是一个可观察的状态机:turn → step → attempt 三层,每个边界都写进 Session 日志。

输入走两条持久队列(next-turn / next-step),压缩、目标续跑、审批都是挂在少数扩展点上的插件。

循环结构

  • driver:kick() 里 while (await turn()),有积压输入就连续开 turn。
  • 三层:turn(一次任务)→ step(一次请求 + 本次工具)→ attempt(失败重试)。
  • 每个边界都落日志:turn/start、step/start、…、step/end、turn/end。
  • 每次请求都从日志投影(Surface)构造,不在内存另存历史。

一次模型调用

  • agent/request waterfall:插件决定 provider / model / effort。
  • 系统提示、运行时上下文按需写成 system/message、user/message。
  • 流式帧实时广播(agent/assistant-stream),结束落 assistant/message。
  • 失败落 assistant/attempt,由 agent/request-error 决定是否重试。

工具执行

  • executeToolCalls:独占调用是屏障,并行调用进有界滚动池。
  • 执行可以交叠,但结果和附加上下文按模型顺序提交。
  • 调度分 prepare → dispatch → finalize,审批与钩子都落成事件。
  • 中止时未启动的调用补合成错误结果,保证回放合法。

用户插话(Inbox)

  • 两条持久队列:next-turn 与 next-step。
  • followup → next-turn 并唤醒;steer → next-step 并唤醒;inject → next-step 不唤醒。
  • 每个 step 前 preStep 领取 next-step(新 turn 再领一条 next-turn)。
  • 取消默认清空队列;keepInbox 可保留。

压缩与失败恢复

  • 压缩是插件:挂在 agent/pre-step,超阈值先压缩。
  • 请求报上下文溢出时,挂在 agent/request-error 压缩后重试。
  • 中断时保存已输出前缀(interrupted: true);失败尝试不进模型可见历史。
  • step 失败时为挂起的工具调用补 tool/result 再关闭 step。

停止条件与外层编排

  • 无 tool-call → completed;输出到上限 → max-tokens(粘性,不会被后续覆盖)。
  • 工具结果带 concludesTurn 也结束 turn;pre-step 拒绝 → blocked。
  • 将停且 next-step 为空时触发 agent/turn-stopping,插件可再塞输入。
  • 外层编排是插件:Goal(同会话续跑)与 Ralph(每轮新开子 agent)。

Agent Loop 运行流程图

核心循环伪代码

*简化自 packages/core/agent-loop/src/agent.ts

while (await this.turn()) {}                            // driver: inbox 有货就开下一个 turn
async turn() {
    log('turn/start')
    while (true) {
        const d = await preStep(target)                  // 领 inbox + agent/pre-step(压缩插件在这)
        if (d.kind === 'reject') { ends = 'blocked'; break }
        log('step/start')
        for (;;) {                                      // attempt 循环
            const cfg = await waterfall('agent/request') // 选 provider / model / effort
            msg = await stream(buildRequest(surface))   // 从日志投影出请求
            if (!failed) { log('assistant/message'); break }
            log('assistant/attempt')
            if ((await waterfall('agent/request-error'))?.kind !== 'retry') throw err
        }
        if (msg.toolCalls.length) await executeToolCalls() // 屏障 + 并行池、按序提交
        else ends = 'completed'
        log('step/end')
        if (ends && inbox.nextStep.length === 0) await serial('agent/turn-stopping')
        if (ends && inbox.nextStep.length === 0) break
        target = 'next-step'
    }
    log('turn/end', { reason: ends })
    return inbox.hasPending
}

扩展点:在循环的哪里插手

  • agent/pre-step:waterfall:每个 step 前 enter / reject,可改写输入。
  • agent/request:waterfall:决定本次请求的模型配置。
  • agent/request-error:waterfall:请求失败时决定是否重试。
  • agent/turn-stopping:serial:turn 将停时,可塞入新输入。
  • agent/inbox/*:inserted / claimed / discarded。
  • agent/status:idle ↔ running 状态切换。

设计取舍

亮点

  • 每个边界都是日志事件,崩溃、回放、fork 都能对齐到 step。
  • 状态机只开放少数 waterfall 扩展点,压缩、目标、审批全是插件。
  • 工具并行执行、按序提交,速度和可重放兼得。
  • turn 的结束原因结构化记录,便于 UI 和统计。

代价

  • 概念多(turn / step / attempt / Surface / inbox),上手成本高。
  • 行为取决于插件的 waterfall 顺序,排查要看日志。
  • 项目较新,接口仍在快速演进。

DeepSeek Harness 把 agent loop 做成了“写日志的状态机”:循环只推进 turn / step,所有策略都通过扩展点插件化。

来源

[1] DeepSeek Harness — 官方仓库
https://github.com/deepseek-ai/deepseek-harness
[2] DeepSeek Harness — Agent Loop 核心实现(agent.ts)
https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/core/agent-loop/src/agent.ts
[3] DeepSeek Harness — Agent Loop 模块说明
https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/core/agent-loop/README.md
[4] DeepSeek Harness — 上下文压缩模块
https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/compaction/README.md
[5] 不同harness的agent loop是怎么做的
https://xhslink.cn/o/8vbCd3WFQjy

最后修改:2026 年 10 月 30 日
如果觉得我的文章对你有用,请随意赞赏