OpenClaw 设计与实现

第2章 架构总览

作者 杨艺韬 · 10,744 字 · 发布于 · 更新于

第2章 架构总览

架构的质量不取决于它能处理多少已知需求,而取决于它面对未知需求时需要修改多少代码。

本章要点

  • 跟踪一条消息从用户发送到 Agent 回复的完整旅程,理解数据流全貌
  • 掌握 OpenClaw 十二个核心子系统的职责边界与协作关系
  • 理解 Gateway 模式与 Agent Framework 模式的根本差异
  • 解读源码目录结构,为后续深入阅读建立导航地图

上一章,我们从四个递进的噩梦(通道地狱、模型困境、失忆症、安全事故)理解了为什么需要 OpenClaw——它不是又一个聊天框架,而是一套骨骼、血管与神经系统。我们还确立了五大设计哲学:通道无关、模型无关、运行时而非框架、约定优于配置、渐进式复杂度。

现在的问题是:这些哲学如何落地为具体的系统架构? 本章将视角拉升到万米高空,俯瞰系统的完整面貌。

本章的目标:读完之后,你对 OpenClaw 的每个核心子系统都有一个清晰的心智模型——不需要记住细节,但当后续章节深入某个模块时,你能立刻在全景图中定位它。

为什么这一章不可跳过:后续 16 章都是对这张全景图的局部放大。没有本章的心智地图,你在后续章节中看到的将是一堆互不关联的零件;有了它,每一个零件都有了位置和意义。这就像去一座陌生城市——先花十分钟看地图,省下的是两小时的迷路。

2.1 一条消息的完整旅程

理解一个系统最好的方式,不是阅读它的类图,而是跟踪一滴水从入口到出口的完整旅程。

让我们跟踪一条真实的用户消息。场景:一个用户在 Telegram 上发送了"帮我查一下北京今天的天气"。这条消息将穿越七个子系统,经历六次形态变换,最终以一条精确的天气信息回到用户屏幕上。

这不是一个简化的示意图——这是消息在 OpenClaw 源码中走过的真实路径。

2.1.1 第一站:通道入站(Channel Ingestion)

一句话职责:把平台特定的消息格式翻译成 OpenClaw 内部的统一格式。

Telegram 通道插件通过长轮询或 Webhook 接收到原始消息——一个包含 chat_id、from、text 等字段的 JSON 对象。通道插件的工作就是翻译:把 Telegram 的 JSON 翻译成 OpenClaw 的内部消息格式。如果消息来自 WhatsApp,翻译的是 protobuf;如果来自 Discord,翻译的是 Gateway 事件。

这就是通道无关(Channel-agnostic)原则的第一个体现——从这一步之后,系统再也不关心消息来自哪个平台。

关键概念:消息标准化(Message Normalization) 消息标准化是通道抽象的核心操作——将平台特定的消息格式(Telegram JSON、WhatsApp protobuf、Discord Gateway 事件)转换为 OpenClaw 内部的统一消息结构。这一步是实现"通道无关"的基础,所有后续子系统只需处理一种格式。

Telegram JSON → [Channel Plugin: 标准化] → OpenClaw 内部消息格式

2.1.2 第二站:路由解析(Routing)

一句话职责:根据"谁在哪个通道的哪个聊天里发的消息",确定目标会话。

路由层(src/routing/session-key.ts)将消息来源映射为一个结构化的 Session Key:

agent:main:telegram:direct:123456789
  │    │      │        │        │
  │    │      │        │        └─ 用户 ID
  │    │      │        └─ 聊天类型(直接/群组)
  │    │      └─ 通道名称
  │    └─ Agent 名称
  └─ 固定前缀

这个 Session Key 是 OpenClaw 会话系统的基石。群聊永远按「通道 + 群 ID」独立成话;私聊的粒度则由 session.dmScope 决定,而这里有个容易踩的反直觉默认——src/routing/session-key.ts:140 的兜底是 params.dmScope ?? "main",即代码默认把所有通道的所有私聊并成同一个 agent:main:main 会话。上面那个带通道名的键形只有在 dmScope 设为 per-channel-peer 时才会出现。换句话说,跨通道会话合并是默认行为,隔离才需要显式配置(src/commands/onboard-config.ts:5 的向导会替你写入 per-channel-peer,所以走过向导的部署看到的是隔离行为)。四档取值的完整对照见第 7 章。

2.1.3 第三站:会话加载(Session Management)

一句话职责:找到或创建这个用户的对话历史,准备好上下文。

会话层根据 Session Key 在会话存储里找到这个会话——存储实现在 src/config/sessions/:每个 Agent 一份 sessions.json 索引,每个会话一份 JSONL 转录文件(src/sessions/ 则放会话 ID、生命周期事件、模型覆盖等周边逻辑)。如果是新会话,创建一个空的上下文;如果是已有会话,由运行时通过 pi-coding-agent 的 SessionManager 从转录文件恢复完整的对话记录。

接下来是上下文窗口管理:Context Engine(src/context-engine/,可插拔;默认的 legacy 实现把压缩委托回内置运行时)负责上下文超出模型窗口时的处理。如果超出——比如用户已经和 Agent 聊了一整天——就会触发上下文压缩(Compaction):将较早的对话轮次摘要为简短的总结,要求保留进行中的任务、已做的决定、待办事项,并把 URL、文件名、ID 等标识符原样保留(src/agents/compaction.ts 的摘要指令),丢弃冗余的寒暄和重复内容。

2.1.4 第四站:系统提示词构建(System Prompt Assembly)

一句话职责:把 Agent 的身份、能力、约束和记忆组装成一个完整的"使命说明书"。

这是 OpenClaw 最精妙的部分之一。系统提示词不是一个静态字符串,而是 src/agents/system-prompt.ts 根据以下维度动态组装的:

