跳到主要内容

数据截至 (上游 commit 8d6cbee1b527)

OpenClaw — 架构与原理

30 秒导读: OpenClaw 是一个你自己开机跑着的常驻网关进程。它同时挂在 WhatsApp、Telegram、Slack、Signal、iMessage 等 20 多个聊天应用上,把各家格式不同的消息归一成同一份数据结构,按会话键路由给一个内嵌的 AI agent;agent 调模型、调工具、动你的文件和命令行,最后把答复按每个通道各自的长度和格式限制切块发回去。整套东西——通道、模型供应商、工具、上下文引擎——都是插件。

1. 这是什么(零基础也能懂)

一句话定义: OpenClaw 是一个自托管的个人 AI 助理:你在自己已经在用的聊天软件里给它发消息,它在你自己的机器上跑模型和工具,把事办完再回你消息。

它解决的是什么问题

普通的 AI 聊天产品有两个别扭的地方:

  • 你得专门打开它的 App,而不是在你本来就一直开着的 Telegram 或 Slack 里说一句话。
  • 它跑在别人的服务器上,够不到你本机的文件、命令行、日历、笔记

OpenClaw 把这两件事反过来:助理住在你的机器上,入口是你已有的聊天软件

它能做什么

能力具体表现
多通道接入README 点名 WhatsApp / Telegram / Slack / Discord / Google Chat / Signal / iMessage 等;extensions/ 下有 26 份清单声明了 "channels"
多模型接入Anthropic、OpenAI、Google、Bedrock、Groq、DeepSeek…… 以插件形式挂进来
真动手读写文件、跑命令、开浏览器、调 MCP 工具、按技能(skill)拉起外部 CLI
多智能体一个进程里跑多个 agent,每个有自己的工作区、模型、技能集合
常驻装成系统级用户服务(macOS launchd / Linux systemd),开机自启、掉了自拉
语音与画布macOS/iOS/Android 上可说可听,还能渲染一块你能操控的实时 Canvas

依据:README.md:70(通道清单);extensions/ 下共 151 个插件目录、149 份 openclaw.plugin.json 清单;守护进程实现分别收在 src/daemon/launchd.ts:1(macOS LaunchAgent 安装与生命周期)、src/daemon/systemd.ts:1(Linux systemd)两个模块簇。

用起来什么样

装一次,跑一个向导,它就变成常驻服务:

curl -fsSL https://openclaw.ai/install.sh | bash # 安装脚本(macOS / Linux / WSL2)
openclaw onboard --install-daemon # 向导装好网关守护进程,配通道、工作区、技能

依据:README.md:27-29(安装脚本)、README.md:54(onboard 向导)。

之后你不再碰终端——直接在 Telegram 里对它说"把昨天的会议纪要整理成待办",它在你机器上跑完,把结果发回那个对话。

网关本身默认监听 127.0.0.1:18789,对外是 WebSocket + HTTP 的控制面,不是聊天界面。

依据:对外入口 startGatewayServer(src/gateway/server.ts:31)是个懒加载壳,真正实现 startGatewayServerCore 里写着默认端口 port = 18789(src/gateway/server-start.ts:23-24)。

一句话直觉

这里只是打个比方(下文不再用它当术语): 把 OpenClaw 当成一台你自己的话务总机——所有聊天软件是接进来的电话线,总机后面坐着同一个助理,不管从哪条线打进来,接电话的都是它,而且它记得你上次说到哪儿。

2. 顶层全景(它大概怎么转)

2.1 进程形态:一个常驻网关 + 一堆短命 CLI

OpenClaw 只有一个二进制 openclaw,但它扮演三种角色:

openclaw(单一 CLI 二进制)
├── openclaw onboard 一次性:向导写配置、装守护进程
├── openclaw gateway 常驻:WebSocket + HTTP 控制平面(默认 127.0.0.1:18789)
│ └─ 进程内同时装着:插件注册表 · 通道监听 · 会话存储 · agent 运行时
└── openclaw <其他子命令> 短命:多数通过 gateway-client 连到那个常驻网关

