Home
avatar

.𝙃𝙖𝙣

对着 OpenClaw 源码拆一遍:一次 Agent 请求是怎么跑起来的

这篇接上一篇 OpenClaw 的认知文往下写,不过这一篇不聊概念,直接看源码。

我这次给自己定的目标很简单:先不管它所有外围能力,只顺着一次 openclaw agent --message "...",把主执行链路看通。

下面这些记录,都是按我这次拉下来的仓库结构记的,重点放在三块:

  • agent runtime 在哪
  • 一次请求怎么进循环
  • tool call、session、context 是怎么被组织起来的

1. 先别被仓库体量吓住,先把要看的范围收窄

OpenClaw 这个仓库很大。

根目录里能看到很多东西:

  • apps/
  • docs/
  • packages/
  • src/
  • ui/
  • extensions/

如果一上来就全看,脑子会很散。

所以我这次先只盯几块:

src/agents/
packages/agent-core/
src/llm/
docs/agent-runtime-architecture.md

原因也很直接。

docs/agent-runtime-architecture.md 已经把内置 runtime 的边界写得比较清楚了:

  • src/agents/embedded-agent-runner/:内置 agent runtime 的尝试循环、模型选择、provider 归一化、compaction 等
  • src/agents/sessions/:session 持久化、resource loader、skills、prompts、themes
  • packages/agent-core/:可复用的 agent loop 核心
  • src/agents/agent-tools*.ts:工具定义、参数、policy、hook
  • src/llm/:模型和 provider 传输层

如果我现在只想回答“OpenClaw 怎么把一次开放任务跑起来”,先看这几块够了。

2. 命令入口怎么进来,我先顺了三层

我这次不是从 UI 看起,而是直接顺命令行。

第一层:openclaw.mjs

这个文件更像启动包装器。

它先做的事情不是跑 agent,而是:

  • 检查 Node 版本
  • 处理 compile cache
  • 处理 respawn
  • 保证当前运行环境可用

OpenClaw 的入口第一步先在解决“这玩意能不能稳定启动”。

第二层:src/entry.ts

这里开始进真正的 CLI 启动流程。

它会做几件事:

  • 处理 argv
  • 处理 profile / container 相关参数
  • 做一些环境和 warning 的初始化
  • 最后走到 runCli(...)

这一层还没到 agent 本身,但已经能看出来一件事:

OpenClaw 不是“一个 agent 函数”,它先是一个完整 CLI / Gateway 程序。

第三层:src/commands/agent.ts

这个文件很薄,基本就是一个 barrel:

export * from "../agents/agent-command.js";

真正干活的是:

src/agents/agent-command.ts

所以我后面就基本是从这个文件往下追。

3. agent-command.ts 做的第一件事,不是回答问题,而是把一次运行的地基先打好

我最开始以为,agent-command.ts 里很快就会看到 prompt 发给模型。

结果不是。

它前面先铺了很多运行时准备工作。

agentCommandInternal(...) 这一层往下看,能先看到这些关键词:

  • runtime config
  • sessionKey / sessionId
  • sessionStore
  • workspaceDir
  • runId
  • deliver
  • send policy
  • lifecycle generation
  • session admission

这说明 OpenClaw 在真正请求模型之前,先在处理这些问题:

  • 这次对话属于哪个 session
  • 这个 session 当前能不能开始新一轮工作
  • 当前回复要不要投递回外部 channel
  • 当前 run 的生命周期怎么标记
  • 当前工作目录和资源目录是什么

这里我印象最深的是 beginSessionWorkAdmission(...) 这一层。

它的意思很像:

先把这次 session 的“开工权”拿稳,别让不同动作把同一个会话的状态同时写乱。

这很关键。

因为只要 agent 不是一次性问答,而是带 session、带工具、带多轮状态的系统,这层排队和准入控制就必须先有。

所以我现在再看 agent runtime,会先把它理解成:

它先是一个 session runtime,然后才是一个模型调用器。

4. 真正的 Agent Loop,在 packages/agent-core/src/agent-loop.ts

这个文件是我这次看源码时最想盯住的一块。

因为“智能体怎么把任务一轮轮推进下去”,核心就在这里。

4.1 runLoop(...) 是总循环

