// AI 与智能体

不同Harness的Agent Loop怎么做?ClaudeCode、Codex等6套方案详解

不同Harness的Agent Loop怎么做?ClaudeCode、Codex等6套方案详解


不同Harness的Agent Loop怎么做?ClaudeCode、Codex等6套方案详解

发布时间:29 10 月, 2026 12:06 上午
来源栏目:MirrorSpeed CN
源站点:微信公众号-PaperWeekly

内容摘要

一个看似简单的 Agent Loop,真正做成 Coding Agent 有多复杂?Agent Loop 的基本流程并不复杂。模型发起工具调用,Harness

图片

一个看似简单的 Agent Loop,真正做成 Coding Agent 有多复杂?


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


    更多阅读




    ·


    作者:

    原文直达:查看原文

    用 MirrorSpeed VPN 无障碍阅读更多技术资讯镜像加速、隐私加密、全球节点。访问技术原文与海外资源更稳定。
    了解 MirrorSpeed
    用 MirrorSpeed VPN 无障碍阅读更多技术资讯镜像加速、隐私加密、全球节点。访问技术原文与海外资源更稳定。
    了解 MirrorSpeed