怎么读这张图: 竖着看是三条互不相同的生命周期。真正"活着"的只有中间那条——网关进程;所有业务都在它进程内。

依据:CLI 入口 src/entry.ts:35(ENTRY_WRAPPER_PAIRS 分发包装入口);网关启动 startGatewayServer(src/gateway/server.ts:31),对外协议版本常量 PROTOCOL_VERSION = 4(packages/gateway-protocol/src/version.ts:2)。

2.2 一条消息的完整旅程

这是全库的主干。从上到下是去程,每一层只干一件事,干完把结果交给下一层:

20+ 聊天应用(WhatsApp / Telegram / Slack / Signal / iMessage …)
│ 各家格式互不相同的原生事件

┌────────────────────────────────────────┐
│ ① 通道插件 │ 归一成同一份 MsgContext
└───────────────────┬────────────────────┘

┌────────────────────────────────────────┐
│ ② 安全闸 + 会话路由 │ allowlist / 群激活 → agent:<id>:<scope>
└───────────────────┬────────────────────┘

┌────────────────────────────────────────┐
│ ③ 回复流水线 │ 先当斜杠指令试;不是才交给 agent
└───────────────────┬────────────────────┘

┌────────────────────────────────────────┐
│ ④ 智能体运行时 │ 模型 ↔ 工具 反复,直到收尾
└───────────────────┬────────────────────┘

┌────────────────────────────────────────┐
│ ⑤ 分块投递 │ 按通道能力切块,交回 ① 发出去
└────────────────────────────────────────┘

怎么读这张图: 从上往下就是时间顺序,一个消息只走一遍。注意 ⑤ 又回到 ①——出站复用的是同一批通道插件,只是走 messaging 适配器而不是监听器。

2.3 部件一句话职责

部件干什么在哪个文件
MsgContext所有通道归一后的统一入站消息结构,后面每一层都读它src/auto-reply/templating.ts:110
会话键构造把「哪个 agent + 哪个对话」编码成 agent:<id>:<scope> 字符串src/routing/session-key.ts:215
dispatchInboundMessage入站总闸:定型上下文、装好投递器、进回复流水线src/auto-reply/dispatch.ts:199
getReplyFromConfig回复决策核心:选模型、开会话、走指令快路径或跑 agentsrc/auto-reply/reply/get-reply.ts:304
runEmbeddedAgent把一次回复请求变成一次真正的 agent 运行src/agents/embedded-agent-runner/run-orchestrator.ts:79
runLoopagent 内核循环本体:调模型 → 执行工具 → 喂回packages/agent-core/src/agent-loop.ts:298
ReplyDispatcher出站投递器:工具结果 / 中间块 / 最终答复三条队列src/auto-reply/reply/reply-dispatcher.types.ts:53
routeReply按通道 id 找到插件的 messaging 适配器把消息发出去src/auto-reply/reply/route-reply.ts:177
PluginRegistry插件注册总表,按能力种类分了 60 多个桶src/plugins/registry-types.ts:527
startGatewayServer起网关:HTTP/WS 监听、插件加载、通道拉起src/gateway/server.ts:31(懒加载壳)、src/gateway/server-start.ts:23(实现 startGatewayServerCore)

