对着 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、themespackages/agent-core/:可复用的 agent loop 核心src/agents/agent-tools*.ts:工具定义、参数、policy、hooksrc/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_startturn_startmessage_startmessage_updatemessage_endtool_execution_starttool_execution_updatetool_execution_endturn_endagent_end
这就很说明问题。
OpenClaw 内部不是把一次 agent 执行当成“拿到一段最终文本”来处理, 而是把它当成一条可观测的运行流来处理。
所以我后来会更愿意把智能体理解成工程对象。
因为只有把执行过程拆成事件,后面才有:
- streaming
- 中间状态展示
- tool progress
- turn 级别控制
- 出错后的恢复和收尾
4.3 assistant message 会先被流式拼起来
streamAssistantResponse(...) 做的事情也很清楚:
- 先拿当前
context.messages - 如果配置了
transformContext,先做上下文变换 - 把内部 message 转成 provider 可接收的 LLM message
- 组装出:
systemPromptmessagestools
- 调真正的
streamFunction(...) - 一边收 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(...)
工具执行之前已经过了三层关:
- 这个工具能不能被找到
- 这个参数形状合不合法
- 当前策略允不允许它执行
所以我现在越来越不愿意把 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(...) 干的就是这个事。
转完以后,一个工具至少会带这些东西:
namelabeldescriptionparametersprepareArgumentsexecutionModeexecute
这相当于把“工具能力”变成了一个统一协议。
6.2 before tool call hook 可以拦,也可以改参数
这个文件里很重要的一层是:
runBeforeToolCallHook(...)
它不只是能 veto,也能调参。
所以工具调用不是黑盒。
系统能在执行前做很多事:
- 风险拦截
- 参数修正
- code mode 下的额外处理
- 审计记录
6.3 exec 失败日志还会专门脱敏
这里有个细节我专门记了一下。
agent-tool-definition-adapter.ts 对 exec 这类工具失败日志做了专门的参数脱敏。
像:
- 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.mdCLAUDE.md- system prompt fragment
最有意思的是它会沿着目录往上找上下文文件。
它不是只读当前目录一个配置文件, 而是会把 agentDir 和 cwd 祖先路径上的 AGENTS.md / CLAUDE.md 都纳进来。
这个设计一下就把“项目上下文”落地了。
我现在再看这件事,会觉得它非常像:
系统在主动收集当前工作现场的约束、偏好和说明书。
这已经不是普通聊天历史了。
7.2 skills / prompts / themes 也不是散文件,而是资源系统
从 docs/agent-runtime-architecture.md 和 resource-loader.ts 里都能看出来,OpenClaw 对资源的理解比较统一。
它支持两种发现方式:
package.json里的openclawmanifest 明确声明- 按约定目录自动发现
这让我一下能理解为什么它后面容易长出很多能力。
因为 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. 这篇先记几个对我最有用的结论
- OpenClaw 的核心难点,不在 prompt 漂不漂亮,而在 runtime、session 和 tool system 怎么长期协同
packages/agent-core/src/agent-loop.ts是理解执行链的关键文件,agent 的推进逻辑基本都能从这里顺出来- tool call 在 OpenClaw 里不是“调用一个函数”这么简单,它前后还带着校验、hook、事件流、策略和结果回写
resource-loader.ts和session-manager.ts说明它把上下文和会话当成正式资源来管理,而不是聊天附属物- compaction、pruning、policy 这些能力看起来不花哨,但它们其实决定了 agent 能不能稳定跑久
这篇先记到这里。
后面如果继续往下挖,我大概率会优先看三块:
src/agents/embedded-agent-runner/里模型选择和 attempt loop 的细节src/llm/里 provider stream 是怎么统一起来的src/agents/sessions/tools/里具体工具是怎么落地的
