AI读码,OpenClaw 源码深度解析(一):规划篇 — 一条消息如何变成一次 Agent 运行
OpenClaw 源码深度解析(一):规划篇 — 一条消息如何变成一次 Agent 运行
基于开源代码 openclaw/openclaw main 分支
引言
你发了一条微信给 AI 助手:“帮我查一下明天北京的天气”。几秒后,天气信息回来了。
看起来很简单,但在 OpenClaw 的世界里,这条消息经历了一段精密的旅程:访问控制 → 群聊过滤 → 信封格式化 → 命令检测 → 调度分发 → Agent 运行 → 上下文压缩。
本文是源码深度解析系列的第一篇——规划篇。我们不深入 Agent 的推理循环(那是运行篇的事),而是聚焦于:在 Agent 开始思考之前,OpenClaw 做了哪些关键的"规划"工作?
1. 第一道关卡:你是谁?——访问控制
消息到达的第一件事,不是理解内容,而是验证身份。
src/channels/allow-from.ts 实现了一个简洁而严谨的访问控制机制:
// src/channels/allow-from.ts
export function isSenderIdAllowed(
allow: { entries: string[]; hasWildcard: boolean; hasEntries: boolean },
senderId: string | undefined,
allowWhenEmpty: boolean,
): boolean {
if (!allow.hasEntries) return allowWhenEmpty; // 默认 false — 没配置名单则拒绝
if (allow.hasWildcard) return true;
if (!senderId) return false;
return allow.entries.includes(senderId);
}
这个函数的设计哲学值得品味:
- 默认拒绝(deny by default):
allowWhenEmpty默认为false。没有配置白名单?直接拒绝。这是安全领域的铁律。 - 三层判断,短路返回:wildcard → 无 senderId → 精确匹配。逻辑清晰,没有多余的分支。
- 预计算优化:
hasWildcard和hasEntries是在解析配置时预先计算好的布尔值,避免每次请求都遍历数组。
这意味着 OpenClaw 的安全模型是白名单制的——只有被显式允许的用户才能与 Agent 交互。
2. 第二道关卡:你在跟谁说话?——群聊 @提及检测
通过了身份验证,接下来要判断:这条消息是"发给我的"还是"群聊背景噪音"?
src/channels/mention-gating.ts 解决了这个问题。不同渠道有截然不同的提及机制:
- Telegram:检测
@bot_username模式 - WhatsApp:使用 jid 引用检测
- Discord:匹配
<@!bot_id>格式
OpenClaw 没有试图用一套通用逻辑覆盖所有平台,而是为每个渠道实现了适配器。这符合适配器模式的思想——每个渠道负责自己的 mention 检测逻辑,上层只需要一个统一的布尔结果:这条消息是否在 @我?
这个设计决策很重要:群聊场景下,如果 Agent 响应每一条消息,既浪费 token 又打扰用户。mention gating 是"是否应该响应"的关键分叉点。
3. 你在哪种对话里?——会话类型归一化
确定了"该不该响应"之后,OpenClaw 需要知道"在什么语境下响应"。
src/channels/chat-type.ts 定义了四种会话类型:
// src/channels/chat-type.ts
export type ChatType = "direct" | "group" | "channel" | "thread";
export function normalizeChatType(raw?: string): ChatType | undefined {
// 将各渠道原始类型统一归一化为上述四种之一
// ...
}
四种类型覆盖了所有主流场景:
| ChatType | 含义 | 典型场景 |
|---|---|---|
direct |
私聊 | 用户和 Agent 的一对一对话 |
group |
群聊 | 微信群、Telegram group |
channel |
频道 | Discord channel、Slack channel |
thread |
帖子/线程 | Discord thread、Slack thread |
normalizeChatType 的职责是抹平渠道差异——Telegram 的 “supergroup”、Discord 的 “GUILD_TEXT”、Slack 的 “channel”,统统归一化为这四个标准类型。后续所有逻辑只依赖这个枚举,再也不需要关心底层渠道。
4. 消息穿上信封:格式化入站消息
身份验证通过、mention gating 放行、会话类型确定——现在,原始消息要被"包装"成 Agent 能理解的结构。
这是 src/auto-reply/envelope.ts 的工作。核心函数 formatInboundEnvelope() 将原始消息转换为这样的格式:
[Channel Telegram from:张三 2026-03-10T15:00:00] 帮我查天气
4.1 头部净化
信封头部包含渠道、发送者、时间等元信息。但这些元信息可能包含恶意注入。sanitizeEnvelopeHeaderPart() 负责净化:
// src/auto-reply/envelope.ts
// 净化逻辑(简化):
// 1. 折叠换行 → 单个空格
// 2. 替换方括号 [ ] 为圆括号 ( )
// 3. 合并连续空白
为什么要把 [ 换成 ( ?因为信封本身就是用方括号包裹的——[Channel ...]。如果发送者名称中包含方括号,会破坏信封的解析。这是防御性编程的典型体现。
4.2 Agent 信封
Agent 发送消息时也使用信封格式,由 formatAgentEnvelope() 处理:
// src/auto-reply/envelope.ts
export function formatAgentEnvelope(params: AgentEnvelopeParams): string {
const channel = sanitizeEnvelopeHeaderPart(params.channel?.trim() || "Channel");
const parts: string[] = [channel];
// ... 拼接 elapsed time, from, host, ip, timestamp
const header = `[${parts.join(" ")}]`;
return `${header} ${params.body}`;
}
4.3 时区与时间间隔
信封的时间戳支持多种时区配置:utc、local、user、IANA 时区字符串(如 Asia/Shanghai)。
还有一个精巧的设计:includeElapsed 参数。开启后,信封中会显示距离上一条消息的时间间隔——比如 (5m ago)。这对 Agent 理解对话节奏非常有帮助:用户隔了 5 分钟才发下一条消息,和连续快速发送,语义是完全不同的。
4.4 群聊 vs 私聊
群聊消息和私聊消息的信封格式不同:群聊带 senderLabel(标识具体是谁说的),私聊只有 from。这同样是上下文信息——在群聊中,Agent 需要知道每句话是谁说的。
5. 是命令还是对话?——命令系统
格式化完成后,OpenClaw 要判断:这条消息是自然语言对话还是系统命令?
src/auto-reply/commands-registry.types.ts 定义了命令的类型系统:
// src/auto-reply/commands-registry.types.ts
export type ChatCommandDefinition = {
key: string;
nativeName?: string;
description: string;
textAliases: string[];
acceptsArgs?: boolean;
args?: CommandArgDefinition[];
argsParsing?: CommandArgsParsing;
scope: CommandScope; // "text" | "native" | "both"
category?: CommandCategory; // "session" | "options" | "status" | ...
};
命令分两类:
- text 命令:通过纯文本匹配检测。用户输入
/new,OpenClaw 在消息文本中发现/new,触发命令。简单直接。 - native 命令:利用平台原生的斜杠命令机制(如 Telegram 的
BotCommand)。用户输入/时平台会弹出命令菜单,选择后触发。体验更好,但依赖平台支持。
scope: "both" 意味着同一个命令同时支持两种触发方式——/new 既可以手动输入,也可以从 Telegram 的命令菜单中选择。
这种双轨设计是一个务实的权衡:不是所有平台都支持 native 命令,所以 text 匹配是兜底方案;但 native 命令能提供更好的 UX,所以能用的地方就用。
6. 并发控制:谁在运行?
在 Agent 真正开始工作之前,还有一个容易被忽略但至关重要的子系统:并发状态机。
src/channels/run-state-machine.ts 用极简的代码管理并发:
// src/channels/run-state-machine.ts
export function createRunStateMachine(params) {
let activeRuns = 0;
return {
onRunStart() { activeRuns += 1; publish(); },
onRunEnd() { activeRuns = Math.max(0, activeRuns - 1); publish(); },
};
}
关键细节:activeRuns = Math.max(0, activeRuns - 1) ——用 Math.max(0) 而不是简单减一。这是防御性编程:如果因为某种异常导致 onRunEnd 被多调了一次,计数器不会变成负数。
状态机追踪当前有多少个 Agent 运行在进行中。这对于消息排队、限流、防止重复触发至关重要——你不会希望在同一个会话中同时启动 5 个 Agent 运行,互相覆盖回复。
7. 核心调度:dispatch
所有前置条件都满足了,终于进入核心调度环节。
src/auto-reply/dispatch.ts 是连接"消息处理"和"Agent 运行"的桥梁:
// src/auto-reply/dispatch.ts
export async function dispatchInboundMessage(params): Promise<DispatchInboundResult> {
const finalized = finalizeInboundContext(params.ctx);
return await withReplyDispatcher({
dispatcher: params.dispatcher,
run: () => dispatchReplyFromConfig({
ctx: finalized,
cfg: params.cfg,
dispatcher: params.dispatcher,
}),
});
}
这个函数做了两件事:
finalizeInboundContext():固化入站上下文。在规划阶段收集的所有信息(渠道、发送者、会话类型、命令解析结果等)在这一步被冻结,不再修改。withReplyDispatcher():用 try/finally 包裹实际的调度逻辑,保证 dispatcher 的资源(reservations)一定会被释放:
// src/auto-reply/dispatch.ts
export async function withReplyDispatcher<T>(params) {
try {
return await params.run();
} finally {
params.dispatcher.markComplete();
await params.dispatcher.waitForIdle();
await params.onSettled?.();
}
}
这个 finally 块是整个调度系统的安全网:无论 run() 成功、失败还是抛异常,markComplete() 都会被调用,确保 dispatcher 的状态不会泄漏。waitForIdle() 等待所有挂起的回复操作完成,onSettled?.() 执行清理回调。
这是资源管理的基本功——用 finally 保证资源释放,比在每个分支手动释放可靠得多。
8. 压缩:当上下文爆了
Agent 运行过程中,对话历史会不断增长。当上下文窗口快满了怎么办?
src/agents/pi-embedded-runner/compact.ts 实现了会话压缩。这是规划篇的最后一块拼图——虽然压缩发生在 Agent 运行过程中,但它的触发条件和参数配置是在规划阶段决定的。
压缩函数接受一个极其丰富的参数类型:
// src/agents/pi-embedded-runner/compact.ts
export type CompactEmbeddedPiSessionParams = {
sessionId: string;
sessionKey?: string;
messageChannel?: string;
messageProvider?: string;
groupId?: string | null;
senderIsOwner?: boolean;
sessionFile: string;
workspaceDir: string;
config?: OpenClawConfig;
provider?: string;
model?: string;
thinkLevel?: ThinkLevel;
trigger?: "overflow" | "manual";
// ... 30+ 参数
};
几个关键设计点:
- 触发方式:
trigger可以是"overflow"(自动,上下文快满时触发)或"manual"(用户手动触发)。 - 独立模型配置:压缩可以用不同于主对话的模型:
// src/agents/pi-embedded-runner/compact.ts
const compactionModelOverride = params.config?.agents?.defaults?.compaction?.model?.trim();
// 如果配置了压缩专用模型,用便宜的模型做压缩
这是个非常务实的优化——压缩是一项"理解 + 总结"的任务,不需要最强的推理模型。用 GPT-4o-mini 压缩、GPT-4o 对话,既省钱又不损失质量。
全景回顾:消息的生命周期
把所有环节串起来,一条消息从到达 OpenClaw 到触发 Agent 运行的完整路径:
原始消息到达
│
▼
┌─────────────────┐
│ isSenderIdAllowed() │ ← 访问控制:你是谁?允许吗?
└────────┬────────┘
│ ✅
▼
┌─────────────────┐
│ mention-gating │ ← 群聊过滤:你在 @我 吗?
└────────┬────────┘
│ ✅
▼
┌─────────────────┐
│ normalizeChatType() │ ← 归一化:私聊/群聊/频道/线程?
└────────┬────────┘
│
▼
┌─────────────────┐
│ formatInboundEnvelope()│ ← 信封:包装成 [Channel from:张三 时间] 内容
└────────┬────────┘
│
▼
┌─────────────────┐
│ 命令检测 │ ← 是 /new 命令还是自然语言?
└────────┬────────┘
│
▼
┌─────────────────┐
│ createRunStateMachine()│ ← 并发控制:有其他运行在进行吗?
└────────┬────────┘
│
▼
┌─────────────────┐
│ dispatchInboundMessage()│ ← 调度:固化上下文,启动 Agent
└────────┬────────┘
│
▼
Agent 开始运行
│
▼
┌─────────────────┐
│ compact (overflow) │ ← 上下文快满了?自动压缩
└─────────────────┘
设计哲学总结
通过阅读这些源码,我们可以提炼出 OpenClaw 规划层的几个核心设计哲学:
1. 防御性编程无处不在
sanitizeEnvelopeHeaderPart() 净化用户输入、Math.max(0, ...) 防止计数器异常、try/finally 保证资源释放——这些细节说明 OpenClaw 的开发者深知:在处理不可信输入时,每一个字节都值得怀疑。
2. 适配器模式抹平渠道差异
Telegram、WhatsApp、Discord、Slack……每个平台的数据格式都不同。OpenClaw 的策略不是发明一个万能格式,而是在入口处做归一化——normalizeChatType()、mention-gating.ts 的渠道适配器——后续逻辑只依赖标准类型。这是经典的**防腐层(Anti-Corruption Layer)**模式。
3. 配置驱动,而非硬编码
压缩模型可以独立配置(config.agents.defaults.compaction.model)、命令系统用声明式类型定义、时区支持多种模式——OpenClaw 尽量让行为通过配置控制,而不是改代码。这对于一个需要适配多种部署场景的开源项目来说,是正确的选择。
4. 关注点分离
信封格式化、命令检测、并发控制、调度分发——每个模块只做一件事,模块之间通过明确的类型接口通信。ChatCommandDefinition 的类型定义、CompactEmbeddedPiSessionParams 的 30+ 参数,都是这种"用类型做契约"的体现。
下篇预告
规划篇覆盖了 Agent 开始运行前的所有准备工作。在下一篇运行篇中,我们将深入 Agent 的推理循环:OpenClaw 如何管理 tool call、如何处理流式输出、如何在多个工具调用之间保持状态一致性。
如果你对某个模块有疑问或想深入了解,欢迎留言讨论。
本文基于 openclaw/openclaw main 分支源码。代码可能有变动,建议对照最新代码阅读。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)