维度 来源 作用
身份与人格 SOUL.md "你是谁"
工作规范 AGENTS.md "你的工作流程"
用户画像 USER.md "你在帮谁"
可用工具 工具注册表 "你能做什么"
可用技能 skills/ 目录 "你擅长什么"
安全准则 内置 Safety 段落 "你不能做什么"
时区 agents.defaults.userTimezone "现在是什么时候"(只写时区,具体时刻提示模型调 session_status 工具查)
记忆指令 记忆系统配置 "如何记住重要的事"

组装模式由 PromptMode 控制——主 Agent 使用 "full" 模式(包含所有段落),子 Agent 使用 "minimal" 模式(仅工具、工作区和运行时等少数段落),"none" 模式只保留一行基本身份。这就是渐进式复杂度(Progressive Disclosure)在提示词层面的体现。

2.1.5 第五站:模型推理(Model Invocation)

一句话职责:把组装好的消息发给 LLM,拿回流式响应。

Agent 运行时通过 Provider 层(src/agents/ 下的 models-config.providers.ts、provider-capabilities.ts、model-fallback.ts 等模块,各厂商适配位于 extensions/<厂商>/)将消息发送给 LLM。Provider 层做了三件关键的事:

  1. 选择模型:根据配置和降级策略决定用哪个模型、哪个 API Key
  2. 适配格式:Anthropic、OpenAI、Google 的 API 格式各不相同——Provider 层做统一转换
  3. 流式处理:响应按 token 流式返回,不需要等待完整响应

默认使用 Anthropic Claude(src/agents/defaults.ts):

export const DEFAULT_PROVIDER = "anthropic";
export const DEFAULT_MODEL = "claude-opus-4-6";
export const DEFAULT_CONTEXT_TOKENS = 200_000;

如果主模型不可用(速率限制、API 故障等;上下文溢出不在此列,它交给内层压缩重试、不换模型),src/agents/model-fallback.ts 会自动启动降级链——这就是模型无关(Provider-agnostic)原则在运行时的具体体现。

2.1.6 第六站:工具调用循环(Tool Execution Loop)

一句话职责:如果模型决定"我需要先做点什么才能回答",就执行工具并把结果反馈给模型。

在我们的天气查询例子中,模型返回的不是文本而是一个工具调用请求——web_search({ query: "北京今天天气" })。Agent 运行时会:

  1. 验证权限:工具在交给模型之前就已按 src/agents/tool-policy.ts 等策略过滤过(不允许的工具模型根本看不到);真正执行前还要经过 src/agents/pi-tools.before-tool-call.ts 的钩子检查,可被插件拦截或转人工审批
  2. 执行工具:调用 Web Search 工具,获取搜索结果
  3. 注入结果:把搜索结果作为 tool_result 追加到上下文
  4. 重新调用模型:带着更新的上下文再次调用 LLM

这个循环可能重复多次——模型可能先搜索天气,再搜索明天的预报,再用浏览器打开一个天气网站获取详细信息。循环在模型生成最终文本回复时终止。

关键概念:工具调用循环(Tool Execution Loop) 工具调用循环是 Agent 系统区别于传统聊天机器人的核心机制。模型不直接回答问题,而是请求执行工具(搜索、命令、浏览器操作),将工具结果注入上下文后再次推理。这个"推理→行动→观察"的循环可重复多次,直到模型积累足够信息生成最终回复。

模型响应 → 包含 tool_call → 执行工具 → 返回结果 → 模型响应 → ...
       ↓ 无 tool_call ↓
      最终文本回复

2.1.7 第七站:通道出站(Channel Egress)

一句话职责:把 Agent 的回复翻译回平台特定的格式,发送给用户。

最终的文本响应("北京今天晴,25°C,微风")沿原路返回:Agent 运行时 → Session Manager → Channel Manager → Telegram 通道插件 → Telegram API → 用户屏幕。

如果回复很长,通道插件会自动分段(Telegram 的消息长度限制是 4096 字符)。如果回复包含 Markdown,通道插件会根据平台能力决定是否渲染(Discord 支持丰富的 Markdown,WhatsApp 只支持有限的格式)。

图 2-1:一条消息的完整旅程

sequenceDiagram
    actor User as 用户 (Telegram)
    participant CH as 通道插件<br/>channels/plugins/
    participant RT as 路由层<br/>routing/session-key.ts
    participant SM as 会话管理<br/>sessions/
    participant CE as 上下文引擎<br/>context-engine/
    participant SP as 系统提示词<br/>agents/system-prompt.ts
    participant AR as Agent 运行时<br/>pi-embedded-runner/
    participant PV as Provider 层<br/>agents/ + extensions/
    participant LLM as LLM API
    User->>CH: "帮我查一下北京今天的天气"
    CH->>CH: Telegram JSON → 内部消息格式
    CH->>RT: 标准化消息 + 通道元数据
    RT->>RT: 解析 Session Key
    RT->>SM: 路由到目标会话
    SM->>CE: 加载历史 + 上下文窗口管理
    CE-->>CE: 历史超限?触发 Compaction
    SM->>SP: 请求组装系统提示词
    SP-->>AR: 返回完整 System Prompt
    AR->>PV: 发送消息载荷
    PV->>LLM: 流式 API 调用
    LLM-->>AR: tool_call: web_search("北京今天天气")
    AR->>AR: 验证权限 → 执行工具
    AR->>PV: 注入搜索结果,重新调用
    PV->>LLM: 第二轮 API 调用
    LLM-->>AR: "北京今天晴,25°C..."
    AR-->>SM: 更新会话历史
    SM-->>CH: 回传到通道
    CH-->>User: "北京今天晴,25°C..."

2.1.8 旅程的启示