2.4 主线走一遍(高层,不进代码)

  1. 通道插件收到原生事件。 Telegram 收到一个 update、Signal 收到一条 envelope,格式各不相同。
  2. 归一成 MsgContext 正文、发件人、媒体、引用链、转发来源、线程信息统一到同一组字段上(src/auto-reply/templating.ts:110)。
  3. 过安全闸。 白名单判断发件人能不能用;群里还要看是不是被 @ 了才激活(src/channels/allow-from.ts:75、src/auto-reply/group-activation.ts:5)。
  4. 算会话键。 按 agent id + 通道 + 对话类型 + 对方 id,拼出 agent:main:telegram:group:123 这样的键;私聊还能按 dmScope 决定是共用一份记忆还是各自分开(src/routing/session-key.ts:215)。
  5. 进回复流水线。 先看是不是 /model/compact 这类斜杠指令或 think: 这类行内指令;是就走快路径直接回,不进模型(src/auto-reply/commands-registry.data.ts:45、src/auto-reply/reply/directives.ts:120)。
  6. 跑 agent。 不是指令,就带着会话历史、系统提示、工具集进 runLoop:调模型 → 模型要工具 → 执行 → 结果喂回 → 再调模型,直到模型不再要工具(packages/agent-core/src/agent-loop.ts:298)。
  7. 分块投递。 答复先按该通道 + 该账号的字数上限和切块模式切开再逐块发出;同一会话里的多条前台回复靠一把 FIFO 租约排队,后到的回复等先到的投递完才出场,不会交错(src/auto-reply/chunk.ts:60、src/auto-reply/dispatch.ts:51)。

3. 阅读地图(往下读哪一章)

项目很大(src/ 下几十个子系统、packages/ 21 个内部包、extensions/ 151 个插件),所以拆成六章。建议按顺序读,前三章是骨架,后三章是肉。

  1. 网关:控制平面与进程形态 —— 常驻网关到底是什么、WebSocket RPC 协议与作用域鉴权、启动时按什么顺序拉起插件和通道、守护进程怎么装。先读这章,它是所有东西的容器。
  2. 插件化内核:通道、模型商、工具都是扩展 —— openclaw.plugin.json 清单、register(api) 注册面、按能力分桶的注册表、以及"不 import 插件运行时就能算出该激活谁"的清单驱动懒激活。
  3. 入站:通道归一化、安全闸与会话路由 —— MsgContext 的字段设计、通道能力声明、白名单与群激活两道闸、agent:<id>:<scope> 会话键的构造与解析规则。
  4. 回复流水线:指令、运行队列与分块投递 —— 斜杠指令与行内指令的快路径、跟进运行队列(followup queue)、三条投递队列、按通道能力切块、前台回复的 FIFO 租约。
  5. 智能体运行时:循环、失败转移与上下文压缩 —— runLoop 的双层循环与插话消息、多模型候选链与失败转移、上下文压缩的触发阈值与摘要生成、可插拔的上下文引擎。
  6. 工具、技能与沙箱:让 agent 动手且不越界 —— 内置编码工具集的构造、按会话解析的工具策略、Docker 沙箱后端与文件系统桥、技能(SKILL.md)的加载与暴露。

4. 巧妙之处(可借鉴的技术)

4.1 会话键是一个可解析的字符串,不是数据库主键

OpenClaw 把"这是谁跟哪个 agent 的哪段对话"直接编码进一个字符串:agent:main:telegram:group:12345。好处是任何一层拿到它都能当场解析出 agent id,不用回查存储。

更妙的是私聊的粒度是可配的:dmScopemain / per-peer / per-channel-peer / per-account-channel-peer 四档,决定同一个人从不同通道找你时,是共用一份记忆还是各自独立。改一个枚举值就换了记忆模型。

依据:buildAgentPeerSessionKey(src/routing/session-key.ts:215)、parseAgentSessionKey(src/sessions/session-key-utils.ts:259)。

4.2 插件注册表按「能力」分桶,不按「插件」分桶

PluginRegistry 不是一个 plugins: Plugin[] 完事,而是把注册结果拆进 60 多个按能力命名的数组:toolschannelsprovidersspeechProvidersimageGenerationProvidersagentHarnessesgatewayHandlers……

于是核心代码永远只问"谁提供了这个能力",从不问"telegram 插件在不在"。这就是它能同时容纳 151 个插件而核心保持中立的原因。

依据:PluginRegistry(src/plugins/registry-types.ts:527);注册面 OpenClawPluginApi 上的 registerTool / registerChannel / registerGatewayMethod / registerProvider(src/plugins/plugin-api.types.ts:204、:222、:230、:271)。