我对它现在的粗理解是:

  • 外层循环负责接住 follow-up message / steering message
  • 内层循环负责处理当前这轮 assistant 输出和 tool call

大概可以记成这个样子:

用户消息进入 context
→ 模型流式生成 assistant message
→ 如果触发 toolUse,就执行 tool call
→ tool result 写回 context
→ 继续下一轮
→ 没有后续动作时结束

这个循环里我觉得很值得记的有几件事。

4.2 它不是一次 request-response,而是事件流

runLoop(...)streamAssistantResponse(...) 里,会不断发这些事件:

  • agent_start
  • turn_start
  • message_start
  • message_update
  • message_end
  • tool_execution_start
  • tool_execution_update
  • tool_execution_end
  • turn_end
  • agent_end

这就很说明问题。

OpenClaw 内部不是把一次 agent 执行当成“拿到一段最终文本”来处理, 而是把它当成一条可观测的运行流来处理。

所以我后来会更愿意把智能体理解成工程对象。

因为只有把执行过程拆成事件,后面才有:

  • streaming
  • 中间状态展示
  • tool progress
  • turn 级别控制
  • 出错后的恢复和收尾

4.3 assistant message 会先被流式拼起来

streamAssistantResponse(...) 做的事情也很清楚:

  1. 先拿当前 context.messages
  2. 如果配置了 transformContext,先做上下文变换
  3. 把内部 message 转成 provider 可接收的 LLM message
  4. 组装出:
    • systemPrompt
    • messages
    • tools
  5. 调真正的 streamFunction(...)
  6. 一边收 provider 的流,一边把 partial message 往当前上下文里更新

这里我觉得很重要的一点是:

OpenClaw 的上下文不是临时字符串拼接,而是结构化消息流先在内部走一遍,再转换成 LLM 所需格式。

这也是后面能做 tool result、custom message、branch summary、compaction 的前提。

5. tool call 真正开始之后,OpenClaw 走得比我想的细很多

这一段我看得比较慢,因为这里正好是“智能体”和“普通聊天”差异最大的地方。

agent-loop.ts 里,当 assistant message 的 stopReason === "toolUse" 时,工具执行链路才会被接上。

5.1 executeToolCalls(...) 先决定串行还是并行

这点我觉得很有意思。

OpenClaw 不是默认所有工具都一个一个执行。

它会先看:

  • 全局 config.toolExecution
  • 当前工具的 executionMode

如果工具里有必须顺序执行的,就走 sequential; 否则可以走 parallel。

它对 tool call 的理解已经不是“模型吐出 JSON,我就跑一下”了。

它已经在做调度策略。

5.2 工具真正执行前,先走 prepareToolCall(...)

这一层会依次做:

  • 解析这次 tool call 对应的是哪个 tool
  • 如果是 deferred tool,就临时解析出来
  • prepareArguments
  • validateToolArguments(...)
  • beforeToolCall(...)

工具执行之前已经过了三层关:

  1. 这个工具能不能被找到
  2. 这个参数形状合不合法
  3. 当前策略允不允许它执行

所以我现在越来越不愿意把 agent tool 简单理解成“给模型挂几个函数”。

因为真实工程里,函数只是最里面那层。

在它前面还有:

  • 解析
  • 校验
  • hook
  • policy
  • 生命周期事件

5.3 executePreparedToolCall(...) 才到真正执行

这一层才会实际调用:

prepared.tool.execute(toolCallId, args, signal, onUpdate)

同时它会把工具执行中的 partial result 继续往外发:

  • tool_execution_update

这就意味着工具不是只能“等执行完再返回”。

如果一个工具本身支持更新,它可以把中间过程不断抛出来。

这个细节我很喜欢,因为这说明 OpenClaw 的工具系统天然考虑到了:

  • 长耗时操作
  • 中间进度
  • UI / channel 侧的流式反馈

5.4 afterToolCall(...) 还能再做一次收口

工具执行完还没算彻底结束。

finalizeExecutedToolCall(...) 里还会再跑一层 afterToolCall(...)

这层可以继续决定:

  • 最终 content 怎么写
  • details 怎么保留
  • 是否终止当前批次
  • 这次结果算不算 error

最后 tool result 会被重新封成 toolResult message,再写回 context。