从这条旅程中,我们可以提炼出三个关键的架构直觉:

  1. 消息经历了六次形态变换:平台原始格式 → 内部格式 → Session Key → 带历史的上下文 → LLM API 请求 → LLM 流式响应 → 平台回复格式。每次变换都发生在明确的模块边界上。

  2. 工具调用循环是唯一的"回路":整个系统的数据流是单向的(入站 → 处理 → 出站),只有工具调用形成循环。这个循环是 Agent 系统区别于传统 Web 应用的核心特征。

  3. 每个子系统只有一个职责:通道负责翻译,路由负责寻址,会话负责记忆,Agent 负责决策,Provider 负责适配。这种清晰的职责边界是运行时而非框架(Runtime over Framework)设计的直接结果。

🔥 深度洞察:架构即城市规划

理解 OpenClaw 的架构,最好的类比不是软件设计,而是城市规划。一座运转良好的城市不需要市长亲自指挥每辆车的行驶路线——它依靠道路网络(通道系统)、交通信号灯(路由层)、医院和学校(子系统服务)的协同运作。城市规划师的智慧不在于控制一切,而在于设计规则,让参与者能自主高效地行动。同样,OpenClaw 的架构质量不取决于它处理了多少已知场景,而取决于面对新场景时需要修改多少代码——好的架构像好的城市一样,给增长预留了空间,而不是为每一栋新建筑重新规划整条道路。

2.2 系统全景图

在跟踪了一条消息的完整旅程之后,让我们退一步,看看整个系统的全貌。OpenClaw 由十二个核心子系统组成,每个子系统用一句话可以概括:

子系统 源码路径 一句话概括
Gateway 引擎 src/gateway/ 系统的大脑——启动、编排、监控一切
Channel 管理器 src/channels/ 翻译官——把各平台的消息翻译成统一格式
Agent 运行时 src/agents/ 决策者——推理、调用工具、管理子 Agent
Provider 抽象层 src/agents/(provider-*.ts、models-config.providers.ts)+ extensions/<厂商>/ 适配器——屏蔽不同 LLM 的 API 差异
Session 管理 src/sessions/ 记忆容器——维护对话历史和会话生命周期
Context Engine src/context-engine/ 压缩器——在有限窗口内最大化信息保留
Memory 系统 extensions/memory-core/src/memory/(另有 extensions/memory-lancedb/) 长期记忆——向量搜索 + BM25 混合检索
安全系统 src/security/(安全审计)+ src/secrets/ + src/agents/sandbox/ 守门人——审计、凭证、沙箱(Gateway 连接认证在 src/gateway/auth.ts)
插件与技能 src/plugins/ + src/agents/skills/ 扩展槽——让第三方能力无缝接入
工具系统 src/process/、src/web-search/ 与 extensions/browser/ 等 四肢——执行命令、操作浏览器、搜索网页
定时任务 src/cron/ 闹钟——让 Agent 主动做事而非被动等待
设备连接 src/node-host/ + src/infra/node-pairing.ts 触手——连接手机、平板等物理设备

图 2-2:系统全景架构

flowchart TB
    subgraph Clients["🌐 外部客户端"]
        TG["📱 Telegram"]
        WA["💬 WhatsApp"]
        DC["🎮 Discord"]
        SG["🔒 Signal"]
        SK["💼 Slack"]
        WC["🖥️ Web Chat"]
        MORE["+ 13 扩展通道"]
    end
    subgraph Gateway["OpenClaw Gateway"]
        direction TB
        subgraph Ingress["入站层"]
            CM["Channel Manager<br/>翻译官"]
            RT["Routing<br/>寻址"]
        end
        subgraph Core["核心引擎"]
            SM["Session Manager<br/>记忆"]
            CE["Context Engine<br/>上下文压缩"]
            AR["Agent Runtime<br/>决策者"]
            PL["Provider Layer<br/>适配器"]
        end
        subgraph Infra["基础设施"]
            SEC["Security<br/>守门人"]
            CR["Cron<br/>闹钟"]
            MEM["Memory<br/>长期记忆"]
        end
        subgraph Extend["扩展性"]
            PLG["Plugins<br/>插件系统"]
            SKL["Skills<br/>技能系统"]
            TL["Tools<br/>四肢"]
            ND["Node Host<br/>设备连接"]
        end
    end
    Clients --> CM
    CM --> RT --> SM --> CE --> AR --> PL
    PL -->|"Claude / GPT / Gemini<br/>Ollama / DeepSeek / ..."| LLM["☁️ LLM APIs"]
    AR --> TL
    AR --> MEM
    SEC -.->|权限校验| AR
    CR -.->|定时唤醒| AR
    PLG -.->|注册通道/工具| CM
    PLG -.->|注册通道/工具| TL
    SKL -.->|技能发现| AR
    ND -.->|设备桥接| TL
    style Gateway fill:#f8f9fa,stroke:#333
    style Ingress fill:#e8f5e9,stroke:#4caf50
    style Core fill:#e3f2fd,stroke:#2196f3
    style Infra fill:#fff3e0,stroke:#ff9800
    style Extend fill:#f3e5f5,stroke:#9c27b0

2.3 为什么是 Gateway 模式而不是 Agent Framework 模式

这是 OpenClaw 最根本的架构决策——选择成为一个运行时服务而不是一个可导入的框架。理解这个决策,就理解了 OpenClaw 与 LangChain、CrewAI、AutoGPT 之间的本质差异。