4.3 清单驱动的懒激活:算激活计划时不加载插件代码

启动时把 151 个插件全 import 一遍会很慢。OpenClaw 的做法是:激活决策只读 JSON 清单,产出一份"这次该激活哪些插件 + 为什么"的计划,再去 import 那几个。

依据:resolveManifestActivationPlan 的注释明写"不 import 插件运行时模块也能给出确定的激活计划"(src/plugins/activation-planner.ts:74-75);清单示例 extensions/telegram/openclaw.plugin.json:8 里的 "activation": { "onStartup": false }

4.4 内部提示用定界符包住,出站时再剥掉——而且先转义用户内容

跑开机自检时,BOOT.md 的内容要塞进提示词,但绝不能让模型原样复读给用户。OpenClaw 用一对定界符把内部内容包起来,出站前统一剥除。

关键细节在于顺序:写入前先把用户可控内容里同名的定界符转义掉,否则用户发一句伪造的结束定界符就能把内部区域"提前关掉"。

依据:INTERNAL_RUNTIME_CONTEXT_BEGIN(src/agents/internal-runtime-context.ts:9)、escapeInternalRuntimeContextDelimiters(:36)、stripInternalRuntimeContext(:252);调用点见 src/gateway/boot.ts:45 的 buildBootPrompt

4.5 不可信内容在类型层面就被点名

群名片、群公告这类"看起来像提示词、但由用户随便写"的字段,在 MsgContext 里有专门的字段名和一行注释,直说它永远不进系统提示。把安全约束写在类型定义旁边,比写在文档里更难被后来人忽略。

依据:untrustedGroupSystemPrompt(src/auto-reply/templating.ts:96),同一结构里还有 untrustedContext 数组承载其它不可信结构化事实。

4.6 分块限制按「通道 + 账号」两级解析

Telegram 一条 4096 字、Slack 不一样、SMS 更短。OpenClaw 不写死,而是先查该通道配置下这个具体账号的 textChunkLimit,查不到再回退到通道级,再回退到默认值;切块模式(按行还是按段)走同一套回退。

这让同一个 agent 的同一句回复,在不同通道自动变成不同的分段方式。

依据:resolveTextChunkLimit(src/auto-reply/chunk.ts:60)、resolveChunkMode(:103)、chunkMarkdownTextWithMode(:295);通道自己声明能力见 ChannelCapabilities(src/channels/plugins/types.core.ts:283)。

4.7 前台回复租约:同一场对话的可见投递按 FIFO 串行

用户连发两句,两个回复运行可能几乎同时跑完。OpenClaw 给每个会话装了一把** keyed FIFO 租约**:同一场对话(通道 + 账号 + 会话键 + 对话类型 + 目标拼成的复合键)的前台回复,后到者的可见投递先 await 前一位的租约,前一位释放后才轮到它出场。

这样同一会话里不会出现"后答先到、先答后到"的交错,租约实现本身只是把每个键的尾 promise 串成链。

依据:租约注册表 foregroundReplyLeases(src/auto-reply/dispatch.ts:51)、复合键构造 resolveForegroundReplyOrderKey(:68)、投递前 await foregroundReplyLease?.wait()(:325)、收尾 release()(:364);租约原语 createKeyedFifoLeaseRegistry(src/shared/keyed-fifo-lease.ts:19)。

5. 代码地图(导航索引)

按符号名 grep 比按行号更抗上游漂移。