然后 agent loop 继续往下走下一轮。

这一步很关键。

因为它说明 OpenClaw 里工具结果不是“副作用”,而是正式进入上下文的消息

6. agent-tool-definition-adapter.ts 让我看清楚:工具在 OpenClaw 里是一个系统边界

如果说 agent-loop.ts 让我看到“工具怎么被调”, 那 agent-tool-definition-adapter.ts 让我看到“工具怎么被包装成系统可控对象”。

这个文件我看完以后,感觉很明确。

OpenClaw 并没有把工具当成随便挂上去的 callback。

它给工具包了一整层适配。

6.1 runtime tool 会先被转成 session 可执行的 ToolDefinition

toToolDefinitions(...) 干的就是这个事。

转完以后,一个工具至少会带这些东西:

  • name
  • label
  • description
  • parameters
  • prepareArguments
  • executionMode
  • execute

这相当于把“工具能力”变成了一个统一协议。

6.2 before tool call hook 可以拦,也可以改参数

这个文件里很重要的一层是:

  • runBeforeToolCallHook(...)

它不只是能 veto,也能调参。

所以工具调用不是黑盒。

系统能在执行前做很多事:

  • 风险拦截
  • 参数修正
  • code mode 下的额外处理
  • 审计记录

6.3 exec 失败日志还会专门脱敏

这里有个细节我专门记了一下。

agent-tool-definition-adapter.tsexec 这类工具失败日志做了专门的参数脱敏。

像:

  • command
  • env

这种可能带密钥的内容,不会直接原样打出去。

这个细节挺工程化的。

因为工具系统一旦接 shell、进程、网络,就很容易把敏感信息一起卷进日志。

OpenClaw 这里明显是有意识在防这个问题。

6.4 client tool 并不是本地直接跑,而是可以被“委托”

toClientToolDefinitions(...) 这块,还能看到另一个思路:

有些工具不是 runtime 直接执行,而是:

  • 先登记这次 client tool call
  • 返回一个 pending 结果
  • 把执行权交给 client

OpenClaw 的 tool 模型不是死绑在一个执行端上。

它允许:

  • host 侧工具
  • client 侧工具
  • 通过统一协议回到 agent loop

这一点其实很像我之前在想的那个问题:

智能体里的工具,不是插件列表,而是系统和外部能力之间的一层契约。

7. session、skills、AGENTS.md 这些东西怎么进上下文,我这次也顺手看了一遍

前面几篇我一直在想一个问题:

为什么智能体系统到后面,越来越像“上下文组织工程”?

OpenClaw 的 resource-loader.ts 刚好把这个问题写得很具体。

7.1 resource-loader.ts 会主动收集上下文资源

这个文件会去加载:

  • extensions
  • skills
  • prompts
  • themes
  • AGENTS.md
  • CLAUDE.md
  • system prompt fragment

最有意思的是它会沿着目录往上找上下文文件。

它不是只读当前目录一个配置文件, 而是会把 agentDir 和 cwd 祖先路径上的 AGENTS.md / CLAUDE.md 都纳进来。

这个设计一下就把“项目上下文”落地了。

我现在再看这件事,会觉得它非常像:

系统在主动收集当前工作现场的约束、偏好和说明书。

这已经不是普通聊天历史了。

7.2 skills / prompts / themes 也不是散文件,而是资源系统

docs/agent-runtime-architecture.mdresource-loader.ts 里都能看出来,OpenClaw 对资源的理解比较统一。

它支持两种发现方式:

  • package.json 里的 openclaw manifest 明确声明
  • 按约定目录自动发现

这让我一下能理解为什么它后面容易长出很多能力。

因为 skills、prompts、extensions 不是临时拼出来的,它们本来就在一个可加载资源系统里。

8. session-manager.ts 让我更确定:OpenClaw 把 session 当成“树”,不只是聊天记录

这个文件虽然长得很像基础设施代码,但我觉得它很值。

因为里面能直接看到这些动作:

  • appendMessage(...)
  • appendCompaction(...)
  • appendResetBoundary(...)
  • appendLabelChange(...)
  • branch(...)
  • branchWithSummary(...)
  • getTree()

session 在 OpenClaw 里不是“平铺日志”。

