OpenClaw 学习系列之六:OpenClaw 设计原理
理解这些设计决策,能帮你在阅读源码时不感到困惑。每个"反共识"都是经过工程权衡的有意选择。
OpenClaw 设计原理
理解这些设计决策,能帮你在阅读源码时不感到困惑。
每个"反共识"都是经过工程权衡的有意选择。
一、Gateway 是业务网关,不是网络网关
常见误解
听到 “Gateway” 这个词,很多人会联想到 Nginx、Kong 这样的网络网关——无状态、只做转发。
实际设计
OpenClaw 的 Gateway 是一个有状态的业务控制平面:
| 维度 | 网络网关 | OpenClaw Gateway |
|---|---|---|
| 状态 | 无状态 | 有状态(会话、记忆、配置) |
| 持久化 | 不存数据 | 持久化会话、记忆、凭据到本地 |
| 业务逻辑 | 无 | 会话管理、路由决策、工具调度、定时任务 |
| 运行方式 | 按请求 | 24x7 持续运行 |
为什么? OpenClaw 是 Local-First 架构——所有数据留在本地,不依赖云端。Gateway 不只是消息通道,它是整个系统的大脑。
代码印证:看 src/gateway/server.impl.ts 的启动逻辑——初始化的全是业务组件(channelManager、cronService、agentEventHandler),不是网络组件。
二、消息经过多层转换,不是直达 AI
常见误解
用户消息 → Gateway → AI 模型 → 回复
实际设计
消息经过 5 层转换才到达 AI 模型:
第 1 层 平台原生消息 WhatsApp/Telegram/Discord 各自的格式
↓ 渠道适配器解析
第 2 层 InboundEnvelope 统一的内部消息格式(channel, peer, content...)
↓ 路由决策
第 3 层 RouteContext agentId + sessionKey + matchedBy
↓ 上下文组装
第 4 层 AgentContext 历史对话 + 系统提示词 + 记忆 + 工具列表
↓ 格式转换
第 5 层 API 请求 ChatCompletion messages + tools(模型厂商格式)
为什么? 每层职责单一,彼此解耦:
- 新增渠道只需实现第 1→2 层的适配器
- 更换 AI 模型只需修改第 4→5 层的格式化
- 路由规则变化不影响其他层
三、会话级串行 + 全局并发控制
常见误解
“消息来了就处理,或者每条消息开一个线程。”
实际设计
OpenClaw 采用两层并发控制:
第一层:同一会话内串行
同一个用户的消息必须按顺序处理,否则会话状态会乱。代码通过 session lane 实现——同一 sessionKey 的消息进入同一队列。
第二层:全局并发度限制
跨会话可以并行,但有上限。防止同时发起太多 AI 调用导致资源耗尽或成本失控。
会话 A 的消息 ─→ [会话 A 队列(串行)] ─┐
├→ [全局调度器(并发度限制)] → Agent 执行
会话 B 的消息 ─→ [会话 B 队列(串行)] ─┘
四种队列模式
| 模式 | 行为 | 场景 |
|---|---|---|
| collect (默认) | 排队消息合并成一条后续回复 | 用户连续发了好几条 |
| steer | 立即插入当前正在进行的 Agent 回合 | 用户想纠正正在处理的问题 |
| followup | 等当前处理完再排队 | 正常的新消息 |
| steer-backlog | 转向当前回合 + 保留后续 | 复杂交互 |
关键代码:src/agents/pi-embedded-runner/run/lanes.ts、src/auto-reply/reply/queue/
四、流式响应 + 工具调用循环
常见误解
“AI 想好了一次性返回,中间调工具就等着。”
实际设计
AI 的响应是流式的(一个 token 一个 token 到达),工具调用是循环执行的:
AI 模型开始流式输出
│
├→ 文字 token → 实时推送给用户("正在输入..."效果)
│
├→ 检测到 tool_call → 暂停流 → 执行工具 → 工具结果喂回模型 → 模型继续输出
│ │
│ 可能再次调用工具 ←┘
│
└→ stop/end_turn → 输出完毕
关键点:
- 一次 Agent 回合可能包含多轮工具调用(模型决定何时停止)
- 文字 token 可以实时推送,不需要等全部生成完
- 工具执行期间,模型确实在等待——但之后可以基于工具结果继续调用更多工具
关键代码:src/agents/pi-embedded-runner/run.ts 中的 runEmbeddedAttempt()
五、组合优于继承
在其他项目中常见
class TelegramChannel extends BaseChannel {
override parseMessage() { ... }
override sendMessage() { ... }
}
OpenClaw 的做法
不用类继承,用可选适配器对象组合:
const telegram: ChannelPlugin = {
id: "telegram",
meta: { name: "Telegram", ... },
config: { listAccountIds, resolveAccount, ... }, // 可选
outbound: { send, ... }, // 可选
messaging: { onInbound, ... }, // 可选
// 不支持的能力就不提供对应适配器
};
为什么?
- 继承层次深了难以理解(“这个方法是从哪一层 override 来的?”)
- 可选适配器让能力组合更灵活——一个渠道不支持线程就不实现
threading - 测试时可以只 mock 需要的适配器
详见 01-tech-basics.md 中的"适配器组合模式"。
六、Local-First 数据架构
常见做法
聊天机器人通常把数据存在云数据库(PostgreSQL、MongoDB 等)。
OpenClaw 的做法
所有数据存在本地文件系统:
| 数据类型 | 存储方式 | 为什么不用数据库 |
|---|---|---|
| 会话记录 | JSONL 追加写 | 简单、可靠、无需数据库进程 |
| 记忆 | SQLite + 向量 | 嵌入式数据库,无需部署 |
| 配置 | JSON5 文件 | 人类可读、可编辑、可版本控制 |
| 凭据 | 文件 (0600) | 不经过网络,最小化暴露面 |
为什么?
- 用户拥有完全的数据控制权
- 部署简单——不需要额外的数据库服务
- 备份就是复制目录
- 迁移就是 rsync
代价:不支持多实例水平扩展(但这对个人 AI 助手来说不是问题)。
更多推荐

所有评论(0)