主题文件路径符号名
CLI 进程入口src/entry.ts顶层入口逻辑、ENTRY_WRAPPER_PAIRS
网关启动(懒加载壳)src/gateway/server.tsstartGatewayServer
网关启动(实现)src/gateway/server-start.tsstartGatewayServerCoreGatewayServerGatewayServerOptions
网关协议版本packages/gateway-protocol/src/version.tsPROTOCOL_VERSIONMIN_CLIENT_PROTOCOL_VERSION
网关权限作用域src/gateway/operator-scopes.tsOperatorScopeADMIN_SCOPE
开机自检src/gateway/boot.tsbuildBootPrompt
插件定义与注册面src/plugins/plugin-definition.types.tssrc/plugins/plugin-api.types.tsOpenClawPluginDefinitionOpenClawPluginApi
插件注册总表src/plugins/registry-types.tsPluginRegistry
插件发现 / 加载src/plugins/discovery.tssrc/plugins/loader-runtime-load.tsdiscoverOpenClawPluginsloadOpenClawPlugins
清单驱动懒激活src/plugins/activation-planner.tsresolveManifestActivationPlanPluginActivationPlan
通道插件契约src/channels/plugins/types.plugin.tsChannelPlugin
通道能力声明src/channels/plugins/types.core.tsChannelCapabilities
通道插件入口写法src/plugin-sdk/channel-entry-contract.tsextensions/telegram/index.tsdefineBundledChannelEntry
统一入站结构src/auto-reply/templating.tsMsgContextSupplementalContextFacts
发件人白名单src/channels/allow-from.tsisSenderIdAllowedmergeDmAllowFromSources
群激活模式src/auto-reply/group-activation.tsGroupActivationModenormalizeGroupActivation
会话键构造 / 解析src/routing/session-key.tssrc/sessions/session-key-utils.tsbuildAgentPeerSessionKeybuildAgentMainSessionKeyparseAgentSessionKey
入站总闸src/auto-reply/dispatch.tsdispatchInboundMessagedispatchInboundMessageWithBufferedDispatcherforegroundReplyLeases
回复决策核心src/auto-reply/reply/get-reply.tsgetReplyFromConfig
斜杠指令注册表src/auto-reply/commands-registry.data.tsgetChatCommandsdefineDockCommand
行内指令解析src/auto-reply/reply/directives.tsextractThinkDirectiveextractReasoningDirective
跟进运行队列src/auto-reply/reply/queue/enqueue.ts.../queue/drain.tsenqueueFollowupRunrememberFollowupDrainCallback
出站投递器src/auto-reply/reply/reply-dispatcher.ts.types.tscreateReplyDispatchercreateReplyDispatcherWithTypingReplyDispatcher
文本切块src/auto-reply/chunk.tsresolveTextChunkLimitresolveChunkModechunkMarkdownTextWithMode
按通道投递src/auto-reply/reply/route-reply.tsrouteReplyisRoutableChannel
agent 运行入口src/agents/embedded-agent-runner/run-orchestrator.tsrunEmbeddedAgent
agent 内核循环packages/agent-core/src/agent-loop.tsagentLooprunAgentLooprunLoopstreamAssistantResponse
模型候选链 / 失败转移src/agents/model-fallback-candidates.tssrc/agents/model-fallback-runner.tsresolveModelCandidateChainrunWithModelFallback
上下文压缩packages/agent-core/src/harness/compaction/compaction.tsshouldCompactcompactDEFAULT_COMPACTION_SETTINGS
可插拔上下文引擎src/context-engine/registry.tsregisterContextEngineInRegistrygetContextEngineRegistration
内置工具集src/agents/agent-tools.tscreateOpenClawCodingTools
工具策略src/agents/sandbox-tool-policy.tspickSandboxToolPolicy
沙箱后端注册src/agents/sandbox/backend.tssrc/agents/sandbox/docker-backend.tsregisterSandboxBackendrequireSandboxBackendFactory
内部上下文防复读src/agents/internal-runtime-context.tsINTERNAL_RUNTIME_CONTEXT_BEGINescapeInternalRuntimeContextDelimitersstripInternalRuntimeContext
技能类型与清单解析src/skills/types.tssrc/skills/loading/frontmatter.tsSkillEntrySkillSnapshot
技能样例extensions/imessage/skills/imsg/SKILL.mdfrontmatter 里的 metadata.openclaw.requires