"运行时 vs 框架"这个区分,是软件生态里一条很深的分界线。框架的心智模型是"你写代码、调用我"——你的应用是主、框架是辅;运行时的心智模型是"我运行、你声明"——运行时是主、你的配置和扩展是辅。这两种模式在不同场景下各有优势——框架适合"嵌入单一应用"的场景(比如把 LangChain 嵌入到一个 Jupyter notebook);运行时适合"长期常驻、多方共用、可独立运维"的场景(比如 OpenClaw 作为常驻的 AI 网关)。不少工程师在做技术选型时本能地倾向"框架"——因为"import 一个库"的心智成本更低,不需要单独部署,也不需要考虑生命周期。但当系统真正长大时,框架模式的局限性会逐渐暴露——状态丢失、独立扩缩容困难、多团队共用资源复杂。这时候,运行时模式的价值就开始体现——它从第一天起就假设"系统是长期运行的、多方共用的、需要独立管理",这些假设让它在复杂场景下更健壮。选择运行时模式,等于默认它服务的场景会变得复杂。

2.3.1 框架模式的局限

框架(如 LangChain)的核心承诺是:你写 from langchain import ...,我帮你简化与 LLM 的交互。这很好,但它隐含了一个假设——你的应用进程负责一切。你的应用进程启动时,框架初始化;你的应用进程退出时,一切状态消失。

这个假设在 Agent 场景下崩溃了:

  1. 通道需要长连接。WhatsApp Web 需要保持 WebSocket 连接,Discord Bot 需要维持 Gateway 连接。你的 Flask/FastAPI 应用不应该也不适合管理这些长连接。
  2. 会话需要跨请求持久化。Agent 的对话不是一次 HTTP 请求-响应,而是持续数小时甚至数天的有状态交互。
  3. 安全需要进程级隔离。工具执行(shell 命令、文件操作)需要进程级别的沙箱控制和审批机制。
  4. 定时任务需要持续运行。Agent 不只是等待消息再回复,还需要主动执行定时巡检、自动发送报告。

2.3.2 Gateway 模式的回答

OpenClaw 的 Gateway 是一个常驻进程,自己管理通道连接、会话状态、安全策略和定时任务。它不需要嵌入你的应用——它就是应用。

框架模式:[你的应用] → import [框架] → 调用 [LLM]
                ↑                         ↑
           你负责运维                  你负责通道适配

网关模式:[通道们] → [OpenClaw Gateway] → [LLM们]
                         ↑
                  OpenClaw 全权负责

图 2-3:框架模式 vs 网关模式

维度 框架模式 网关模式
进程生命周期 由你的应用管理 自管理 Daemon
通道连接 你自己写适配器 插件化,内置 9 + 扩展 13
会话状态 内存级,进程退出即丢 持久化存储,自动恢复
安全策略 你自己实现 安全审计 + exec 审批 + 沙箱
配置变更 重启应用 热重载(默认 hybrid,部分改动仍需重启 Gateway)
定时任务 外部 cron + 你的应用 内置调度器

代价:Gateway 模式不如框架模式灵活——你不能像调用函数一样自由组合 OpenClaw 的组件。但对于"让 AI Agent 在生产环境中持久、安全、多通道地运行"这个特定目标,Gateway 模式是更正确的抽象层级。

OpenClaw 的 VISION.md 明确将"Agent-hierarchy frameworks (manager-of-managers / nested planner trees) as a default architecture"列为暂不合并的方向。VISION.md 没有逐条给出理由;一种合理的解读是:这类架构会增加调试和审计的复杂度,与"安全优先、行为可审计"的取向相矛盾。

2.4 关键设计决策与 Trade-off

在深入各子系统之前,先纵览 OpenClaw 在六个关键架构节点上的设计选择。每一个选择都有明确的理由和代价——理解这些 trade-off,比记住任何实现细节都更重要。

"理由和代价并列"——这种架构决策记录是很多成熟团队都在用的工程实践,它有个正式的名字:ADR(Architecture Decision Record)。ADR 的核心价值不在于"说服别人我做得对",而在于"让未来的维护者理解为什么这样做"。任何架构决策在当时都是"基于当前信息做出的最合理选择"——但几年后,随着技术演化、信息变化,这个决策可能不再合理。没有 ADR,后人会困惑"为什么要用这种奇怪的方式",甚至凭直觉推翻它、引入新的问题;有 ADR,后人能理解"当初为什么这样选,现在条件变了,可以考虑重新选择"。建议你在自己的项目里养成写 ADR 的习惯。

决策维度 OpenClaw 的选择 备选方案 选择理由 代价
部署模型 Daemon 常驻进程 Serverless 长连接通道需要;会话状态需要内存一致性 不能水平扩展单实例
通道接入 插件注册表 硬编码 第三方可扩展;内置与扩展通道同等地位 间接层增加调试复杂度
Agent 运行时 自建嵌入式 使用 LangChain 等 端到端可控:提示词、工具、流式、压缩全链路 更大维护负担
配置管理 JSON5 + 热重载 YAML / 硬编码 运行时可修改;支持多层合并 类型定义分散在 37 个 types.*.ts 分模块
模型接入 Provider 适配 + Auth Profile 轮换 单一 SDK 20+ 提供商;密钥轮换和降级 每新增提供商需实现适配器
上下文管理 Context Engine + Compaction 固定窗口截断 智能压缩保留关键信息 压缩本身消耗额外 Token

💡 设计洞察:为什么选择 JSON5 而不是 YAML?

这是一个看似反直觉的选择——YAML 在 DevOps 圈几乎是配置文件的"默认标准"。但可以这样理解这个选择:YAML 的缩进敏感语法是定时炸弹。在凌晨两点调试 Agent 配置时,一个多余的空格导致整个配置解析失败,而错误信息只会告诉你"第 47 行格式错误"——这种体验让人崩溃。JSON5 保留了 JSON 的确定性解析(花括号和逗号明确界定结构),同时添加了注释和尾逗号支持(解决了 JSON 的两大痛点)。更关键的是,JSON5 是 JSON 的超集(任何合法 JSON 都是合法 JSON5),而 YAML 解析器在不同语言中的行为差异不小——yes 在某些 YAML 解析器中被解释为布尔值 true,3.0 可能被解释为浮点数而非字符串。对于一个安全敏感的系统,配置解析的确定性比配置书写的美观性重要得多。