它更像:

  • 可持久化
  • 可分支
  • 可压缩
  • 可标记边界
  • 可回看结构

的会话树。

这个认知对我帮助很大。

因为我以前更容易把 session 想成“历史消息数组”。

OpenClaw 这里明显已经往前走了一层:

session 是 agent runtime 的状态容器。

只要进入:

  • 分支
  • 重试
  • compaction
  • 多轮工具执行
  • reset

这种场景,纯数组会很快不够用。

9. 我这次终于能比较具体地理解,OpenClaw 为什么一直在做 compaction、pruning、policy 这些东西

这一段其实和前一篇认知文正好能接上。

以前看智能体,最容易只盯着:

  • 模型会不会规划
  • 工具会不会调用
  • 回答聪不聪明

但对着 OpenClaw 源码看一遍后,我更在意另外几层。

9.1 context pruning 只修当前请求的内存上下文

src/agents/agent-hooks/context-pruning.ts 顶上的注释写得很直接:

  • 它影响的是当前请求的 in-memory context
  • 不会直接改写磁盘上的 session history

这个边界我觉得很稳。

因为它把“为了这轮推理减负”和“历史持久化”分开了。

9.2 compaction safeguard 在解决长对话质量问题

src/agents/agent-hooks/compaction-safeguard.ts 这块我没有逐行啃完,但大方向已经很明显了。

它不是简单做一段总结。

里面能看到很多和质量、安全、结构有关的东西:

  • structured summary
  • quality audit / repair
  • previous summary redistill
  • tool use / tool result pairing 修复
  • workspace bootstrap 文件控制

这说明 OpenClaw 已经把“长上下文怎么压缩还能不丢关键信息”当成 runtime 自带问题在处理。

9.3 tool policy 也已经是系统层能力了

agent-tools.policy.ts 也挺能说明问题。

里面会处理:

  • allow / deny
  • sandbox policy
  • sub-agent 的工具限制
  • group session / provider / scope 相关策略

尤其是 sub-agent 默认 deny 的那一串,我一看就明白了:

OpenClaw 不是把 sub-agent 当成“缩小版主 agent”, 而是在非常明确地收它的权限边界。

这一下就回到了我最关心的点上:

智能体系统真正麻烦的部分,经常不是模型,而是状态、边界、上下文、权限。

10. 如果只用一句话总结这次看源码的收获,我会这样记

我现在会把这条主链先记成下面这样:

命令入口
→ CLI / Gateway 启动
→ agent-command 准备 session / runtime / delivery
→ agent-loop 驱动多轮执行
→ streamAssistantResponse 拿到流式 assistant message
→ 命中 toolUse 后进入 tool 调度
→ tool result 回写成正式消息
→ session manager 持久化和分支管理
→ pruning / compaction / policy 负责长程稳定性

这个图一旦在脑子里立起来,OpenClaw 就不再像一个“会自己调用工具的黑盒”。

它更像是一套很完整的 runtime:

  • 前面有入口和环境启动
  • 中间有 session、context、loop、tool
  • 后面有 compaction、policy、delivery、persistence

所以我这次最大的感受反而不是“它真复杂”。

而是:

它把智能体难的地方,基本都摊开了。

11. 这篇先记几个对我最有用的结论

  1. OpenClaw 的核心难点,不在 prompt 漂不漂亮,而在 runtime、session 和 tool system 怎么长期协同
  2. packages/agent-core/src/agent-loop.ts 是理解执行链的关键文件,agent 的推进逻辑基本都能从这里顺出来
  3. tool call 在 OpenClaw 里不是“调用一个函数”这么简单,它前后还带着校验、hook、事件流、策略和结果回写
  4. resource-loader.tssession-manager.ts 说明它把上下文和会话当成正式资源来管理,而不是聊天附属物
  5. compaction、pruning、policy 这些能力看起来不花哨,但它们其实决定了 agent 能不能稳定跑久

这篇先记到这里。

后面如果继续往下挖,我大概率会优先看三块:

  • src/agents/embedded-agent-runner/ 里模型选择和 attempt loop 的细节
  • src/llm/ 里 provider stream 是怎么统一起来的
  • src/agents/sessions/tools/ 里具体工具是怎么落地的
OpenClaw 智能体 源码拆解 Agent 工具调用