乐于分享
好东西不私藏

OpenClaw 代码框架分析

OpenClaw 代码框架分析

  OpenClaw 代码框架分析

  整体技术栈

  - 语言: TypeScript (Node.js),monorepo 结构(pnpm workspace)

  - 核心 LLM 引擎: @mariozechner/pi-agent-core(第三方依赖)

  - 数据库: SQLite(会话、记忆持久化)

  - UI: 独立 ui/ 目录(React)

  - 多平台客户端: apps/ 下有 iOS/Android/macOS

  ---

  目录结构全景

  openclaw/

  ├── src/                        # 核心源码

  │   ├── entry.ts                # CLI 主入口

  │   ├── runtime.ts              # 输出抽象层(log/error/exit)

  │   ├── gateway/                # WebSocket/HTTP 网关服务器

  │   │   └── server-methods/

  │   │       ├── chat.ts         # 接收用户消息

  │   │       └── send.ts         # 主动发送消息

  │   ├── routing/                # 消息路由 → 决定交给哪个 Agent

  │   │   ├── resolve-route.ts    # 核心路由逻辑

  │   │   └── session-key.ts      # 会话 key 生成/解析

  │   ├── agents/                 # Agent 执行核心

  │   │   ├── agent-command.ts    # Agent 指令总调度

  │   │   ├── agent-scope.ts      # Agent 配置/ID 解析

  │   │   ├── pi-embedded-runner/ # 调用 LLM 的执行引擎

  │   │   ├── command/

  │   │   │   ├── attempt-execution.ts  # 执行一次 LLM 调用

  │   │   │   ├── session.ts            # 会话加载/初始化

  │   │   │   └── delivery.ts           # 响应投递

  │   │   └── skills/             # Skill 过滤/配置

  │   │       └── config.ts       # shouldIncludeSkill()

  │   ├── context-engine/         # 上下文组装引擎接口

  │   │   ├── types.ts            # ContextEngine 接口定义

  │   │   └── registry.ts         # 引擎注册/实例化

  │   ├── memory/                 # 记忆系统(MEMORY.md 等)

  │   │   └── root-memory-files.ts

  │   ├── config/                 # 配置类型定义

  │   │   └── types.openclaw.ts   # OpenClawConfig 主类型

  │   ├── sessions/               # 会话管理策略

  │   └── channels/               # 渠道抽象(chat-type 等)

  ├── skills/                     # 内置 Skill 脚本

  ├── packages/                   # 共享包

  │   ├── plugin-sdk/

  │   └── memory-host-sdk/

  ├── ui/                         # Web 前端

  └── apps/                       # 原生客户端(iOS/macOS 等)

  ---

  端到端例子:用户发问全流程

  用户问题(通过 Telegram 发来):"帮我查一下明天北京的天气,顺便记住我偏好用华氏度"

  Step 1 — 消息接收:Gateway 层

  文件: src/gateway/server-methods/chat.ts

  Telegram Bot → WebSocket/HTTP → chat.ts

  Gateway 收到消息后:

  1. parseMessageWithAttachments() — 解析文本 + 媒体附件

  2. loadSessionEntry() — 读取上一条会话记录(用于去重)

  3. setGatewayDedupeEntry() — 标记已接收,防止重复投递

  4. 准备好 AgentCommandOpts,传给下一步

  // chat.ts 核心结构(简化)

  const opts: AgentCommandOpts = {

    message: "帮我查一下明天北京的天气,顺便记住我偏好用华氏度",

    channel: "telegram",

    accountId: "tg-workspace-001",

    peer: { kind: "direct", id: "user-12345" },

    senderIsOwner: true,

  };

  ---

  Step 2 — 路由决策:确定交给哪个 Agent

  文件: src/routing/resolve-route.ts:610

  const route = resolveAgentRoute({

    cfg: loadedConfig,

    channel: "telegram",

    accountId: "tg-workspace-001",

    peer: { kind: "direct", id: "user-12345" },

  });

  路由器按优先级层级匹配 Binding(代码中的 tiers 数组,第 731 行):

  ┌────────┬───────────────────────┬─────────────────────────┐

  │ 优先级 │       匹配方式        │          说明           │

  ├────────┼───────────────────────┼─────────────────────────┤

  │ 1      │ binding.peer          │ 精确匹配某个用户 ID     │

  ├────────┼───────────────────────┼─────────────────────────┤

  │ 2      │ binding.peer.parent   │ 线程继承父 peer         │

  ├────────┼───────────────────────┼─────────────────────────┤

  │ 3      │ binding.peer.wildcard │ 匹配该类型所有用户      │

  ├────────┼───────────────────────┼─────────────────────────┤

  │ 4      │ binding.guild+roles   │ Discord 服务器 + 身份组 │

  ├────────┼───────────────────────┼─────────────────────────┤

  │ 5      │ binding.guild         │ Discord 服务器级别      │

  ├────────┼───────────────────────┼─────────────────────────┤

  │ 6      │ binding.account       │ 账号级别                │

  ├────────┼───────────────────────┼─────────────────────────┤

  │ 7      │ binding.channel       │ 整个渠道                │

  ├────────┼───────────────────────┼─────────────────────────┤

  │ 8      │ default               │ 兜底默认 Agent          │

  └────────┴───────────────────────┴─────────────────────────┘

  本例中,直接聊天没有特殊 Binding,走 default → 返回:

  {

    agentId: "main",

    sessionKey: "agent:main:telegram:tg-workspace-001:direct:user-12345",

    mainSessionKey: "agent:main:main",

    lastRoutePolicy: "session",

    matchedBy: "default"

  }

  ---

  Step 3 — 加载用户配置 & Agent 配置

  文件: src/agents/agent-command.ts,src/agents/agent-scope.ts

  config.json (用户全局配置)

    └── agents.list[id="main"]

          ├── model: "claude-opus-4-6"

          ├── workspaceDir: "~/.openclaw/agents/main"

          └── skills: { ... }

  此步骤:

  1. resolveAgentRuntimeConfig() — 读取该 Agent 的运行时配置(模型、provider、超时等)

  2. buildAllowedModelSet() — 确定可用模型列表

  3. resolveAgentWorkspaceDir() — 得到工作目录 ~/.openclaw/agents/main/

  4. ensureAuthProfileStore() — 加载 API Key / OAuth token

  ---

  Step 4 — 记忆加载:MEMORY.md + 历史会话

  文件: src/memory/root-memory-files.ts,src/agents/command/session.ts

  两个并行的记忆来源:

  4a. 长期记忆:MEMORY.md

  // 路径: ~/.openclaw/agents/main/MEMORY.md

  const memoryPath = resolveCanonicalRootMemoryPath(workspaceDir);

  // 内容示例:

  // - 用户名: 张三

  // - 所在城市: 北京

  // - 偏好: 使用中文回答

  // (注意:华氏度偏好这次是新的,还没记进去)

  MEMORY.md 会被直接注入到 system prompt 中。

  4b. 短期记忆:历史对话 transcript

  ~/.openclaw/sessions/

    agent:main:telegram:...:direct:user-12345/

      DAG/

        entry-001  ← 之前的对话轮次(JSON)

        entry-002

        .current   ← 指向最新 branch

  会话文件以 DAG(有向无环图)结构存储,支持分支和 compaction。

  ---

  Step 5 — 上下文组装:Context Engine

  文件: src/context-engine/types.ts:6,src/context-engine/registry.ts

  const engine = await resolveContextEngine(config);  // 根据配置选择引擎实现

  const { messages, estimatedTokens, systemPromptAddition } = await engine.assemble({

    sessionId: "...",

    messages: historicalMessages,   // 历史对话

    tokenBudget: 100_000,           // token 预算

    model: "claude-opus-4-6",

    availableTools: new Set(["web_search", "remember", "weather"]),

    citationsMode: "enabled",

  });

  组装后的 messages 大致如下:

  [system prompt]

    你是...(agent 人格)

    === MEMORY ===

    - 用户名: 张三

    - 所在城市: 北京

    - 偏好: 使用中文回答

  [历史对话]

    user: 上次问了什么...

    assistant: 上次回答了什么...

  [当前消息]

    user: 帮我查一下明天北京的天气,顺便记住我偏好用华氏度

  若 token 超预算,会触发异步 compact() — 用 LLM 对旧对话写摘要,缩减 token。

  ---

  Step 6 — Skill 过滤:确定可用工具

  文件: src/agents/skills/config.ts:73

  // shouldIncludeSkill() 对每个 skill 做检查:

  function shouldIncludeSkill({ entry, config, eligibility }) {

    // 1. 用户显式禁用了?

    if (skillConfig?.enabled === false) return false;

    // 2. 捆绑白名单限制?

    if (!isBundledSkillAllowed(entry, allowBundled)) return false;

    // 3. 运行时条件(平台/二进制/环境变量)

    return evaluateRuntimeEligibility({

      os: entry.metadata?.os,           // 如 ["macos", "linux"]

      requires: entry.metadata?.requires, // 如 ["curl"]

      hasEnv: (name) => Boolean(

        process.env[name] ||

        skillConfig?.env?.[name] ||      // config 里配了 API key

        skillConfig?.apiKey              // 直接配了 apiKey

      ),

      ...

    });

  }

  本例中,过滤后可用的 skill 包括:

  - web_search — 联网搜索(有 SERPAPI_KEY 或类似配置)

  - remember — 写入记忆

  - weather — 天气查询(若安装了此 skill)

  ---

  Step 7 — LLM 调用:Pi Embedded Runner(ReAct 循环)

  文件: src/agents/pi-embedded-runner/run.ts

  LLM 以 ReAct(Reason + Act) 模式循环执行:

  轮次 1:

    LLM 思考 → 决定调用 weather skill

    → tool_use: { name: "weather", input: { city: "北京", date: "tomorrow" } }

    → skill 执行,返回: "明天北京: 晴,最高 28°C / 82°F"

  轮次 2:

    LLM 收到天气结果,思考 → 决定调用 remember skill

    → tool_use: { name: "remember", input: { key: "temperature_unit", value: "fahrenheit" } }

    → skill 执行,将 "华氏度偏好" 写入 MEMORY.md

  轮次 3:

    LLM 已完成所有 tool call → 生成最终文字回复

    → "明天北京天气晴,最高温度 82°F(约 28°C)。已记住您偏好使用华氏度。"

  stop_reason: "end_turn"

  内部故障处理:

  - API 认证失败 → 切换下一个 provider

  - 限流(429)→ 指数退避重试

  - token 超限 → 触发 compact

  ---

  Step 8 — 记忆写回

  文件: src/agents/command/attempt-execution.ts

  // 将用户消息和 AI 回复都持久化到会话 DAG 中

  await persistTextTurnTranscript({

    sessionManager,

    userMessage: "帮我查一下明天北京的天气,顺便记住我偏好用华氏度",

    assistantReply: "明天北京天气晴,最高温度 82°F...",

    model: "claude-opus-4-6",

    usage: { inputTokens: 1240, outputTokens: 89 },

  });

  同时,remember skill 已将 华氏度偏好 写入 MEMORY.md:

  # MEMORY.md(更新后)

  - 用户名: 张三

  - 所在城市: 北京

  - 偏好: 使用中文回答

  - 温度单位偏好: 华氏度   ← 新增

  之后的每次对话,这条记忆都会被注入 system prompt。

  Context Engine afterTurn() 也会被调用,执行可选的后置维护(如更新向量索引)。

  ---

  Step 9 — 响应投递

  文件: src/agents/command/delivery.ts,src/auto-reply/dispatch.ts

  const payloads = normalizeAgentCommandReplyPayloads({ cfg, opts, payloads: runResult.payloads });

  // 根据 Binding 配置,决定回到哪个渠道

  const deliveryPlan = resolveAgentDeliveryPlan(cfg, sessionKey);

  // → { channel: "telegram", target: { kind: "direct", id: "user-12345" } }

  await deliverOutboundPayloads(payloads, deliveryPlan);

  // → Telegram Bot API: sendMessage("明天北京天气晴,最高温度 82°F...")

  ---

  全流程一图总览

  用户发消息 (Telegram)

          │

          ▼

  ┌──────────────────┐

  │  Gateway         │  chat.ts — 解析消息、去重

  └────────┬─────────┘

           │

           ▼

  ┌──────────────────┐

  │  路由层           │  resolve-route.ts — 8 级 Binding 匹配

  │                  │  → agentId="main", sessionKey="..:direct:user-12345"

  └────────┬─────────┘

           │

           ▼

  ┌──────────────────┐

  │  Agent 配置加载   │  agent-command.ts

  │                  │  → 模型、auth、workspaceDir、skill 白名单

  └────────┬─────────┘

           │

      ┌────┴────┐

      ▼         ▼

  记忆加载    历史会话

  MEMORY.md   transcript DAG

      └────┬────┘

           │

           ▼

  ┌──────────────────┐

  │  Context Engine  │  assemble()

  │                  │  → 拼装 messages[]: system+memory+history+当前问题

  │                  │  → 估算 token 数,按预算截断

  └────────┬─────────┘

           │

           ▼

  ┌──────────────────┐

  │  Skill 过滤       │  shouldIncludeSkill()

  │                  │  → [weather, web_search, remember, ...]

  └────────┬─────────┘

           │

           ▼

  ┌──────────────────┐

  │  Pi Runner       │  ReAct 循环

  │  (LLM 调用)      │  轮1: tool_use weather

  │                  │  轮2: tool_use remember → 写 MEMORY.md

  │                  │  轮3: end_turn → 生成回复文本

  └────────┬─────────┘

           │

           ▼

  ┌──────────────────┐

  │  持久化           │  persistTextTurnTranscript()

  │                  │  → 写入会话 DAG

  │                  │  → afterTurn() context engine 维护

  └────────┬─────────┘

           │

           ▼

  ┌──────────────────┐

  │  投递层           │  delivery.ts → Telegram sendMessage

  └──────────────────┘

           │

           ▼

     用户收到回复

  ---

  关键设计亮点总结

  ┌──────────────────────┬─────────────────────────────┬────────────────────────────────────────────────────────────────┐

  │         设计         │            位置             │                              说明                              │

  ├──────────────────────┼─────────────────────────────┼────────────────────────────────────────────────────────────────┤

  │ 8 级路由 Binding     │ resolve-route.ts:731        │ peer > guild+roles > guild > account > channel,精细多租户路由 │

  ├──────────────────────┼─────────────────────────────┼────────────────────────────────────────────────────────────────┤

  │ DAG 会话结构         │ sessions/                   │ 会话历史是有向无环图,支持分支和压缩,不怕 token 爆炸          │

  ├──────────────────────┼─────────────────────────────┼────────────────────────────────────────────────────────────────┤

  │ ContextEngine 接口   │ context-engine/types.ts:6   │ 可插拔设计,不同实现(向量检索/全文/纯截断)可自由替换         │

  ├──────────────────────┼─────────────────────────────┼────────────────────────────────────────────────────────────────┤

  │ Skill 动态过滤       │ skills/config.ts:73         │ 按 OS/二进制/env 变量运行时决定可用工具,不硬编码              │

  ├──────────────────────┼─────────────────────────────┼────────────────────────────────────────────────────────────────┤

  │ MEMORY.md 即记忆     │ memory/root-memory-files.ts │ 人类可读的 Markdown 就是记忆存储,每轮都注入 system prompt     │

  ├──────────────────────┼─────────────────────────────┼────────────────────────────────────────────────────────────────┤

  │ ReAct 循环           │ pi-embedded-runner/run.ts   │ Reason→Act 多轮 tool call,直到 end_turn                       │

  ├──────────────────────┼─────────────────────────────┼────────────────────────────────────────────────────────────────┤

  │ 多 provider 故障转移 │ agent-command.ts            │ auth 失败自动切下一个 provider,限流指数退避                   │

  └──────────────────────┴─────────────────────────────┴────────────────────────────────────────────────────────────────┘

  Sources:

  - https://github.com/openclaw/openclaw

  - https://github.com/liyupi/openclaw-guide

  - https://qubittool.com/zh/blog/openclaw-ai-agent-complete-guide