前两篇分别看清了 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 是"程序循环 + 模型决策"
把它写成伪代码,结构会更直白:
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. 实验设计:两跳读取,逼出真实的多步
关键是让第二步"无法被提前预判",这样模型就不得不真的循环两圈:
- 先读
manifest.txt; - 第二个文件的路径写在 Manifest 里,只有读完第一步才知道;
- 再读
target.txt; - 根据目标文件返回
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=17、VALUE_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)↔TR1、read(target)↔TR2。这是从证据(而非模型自述)判断"谁对应谁"的唯一可靠依据。
其三,Tool Result 会重新进入下一次 Context。 模型是从第一条 Tool Result 里读到 TARGET_FILE 的值,才发出了第二个 read。也就是说,工具结果被回填进历史、参与了下一轮 Context 组装------这正是 Mission012 里"Transcript 增长 → Context 重组"在循环中的体现。
顺带一个缓存现象(Mission012 已详述,这里只作印证):三次调用的模型 input 依次是 5832 → 72 → 43,而 cacheRead 是 8192 → 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=5947、cacheRead=36480、total=14275;lastCallUsage(仅最后一次):input=43、cacheRead=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.started、trace.metadata、context.compiled、prompt.submitted、model.completed、trace.artifacts、session.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. 结论:一条固定循环,套着一个会决策的模型
- 一个用户 Turn ≠ 一次模型调用。本次一个 Turn = 1 Run / 3 Model Call / 2 Tool Call / 2 Tool Result / 1 最终回答。
stopReason是循环红绿灯 :toolUse继续、stop退出;Tool Result 不带stopReason。toolCallId是配对锚点 :Tool Call 的id= Tool Result 的toolCallId,靠它从证据判断因果,而非听模型自述。- Tool Result 会回填进下一次 Context :第二个
read的路径,来自第一条 Tool Result。 - Runtime 执行、模型决策:Runtime 只判断"技术上成没成功",模型判断"语义上够不够、下一步做什么"。
- 模型是循环里的 if/else:同规则、不同 Tool Result → 不同循环长度;但它是概率式判断,不是确定性分支。
- 聚合 usage 会重复计缓存 :聚合
cacheRead是逐次相加,不能当单次上下文大小。 - 观测用 Transcript:本构建的 Trajectory 只是收尾总结快照,逐圈细节在 Transcript 里。
一句话:OpenClaw 提供的是一条写死的程序循环,循环里的每一步动作则交给模型概率式地决定 。理解 Agent,就是理解这条"程序 + 模型"的分工------以及用 toolCallId、stopReason、Transcript 这些证据,而不是模型的自我描述,去还原它真正做了什么。
官方扩展阅读(简体中文)
- Agent loop · 智能体循环 --- 接收→组装→推理→工具执行→流式→持久化的完整链路,含队列、超时、压缩+重试与提前结束的位置。
- Context engine · 上下文引擎 --- 每次 Model Call 前的 Context 组装(
ingest / assemble / compact / after turn)。 - Compaction · 压缩 --- 循环中 Context 过大时的摘要与重试。
- Tools · 工具 --- 可用的 Agent 工具与 Tool Call 语义。
- Session management · 会话管理 --- Session、Transcript 与运行记录。