一次消息如何变成多步行动:拆开 OpenClaw 的 Agent Loop

前两篇分别看清了 Agent Runtime 的分层(Mission011),也拆开了 Session、Transcript、Context、Compaction、Pruning、Memory(Mission012)。本篇把这些静态概念串成一台运行中的状态机,回答问题:

一次用户消息,为什么可能产生多次模型调用、多个 Tool Call,最后才形成一个回答?

答案是:因为 OpenClaw 在外层提供了一个固定的程序循环 ,而"这一步是调工具还是收尾"由模型在每一轮里临时决定。

本文用一个"两跳读取"任务把这条循环逼出来,再用一个分支实验证明:真正决定循环长短的,是 Tool Result 的内容。


1. 先把词分清:Turn / Run / Model Call / Tool Call

概念 含义
Session 可持续多轮的逻辑会话
Turn 用户发一条消息、拿到一个最终回答的交互单元
Run OpenClaw 对一次输入执行的完整 Agent 运行(一个 runId
Model Call Agent Loop 中一次 Provider 推理请求
Tool Call 模型请求 OpenClaw 执行某个工具(带 toolCallId、工具名、参数)
Tool Result 工具执行结果(靠 toolCallId 与 Tool Call 配对)
Final Answer 循环退出前的最终文本(通常 stopReason=stop
Transcript 持久化的会话事件(JSONL)
Trajectory 运行时观测轨迹(Context / Prompt / 模型事件)

一句话记牢:一个用户 Turn ≠ 一次模型调用;一个 Run 可以包含多个 Model Call 和多个 Tool Call

官方把这条循环定义为一次按会话串行的"真实运行":接收 → 上下文组装 → 模型推理 → 工具执行 → 流式回复 → 持久化(来源:Agent loop)。本文要观察的,就是这条链路在一次任务里到底转了几圈。


2. 理论:Agent Loop 是"程序循环 + 模型决策"

sequenceDiagram autonumber participant U as 用户 participant R as Runtime participant E as Context Engine participant M as Model participant X as Tool U->>R: 一条消息(Turn 开始) loop 直到模型不再要求工具 R->>E: 组装本轮 Context(含旧 Tool Result) E->>M: 提交 Prompt alt stopReason = toolUse M-->>R: 返回 Tool Call R->>X: 校验并执行工具 X-->>R: Tool Result R->>R: 回填 Transcript,进入下一圈 else stopReason = stop M-->>R: 返回最终文本 end end R-->>U: Final Answer(Run 结束)

把它写成伪代码,结构会更直白:

python 复制代码
while True:
    context = assemble_context(system_prompt, user_prompt,
                               past_tool_calls, past_tool_results,
                               past_assistant_messages)
    response = model(context, available_tools)

    if response.has_tool_call:          # 模型选择:继续
        result = runtime.execute(response.tool_call)
        transcript.append(response.tool_call)
        transcript.append(result)
        continue
    return response.text                # 模型选择:收尾

while 循环、执行工具、回填结果、再次调用模型------这一层是代码写死的 ;而每一圈"要不要调工具、调哪个"------这一层由模型决定。职责划分是:

  • Prompt:定义任务目标、约束与分支规则;
  • Model:做语义判断、选择下一步动作;
  • Runtime:执行工具、回填结果、驱动循环与 Run 生命周期;
  • Tool:完成外部操作(读文件、执行命令等);
  • Provider :返回模型输出,并标记结束原因(toolUse / stop)。

说明:stopReason=toolUse/stop 是 Provider(本例是 openai-completions 风格)层面的字段,官方 Agent Loop 文档并未定义它;不同 Provider 或定制构建的具体字符串可能不同,应以 Transcript 实际输出为准。本文所有 stopReason 都来自实测。


3. 实验设计:两跳读取,逼出真实的多步

关键是让第二步"无法被提前预判",这样模型就不得不真的循环两圈:

  1. 先读 manifest.txt
  2. 第二个文件的路径写在 Manifest 里,只有读完第一步才知道;
  3. 再读 target.txt
  4. 根据目标文件返回 MARKER / VALUE_A / VALUE_B / SUM

因为第二个路径在第一次 Tool Result 返回后才出现,模型不可能 在第一次调用里就发出有效的第二个 Tool Call。预期循环是:read(manifest) → read(target) → 最终回答

运行环境:Provider/Model 为 deepseek-official / deepseek-v4-flash,内嵌运行时,单会话,Context 窗口 64,000,不改任何全局配置(避免再引入 Compaction/Pruning 变量)。


4. 主实验:1 个 Turn → 3 次模型调用 / 2 次工具调用

Transcript 完整记录了这条循环:

# 事件 stopReason 关键内容
1 Model Call 1 toolUse read(manifest.txt)
2 Tool Result 1 --- Manifest 暴露 TARGET_FILE=.../target.txt
3 Model Call 2 toolUse read(target.txt)
4 Tool Result 2 --- VALUE_A=17VALUE_B=25
5 Model Call 3 stop 最终回答 SUM=42

于是一个用户 Turn 实际产生了:1 个 Run、3 次 Model Call、2 次 Tool Call、2 条 Tool Result、1 条最终回答。这里有三个可直接读出的机制。

其一,stopReason 是循环的红绿灯。 toolUse 表示"模型还要调工具,循环继续";stop 表示"模型收尾,循环退出"。注意 Tool Result 本身没有 stopReason------它是工具执行结果,不是模型响应;stopReason 只属于 Assistant 消息。

其二,Tool Call 与 Tool Result 靠 toolCallId 配对。 Assistant 消息里 Tool Call 的 id,等于对应 Tool Result 的 toolCallId。本次两对都严格匹配:read(manifest)↔TR1read(target)↔TR2。这是从证据(而非模型自述)判断"谁对应谁"的唯一可靠依据。

其三,Tool Result 会重新进入下一次 Context。 模型是从第一条 Tool Result 里读到 TARGET_FILE 的值,才发出了第二个 read。也就是说,工具结果被回填进历史、参与了下一轮 Context 组装------这正是 Mission012 里"Transcript 增长 → Context 重组"在循环中的体现。

顺带一个缓存现象(Mission012 已详述,这里只作印证):三次调用的模型 input 依次是 5832 → 72 → 43,而 cacheRead8192 → 14080 → 14208------历史在涨,但重复前缀命中了 Provider 缓存,单次计费 input 很小。


5. 谁在做决定:Runtime 执行,模型选择

Runtime 不理解业务语义。它只知道"工具在技术上是否执行成功"(比如 isError=false),不会主动判断"这段结果里有目标路径,所以下一步该读它"。真正读懂 TARGET_FILE、决定再发一个 read 的,是第二次模型调用。

为了坐实"模型才是分支决策者",再做一个对照实验:规则完全相同,只改 Manifest 的内容

Manifest 里写一条决策规则:ACTION=READ_TARGET 就继续读目标文件,ACTION=ANSWER_DIRECTLY 就直接收尾。结果:

  • Continue 分支 (Manifest 写 READ_TARGET):read(manifest) → read(target) → 最终回答,共 3 次模型调用 / 2 次工具
  • Stop 分支 (Manifest 写 ANSWER_DIRECTLY):read(manifest) → 最终回答,共 2 次模型调用 / 1 次工具

规则一字未改,只有第一条 Tool Result 的内容不同,模型就走了不同的循环长度。结论很清楚:

模型在循环里扮演的是 if/else,但它是概率式的自然语言判断 ,不是确定性的程序分支;Runtime 只负责那个固定的 while 循环。整个系统 =「程序式的 Agent Loop 框架」+「模型驱动的具体行动选择」。


6. 一次 Run 的账本:usage 聚合 vs 单次;Transcript vs Trajectory

6.1 别把聚合 usage 当成单次上下文大小

运行报告里有两个 usage,含义完全不同:

  • agentMeta.usage(整个 Run 聚合):input=5947cacheRead=36480total=14275
  • lastCallUsage(仅最后一次):input=43cacheRead=14208

注意聚合值是逐次相加 的:input 5947 = 5832+72+43,cacheRead 36480 = 8192+14080+14208。也就是说,同一段被缓存的前缀,在三次调用里被重复计入了聚合 cacheRead 。所以不要用聚合 cacheRead 去估单次 Prompt 大小------单次规模要看那一次input + cacheRead (+ cacheWrite)

6.2 观察 Agent Loop 要用 Transcript,而不是 Trajectory

同一次 Run,两份记录粒度不同:

  • Transcript:完整事件序列(3 条 assistant + 2 条 toolResult),能干净还原整条循环;
  • Trajectory (本构建的 sidecar):只有 7 行 ------session.startedtrace.metadatacontext.compiledprompt.submittedmodel.completedtrace.artifactssession.ended

耐人寻味的是:context.compiled / prompt.submitted / model.completed 各只出现一次 ,尽管 Run 内实际发生了 3 次模型调用。那唯一的一次 model.completed 里,messagesSnapshot 一次性打包了全部 6 条消息(user → toolCall → toolResult → toolCall → toolResult → final)。

也就是说,本构建的 Trajectory 是一份"收尾时的总结快照",不是逐轮流水 。要逐圈观察 Agent Loop,得靠 Transcript。(官方 Agent Loop 文档描述的实时事件流是 lifecycle / assistant / tool;Trajectory sidecar 的具体形态属于构建实现,应以本机实际输出为准。)


7. Context 的机制,落在循环的哪一步

把这条循环和上一篇接起来,就能定位每个机制的位置:

  • 每次 Model Call 之前,Context 都要重新组装一遍;
  • 每条 Tool Result 之后,Transcript 增长 → Context Engine 重组 →(视配置)Pruning / 预算检查 → 提交下一次 Prompt;
  • 压力过大时 ,才进入 Compaction 或 Context Overflow Recovery。官方也说明:自动压缩会发出 compaction 事件并可能触发重试 ,重试时会重置内存缓冲与工具摘要以避免重复输出(来源:Agent loop · 压缩+重试)。

换句话说,Mission012 讲的"组装、缓存、裁剪、摘要",都是发生在这条循环里每一圈的 Context 组装阶段


8. 结论:一条固定循环,套着一个会决策的模型

  1. 一个用户 Turn ≠ 一次模型调用。本次一个 Turn = 1 Run / 3 Model Call / 2 Tool Call / 2 Tool Result / 1 最终回答。
  2. stopReason 是循环红绿灯toolUse 继续、stop 退出;Tool Result 不带 stopReason
  3. toolCallId 是配对锚点 :Tool Call 的 id = Tool Result 的 toolCallId,靠它从证据判断因果,而非听模型自述。
  4. Tool Result 会回填进下一次 Context :第二个 read 的路径,来自第一条 Tool Result。
  5. Runtime 执行、模型决策:Runtime 只判断"技术上成没成功",模型判断"语义上够不够、下一步做什么"。
  6. 模型是循环里的 if/else:同规则、不同 Tool Result → 不同循环长度;但它是概率式判断,不是确定性分支。
  7. 聚合 usage 会重复计缓存 :聚合 cacheRead 是逐次相加,不能当单次上下文大小。
  8. 观测用 Transcript:本构建的 Trajectory 只是收尾总结快照,逐圈细节在 Transcript 里。

一句话:OpenClaw 提供的是一条写死的程序循环,循环里的每一步动作则交给模型概率式地决定 。理解 Agent,就是理解这条"程序 + 模型"的分工------以及用 toolCallIdstopReason、Transcript 这些证据,而不是模型的自我描述,去还原它真正做了什么。


官方扩展阅读(简体中文)

  1. Agent loop · 智能体循环 --- 接收→组装→推理→工具执行→流式→持久化的完整链路,含队列、超时、压缩+重试与提前结束的位置。
  2. Context engine · 上下文引擎 --- 每次 Model Call 前的 Context 组装(ingest / assemble / compact / after turn)。
  3. Compaction · 压缩 --- 循环中 Context 过大时的摘要与重试。
  4. Tools · 工具 --- 可用的 Agent 工具与 Tool Call 语义。
  5. Session management · 会话管理 --- Session、Transcript 与运行记录。
相关推荐
黄华SJ520it1 小时前
甄味康商城系统开发|大健康新零售解决方案
人工智能·零售·系统开发
掘金一周1 小时前
看看大家每月的成本有多少 | 沸点周刊 7.30
前端·人工智能·后端
Zzj_tju1 小时前
如何复现一篇 LLM 论文:环境、数据、权重、seed 和日志
人工智能·自然语言处理
ZHOU_WUYI1 小时前
4. light wam 模型loss计算过程
开发语言·人工智能·python
SelectDB2 小时前
AI Agent 可观测性实战教程:用 Apache Doris 5 分钟搭建 Agent 监控系统
人工智能
尤乐娃子2 小时前
进入大厂(厂子大)实习Day5
人工智能
骄阳如火2 小时前
论文SKILLS实测系列一、nature-skills:把“中式论文腔“润色成 Nature 风格
人工智能
逻辑君2 小时前
基于 PPO 的双足机器人行走控制
人工智能·深度学习·机器人
xiaoeshuo2 小时前
产品介绍PPT模板哪家强?8个平台实测对比
人工智能