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
夜雨聆风