2.5 源码目录结构导读

OpenClaw v2026.3.31 的 src/ 目录包含 51 个顶层子目录。按照功能分类:

图 2-4:源码模块全景

flowchart LR
    SRC["src/"]
    subgraph Core["核心引擎"]
        GW["gateway/<br/>网关主进程"]
        AG["agents/<br/>Agent 运行时"]
        CFG["config/<br/>配置管理"]
        SS["sessions/<br/>会话管理"]
        CTX["context-engine/<br/>上下文引擎"]
        ROU["routing/<br/>会话路由"]
    end
    subgraph Chan["通道系统"]
        CHS["channels/<br/>插件注册"]
        EXW["extensions/whatsapp/<br/>WA 实现"]
        EXL["extensions/line/<br/>LINE 实现"]
    end
    subgraph Sec["安全"]
        SEC["security/<br/>审计策略"]
        SCR["secrets/<br/>凭证管理"]
    end
    subgraph Tool["工具"]
        BR["extensions/browser/<br/>浏览器"]
        PR["process/<br/>命令执行"]
        WS["web-search/<br/>搜索"]
        TTS["tts/<br/>语音"]
    end
    subgraph Infra["基础设施"]
        CLI["cli/<br/>命令行"]
        DAE["daemon/<br/>守护进程"]
        CRN["cron/<br/>定时任务"]
        HOK["hooks/<br/>生命周期钩子"]
    end
    subgraph Ext["扩展性"]
        PLG["plugins/<br/>插件加载"]
        SDK["plugin-sdk/<br/>开发 SDK"]
        ACP["acp/<br/>ACP 桥接"]
    end
    SRC --> Core
    SRC --> Chan
    SRC --> Sec
    SRC --> Tool
    SRC --> Infra
    SRC --> Ext

2.5.1 核心引擎

目录 职责 关键文件
gateway/ 网关主进程、HTTP/WS 服务器、配置热重载 server.impl.ts、boot.ts、config-reload.ts
agents/ Agent 运行时、模型调用、工具编排 system-prompt.ts、pi-embedded-runner/、model-fallback.ts
config/ 配置加载、验证、多层合并 config.ts、types.ts(另有 37 个 types.*.ts 分模块)
sessions/ 会话元数据和生命周期 session-lifecycle-events.ts、session-id.ts
context-engine/ 上下文窗口管理和压缩 registry.ts、delegate.ts
routing/ 会话路由和 Session Key 解析 session-key.ts、account-id.ts

2.5.2 通道、安全、工具、基础设施、扩展性

分类 目录 关键职责
通道 src/channels/、extensions/whatsapp/、extensions/line/ 通道插件注册、消息标准化、出站适配
安全 security/、secrets/、agents/sandbox/ 安全审计(security/ 下 28 个源文件)、凭证管理、沙箱
工具 extensions/browser/、src/process/、src/web-search/、src/tts/、src/image-generation/ 浏览器控制、命令执行、网页搜索、语音合成、图片生成
基础设施 cli/、daemon/、cron/、hooks/、infra/、logging/ 命令行、守护进程、定时任务、生命周期钩子
扩展性 plugins/、plugin-sdk/、acp/、node-host/ 插件加载、开发 SDK(package.json 里 229 个 ./plugin-sdk/* 导出子路径,另有根入口 ./plugin-sdk)、ACP(Agent Client Protocol)桥接、设备连接

2.5.3 核心模块依赖关系

图 2-5:核心模块依赖图

flowchart TD
    CLI["cli/"] --> GW["gateway/"]
    GW --> CFG["config/"]
    GW --> CH["channels/"]
    GW --> SS["sessions/"]
    GW --> AG["agents/"]
    GW --> PLG["plugins/"]
    GW --> SEC["secrets/"]
    GW --> CR["cron/"]
    CH --> PLG
    CH --> ROU["routing/"]
    ROU --> SS
    AG --> SP["system-prompt.ts"]
    AG --> PIR["pi-embedded-runner/"]
    AG --> TP["tool-policy.ts"]
    AG --> SKL["skills/"]
    PIR --> PV["agents/provider-*.ts"]
    PIR --> CE["context-engine/"]
    PIR --> TOOLS["process/ web-search/ tts/<br/>extensions/browser/"]
    CE --> SS
    style GW fill:#e1f5fe
    style AG fill:#fff3e0
    style PIR fill:#fff3e0
    style CH fill:#e8f5e9
    style PV fill:#fce4ec

蓝色 = 网关层,橙色 = Agent 运行时层,绿色 = 通道层,粉色 = Provider 层。箭头表示依赖方向。

请注意依赖方向的设计意图:主干依赖是从上到下、从外到内的——Gateway 依赖 Agent,Agent 运行时再依赖 Provider 与上下文引擎。这只是主干,并非严格分层:src/config/、src/routing/ 也会从 src/agents/ 引入少量工具函数与常量(如 src/config/defaults.ts 引用 src/agents/defaults.ts、src/routing/resolve-route.ts 引用 src/agents/agent-scope.ts)。守住主干方向,是模块化单体保持可维护性的关键。

2.6 运行时环境

"运行时环境"这个名字朴素,但它承载的责任很重——所有的模块、并发、I/O 和错误处理,最终都依赖于运行时的行为。Node.js 22+、TypeScript ESM、端口 18789,这几项选型各有来由:较新的 Node 带来了成熟的 ESM 支持和原生 fetch,22 系列提供的编译缓存(compile cache)能加快冷启动(下文 src/entry.ts 会启用它);默认端口 18789(src/config/paths.ts 的 DEFAULT_GATEWAY_PORT)避开了 80、443、8080、3000 这类常用端口,与本机其他服务撞车的机会小。

OpenClaw 基于 Node.js 22+(兼容 Bun),使用 TypeScript ESM 模块系统。

Gateway 默认监听端口 18789,通过 HTTP 和 WebSocket 同时提供服务:

  • HTTP:Control UI(Web 管理界面)、可选开启的 OpenAI 兼容 API(/v1/chat/completions,默认关闭)、健康检查(/health、/healthz 等)
  • WebSocket:Gateway 的控制面协议——CLI、Control UI、移动端 App 和节点都通过它以"请求/响应 + 事件推送"帧调用 Gateway 方法:发消息、订阅会话与 Agent 事件、设备配对(device.pair.*)与节点配对(node.pair.*)等。src/acp/ 里的 ACP(Agent Client Protocol)是另一回事:openclaw acp 在 stdio 上对 IDE 讲 ACP,再把请求转成这条 WebSocket 上的调用

运行时环境的抽象定义在 src/runtime.ts 中:

export type RuntimeEnv = {
  log: (...args: unknown[]) => void;
  error: (...args: unknown[]) => void;
  exit: (code: number) => void;
};

这个抽象让核心逻辑可以在不同的运行上下文中复用——生产环境使用 defaultRuntime,测试环境注入不会真正退出进程的 mock runtime。

进程入口 src/entry.ts 的设计体现了 OpenClaw 对启动性能和副作用隔离的重视:

// src/entry.ts:37-58(节选:参数有简写,省略了 gaxios fetch 兼容垫片的安装)
// 仅当作为主模块运行时才执行入口逻辑
if (!isMainModule({ currentFile, wrapperEntryPairs })) {
  // Imported as a dependency — skip all entry-point side effects.
} else {
  // …省略:await installGaxiosFetchCompat();
  process.title = "openclaw";
  ensureOpenClawExecMarkerOnProcess();
  installProcessWarningFilter();
  normalizeEnv();
  if (!isTruthyEnvValue(process.env.NODE_DISABLE_COMPILE_CACHE)) {
    try {
      enableCompileCache(); // Node.js 22+ 编译缓存加速;失败也不阻塞启动
    } catch {}
  }
}

isMainModule 守卫确保 bundler 将 entry.ts 作为依赖引入时不会重复执行入口逻辑——避免"启动两个 Gateway 导致端口冲突"的问题。enableCompileCache() 利用 Node.js 22 的编译缓存加速后续启动。

2.7 模块化单体:一个精心选择的折中

OpenClaw 是一个模块化单体(Modular Monolith)——51 个顶层模块在同一个进程内运行,通过明确的接口边界解耦。

为什么不用微服务?

模块化单体:进程内函数调用 = 纳秒级延迟
微服务架构:跨服务网络调用 = 毫秒级延迟

Agent 系统会放大这个差异。一次工具调用循环包含数十次模块间调用(路由 → 会话 → Agent → 工具 → Provider → 回到 Agent)。如果每次调用都走网络,延迟会积累到不可接受的程度。

维度 模块化单体 微服务
模块间延迟 纳秒级 毫秒级
部署复杂度 一个进程 N 个服务 + 消息队列
状态共享 直接引用 需要序列化传输
适合团队规模 个人 ~ 小团队 大团队
水平扩展 受限 独立扩展各服务

对于"个人或小团队的 AI Agent 运行时"这个目标场景,模块化单体是压倒性的正确选择。这不是技术债,而是工程智慧。

💡 设计洞察:为什么是模块化单体而不是微服务?

在不少人的印象里,"微服务"几乎等同于"现代架构",选择单体看起来像是技术倒退——但放在 Agent 场景里,这是站得住的选择。考虑 Agent 的典型工作流:一条用户消息触发路由解析(1 次调用)→ 会话加载(1 次调用)→ 系统提示组装(3-5 次调用)→ 模型推理(1 次调用)→ 工具执行(1-10 次调用)→ 结果注入并再次推理(循环 2-5 轮)。一条消息的处理会涉及大量模块间调用。如果每次调用都走网络(即使是本地 gRPC),这些进程内的函数调用就都变成了网络往返——对于一个期望秒级响应的对话系统,这笔额外开销很难接受。微服务的价值在于大团队的独立部署和独立扩展,但 OpenClaw 的目标用户是个人或小团队——他们要的不是"每个组件独立扩缩容",而是"一条命令启动一切"。

如果不这样做会怎样? 想象一个微服务版的 OpenClaw:路由服务、会话服务、Agent 服务、工具服务各自独立部署。一条消息在四个服务之间来回跳转,每次跳转都要付出一次网络往返;一轮工具调用循环重复几次之后,这些往返叠加起来足以让用户明显感觉到"这个 Agent 怎么这么慢"。更糟糕的是,调试一条消息的处理需要关联四个服务的日志,开发者体验从"看一个进程的日志"变成"配置分布式追踪系统"。对于目标用户来说,这是用大炮打蚊子。

关键概念:模块化单体(Modular Monolith) 模块化单体是介于传统单体和微服务之间的架构风格——所有模块在同一进程内运行(享受纳秒级调用延迟),但通过明确的接口边界解耦(保持可维护性)。对于 Agent 系统,工具调用循环中的高频模块间调用使得进程内通信成为性能关键。

⚠️ 注意:模块化单体不等于"不考虑扩展",但也别高估 v2026.3.31 已有的扩展点:会话存储就是 session.store 指向的本地文件,源码里没有数据库会话后端或消息队列接入。它预留的是进程内的可插拔点——上下文引擎可以替换(src/context-engine/registry.ts),ACP 运行时后端可以由插件提供(src/acp/runtime/registry.ts 的 registerAcpRuntimeBackend,如 extensions/acpx/,把外部编码 Agent 作为独立进程接进来)。过早引入微服务是常见的架构错误——先确保单体的模块边界清晰,再考虑拆分;扩展点按需启用,这正是渐进式复杂度(Progressive Disclosure)在架构层面的体现。

2.8 小结:四个架构直觉

本章通过消息的完整旅程、Gateway 模式、模块化单体、配置驱动四条主线,把 OpenClaw 的整体骨架一次性摊开。四条主线都是显性的 trade-off——它们承认自己的代价,也清楚自己换来的好处。

读完本章,你应该带走四个核心直觉:

  1. 消息的旅程就是理解系统的金钥匙。 从通道入站到模型推理再到通道出站,每一步都有明确的职责边界。记住这条主线,任何子系统都不会迷路。

  2. Gateway 模式是根本选择。 选择成为常驻运行的服务而非可导入的库,决定了一切下游设计——会话持久化、通道长连接、安全隔离、配置热重载,都是这个选择的自然推论。

  3. 模块化单体是精心选择的折中。 进程内调用的纳秒级延迟在 Agent 场景中至关重要——工具调用循环的每一轮包含数十次模块间调用。

  4. 配置驱动而非代码驱动。 你可以对配置文件做版本控制、diff、审计和回滚。代码描述能力,配置描述意图。这就是约定优于配置(Convention over Configuration)的实践——常见场景用约定自动处理,特殊需求通过配置显式声明。

💡 设计洞察:为什么用文件系统而不是数据库?

OpenClaw 的会话、配置、技能、记忆全部以文件形式放在本地磁盘上——不需要 Redis、PostgreSQL 这类独立数据库服务。(例外是嵌入式的 SQLite:记忆检索索引用 Node 内置的 node:sqlite 加 sqlite-vec,默认落在状态目录的 memory/<agentId>.sqlite,见 src/agents/memory-search.ts;后台任务登记簿是 runs.sqlite,见 src/tasks/task-registry.paths.ts。它们同样只是本地文件。)这看起来像是 2005 年的技术选型,但对这个场景是合理的取舍。文件方案意味着:(1) 零依赖部署——不需要安装数据库服务、不需要管理连接池、不需要处理 schema 迁移;(2) 天然的版本控制——把配置、工作区这类文本状态放进 Git(凭证文件除外),你就有了备份、回滚和审计追踪;(3) 人类可读——调试时直接 cat 一个 JSON 文件,比写 SQL 查询快得多;(4) 工具生态——grep、jq、find 这些 Unix 工具直接可用。当然,代价是显而易见的:没有事务保证、没有并发写入控制、查询能力有限。但对于"单运营者的 Agent 运行时"这个场景,这些代价完全可以接受——你不需要数据库的并发控制,因为只有一个进程在写;你不需要复杂查询,因为访问模式几乎全是"按 key 读写"。选择最简单的能满足需求的方案,而不是最炫酷的方案——这本身就是工程智慧。

全专栏路线图:后续每一章将沿着消息的旅程逐站展开。

旅程阶段 对应章节 核心问题
消息进入系统 第 3 章 Gateway 系统如何启动、如何接收消息?
选择 AI 模型 第 4 章 Provider 用哪个模型?故障了怎么办?
加载对话上下文 第 5 章 Session 如何记住之前聊了什么?
Agent 决策与执行 第 6 章 Agent 系统 如何推理、调用工具、管理子任务?
消息到达用户 第 7-8 章 通道 如何适配不同平台的协议差异?
能力扩展 第 9-10 章 插件与工具 如何添加新能力而不改核心代码?
物理世界连接 第 11 章 Node 系统 如何控制手机等物理设备?
主动行动 第 12 章 定时任务 如何让 Agent 主动做事?
安全防护 第 13 章 安全 如何防止 Agent 做危险的事?
运维部署 第 14-16 章 CLI、部署方案、技能系统
设计提炼 第 17-18 章 设计模式与未来展望

架构的终极检验标准只有一个:当你需要添加一个从未想过的功能时,是改配置还是改代码?是加一个插件还是 fork 整个仓库?

2.9 快速上手:从零运行你的第一个 Agent

💡 本节提供从安装到第一次对话的完整步骤。如果你已经有 OpenClaw 运行环境,可以跳到第 3 章。

2.9.1 前置条件

  • Node.js 22+(推荐使用 nvm 管理版本)
  • 至少一个 LLM Provider 的 API Key(推荐 Anthropic 或 OpenAI)
  • 至少一个通道 Token(推荐 Telegram Bot Token,最容易获取)
# 检查 Node.js 版本
node --version  # package.json 要求 >=22.14.0

# 如果版本不够,使用 nvm 升级
nvm install 22
nvm use 22

⚠️ 常见陷阱:Node.js 版本不足

OpenClaw 要求 Node.js 22+,版本检查做了两道。第一道在 bin 入口——仓库根目录的 openclaw.mjs(第 8–38 行,不在 src/ 下):低于 22.12 直接写 stderr openclaw: Node.js v22.12+ is required (current: vX) 并附上 nvm 升级三连,然后以退出码 1 结束进程。第二道在 src/infra/runtime-guard.ts:12,113:常量 MIN_NODE 为 22.14.0,不满足则打印 openclaw requires Node >=22.14.0. 连同检测到的运行时与 PATH。所以你看到的不会是 SyntaxError 或 fetch is not defined 这类语言层报错,而是一条明确指路的提示——package.json 的 engines 写的是 >=22.14.0,两道闸门的阈值略有差异(bin 层更宽松),以后者为准。

2.9.2 安装

npm install -g openclaw

验证安装:

openclaw --version
# 应输出类似 OpenClaw 2026.3.31

2.9.3 首次配置

openclaw onboard --install-daemon

(openclaw setup 不带 --wizard 时只初始化配置文件和工作区、不进向导。)配置向导会依次引导你:

  1. 确认安全须知:向导先展示安全提示,要求你确认已了解 Agent 拥有真实的系统操作能力
  2. 选择 Provider:输入 Anthropic/OpenAI 的 API Key,选默认模型
  3. Gateway 设置:写入 gateway.mode: "local"、端口与认证
  4. 选择通道:输入 Telegram Bot Token(从 @BotFather 获取)

--install-daemon 让向导最后把 Gateway 装成系统服务。向导不会替你定 exec 的安全档位:tools.exec.security 不显式配置时网关/节点主机取 "allowlist"(沙箱主机上命令直接在容器内执行,不经过这一档位与审批);要固定放行范围,显式写成 "deny" / "allowlist" / "full"(详见第 13 章)。

配置文件会生成在 ~/.openclaw/openclaw.json(文件名固定在 src/config/paths.ts 的 CONFIG_FILENAME,内容按 JSON5 解析,因此注释和尾逗号都合法)。你也可以手动创建:

// ~/.openclaw/openclaw.json
{
  // Provider 凭证:写进 config 的 env 段,由运行时注入进程环境
  "env": {
    "ANTHROPIC_API_KEY": "sk-ant-..."
  },

  // 通道配置(通道都挂在 channels 下,不是顶层键)
  "channels": {
    "telegram": {
      "enabled": true,
      "botToken": "123:abc"
    }
  },

  // Agent 配置(可选,有合理默认值)
  "agents": {
    "defaults": {
      "model": { "primary": "anthropic/claude-opus-4-6" }
    }
  },

  // 本机运行 Gateway 必须显式声明,否则 `gateway run` 拒绝启动
  "gateway": { "mode": "local" }
}

⚠️ 常见陷阱:把 openclaw.json 当成严格 JSON 写

文件后缀虽然是 .json,解析走的却是 JSON5——注释、尾逗号、无引号的键都是合法的。反过来,别指望你的编辑器默认的 JSON 校验器能看懂它,openclaw.json 里的注释在严格 JSON linter 下会被报成语法错误。

// ✅ 合法:注释
{
  "channels": {
    "telegram": { "enabled": true, "botToken": "123:abc", },  // ✅ 合法:尾逗号
  },
}

⚠️ 常见陷阱:API Key 直接写在配置文件中

尽量不要把 API Key 明文留在 openclaw.json 里。配置支持 ${ENV_VAR} substitution 与 $include 引用外部文件(见 src/config/env-substitution.ts、src/config/includes.ts),也可以用 OpenClaw 的 SecretRef 密钥管理。配置文件可能被意外提交到版本控制系统,导致密钥泄露。

2.9.4 启动 Gateway

# 上一步 onboard --install-daemon 已把 Gateway 装成系统服务并启动,查看状态:
openclaw gateway status

# 若当初跳过了服务安装:先安装,再启动(start 只启动已安装的服务)
openclaw gateway install
openclaw gateway start

# 或前台运行(开发/调试用;要求配置里已有 gateway.mode=local)
openclaw gateway run

前台运行时,日志里出现类似下面几行表示启动成功(按 src/gateway/server-startup-log.ts 与 Telegram 插件的日志语句整理,前缀格式随日志设置而变,未实跑):

agent model: anthropic/claude-opus-4-6
listening on ws://127.0.0.1:18789 (PID 12345)
log file: /tmp/openclaw/openclaw-2026-04-05.log
[default] starting provider (@your_bot)

2.9.5 创建你的第一个 Agent 人格

编辑工作区(默认 ~/.openclaw/workspace/)里的 SOUL.md(初始化工作区时已放好一份模板,没有就新建):

# SOUL.md

你是一个友好的技术助手。回答简洁、准确。
当用户要求执行命令时,先确认再执行。

OpenClaw 会自动发现并注入这个文件——不需要修改任何配置(约定优于配置原则)。

2.9.6 第一次对话

打开 Telegram,给你的 Bot 发送:

你好!请帮我查一下当前时间。

Telegram 私聊默认 dmPolicy: "pairing":陌生账号第一次发消息只会收到一个配对码。在终端批准它(配对码 1 小时内有效):

openclaw pairing list telegram
openclaw pairing approve telegram <CODE>

批准后再发一次,Agent 应该会调用 session_status 工具获取时间并回复。恭喜——你的第一个 AI Agent 已经在运行了!

2.9.7 下一步

  • 添加更多通道:在配置文件中添加 Discord、Slack 等通道的 Token
  • 定制技能:在 ~/.openclaw/workspace/skills/<技能名>/ 下创建 SKILL.md 文件
  • 配置安全策略:参见第 13 章了解工具权限和沙箱配置
  • 设置定时任务:参见第 12 章让 Agent 主动工作

下一章,我们降落到地面,走进第一栋建筑——Gateway 网关引擎。如果说 OpenClaw 是一座城市,Gateway 就是市政厅:它不亲自铺路、不亲自送水,但城市的每一项公共服务都在它的调度之下运转。

思考题

  1. 概念理解:OpenClaw 的"一条消息的旅程"经过了哪些关键子系统?如果去掉其中任何一个,系统会发生什么?
  2. 实践应用:假设你需要在 OpenClaw 架构中新增一个"消息审计日志"功能,记录每条消息经过的所有处理阶段,你会在架构的哪个层次植入这个能力?
  3. 开放讨论:OpenClaw 的架构在"单体应用"和"微服务"之间做了哪些权衡?你认为这些权衡在不同规模的部署场景下是否合理?

📚 推荐阅读