乐于分享
好东西不私藏

万字拆解 OpenClaw:从 Gateway、Memory、Skills、多 Agent 到 Runtime

万字拆解 OpenClaw:从 Gateway、Memory、Skills、多 Agent 到 Runtime

AI训练营8期3月17日开班,欢迎咨询

书接上文:498 装“小龙虾”年入百万?先别信,我从工程角度为你拆解 OpenClaw

今天我们再深入一点,从源头开始,看看这个 Agent 是如何实现的,和其他 Agent 有啥不同。

但单纯按模块介绍容易显得零散,所以我们回到最底层的问题:

当用户真的发来一条消息时,OpenClaw 这个 Agent 系统内部,到底是怎么跑起来的?

这件事其实比表面上看起来更重要。

因为你只要真的把一条消息从头跟到尾,就会发现 OpenClaw 跟普通聊天机器人、传统工作流系统、以及很多只会调工具的 Agent 框架,差别并不在于它会不会聊天,而在于它背后有一整套完整的运行链路:

消息接收、协议适配、路由分发、会话隔离、上下文组装、技能注入、流式执行、工具调用、持久化存储,以及在复杂任务下的多 Agent 协作。

为了让整篇文章更容易理解,我们先假设一个典型场景:

在 钉钉 里发来一句:帮我整理今天的重要邮件,提炼待办并生成一份给老板的简报

接下来,我们就沿着这条消息,看看它是如何从外部世界的一段文本,最终变成一套真正被 Agent 执行起来的任务链路的。

希望大家带着三个问题阅读此文:

  1. OpenClaw 的整体架构设计到底是什么
  2. 一条消息在系统中的完整执行路径是什么
  3. 多 Agent 协作到底是怎么落地实现的

OpenClaw 怎么跑的

很多人第一次看 OpenClaw,很容易把它理解成一个能聊天、能调工具、还能帮你跨平台干活的智能助理。

如果从工程实现的角度看,OpenClaw 更像是一个围绕 Agent 构建出来的运行时网关系统:Agent Runtime

它不是简单地把一句用户输入丢给大模型,然后把输出再发回来,而是把整个过程拆成了一条清晰的执行链路,并在每个关键节点上做了工程治理。

它的整体架构,基本可以抽象成五层:

第 1 层:用户接口层

提供 CLI、Web UI、移动 App、WebSocket API 等入口,把用户操作转成统一的内部请求。

对用户来说,可能只是网页里输入一句话,或者在 钉钉、飞书 里发一条消息;但对系统来说,这些入口最终都要收敛成统一的内部消息模型。

第 2 层:Gateway 核心层

这是 OpenClaw 的核心运行时。它负责连接管理、请求接入、配置热加载、健康监控等基础治理工作。

换句话说,真正让整个系统常驻运行、能接消息、能回消息、能维持状态的,不是单个 Agent,而是这个 Gateway。

第 3 层:消息处理层

这是业务逻辑真正流转的核心,包括:

  1. Agent 执行器
  2. 路由系统
  3. 会话管理
  4. 媒体处理
  5. 出站投递
  6. ...

一条消息从进入系统到最终响应,最核心的执行动作都发生在这一层。

第 4 层:扩展与插件层

所有可插拔的扩展都在这里:

  1. 通道插件,对接 钉钉/飞书、Telegram、WhatsApp、Slack 等
  2. 技能工具系统
  3. sub Agent 机制

也正因为有这一层,OpenClaw 才能不断往上接新通道、往下接新工具、往内部接多 Agent 协作。

第 5 层:基础设施层

这一层为整个系统提供通用能力,包括:

  1. 配置与密钥管理
  2. 结构化日志
  3. 定时任务
  4. 事件总线
  5. 记忆检索
  6. 沙箱安全

这部分平时不太显眼,但没有它,前面几层都跑不稳。

从数据流转的视角看,一条消息的完整路径其实很清楚:

消息源 → 协议适配 → 路由分发 → 会话构建 → Agent 执行 → 响应投递 → 状态持久化

所以接下来,我们就沿着这条路径往下看。

消息进门

先回到刚才那个例子。

帮我整理今天的重要邮件,提炼待办并生成一份给老板的简报

从用户视角看,这就是一条普通消息。但从系统视角看,问题马上就来了:

钉钉 的消息格式和 飞书 不一样,Discord 和 WhatsApp 不一样,Telegram 和内部 WebSocket 通道也不一样。

有的平台带 message_id,有的平台叫 thread_ts,有的消息体里还嵌着复杂的结构,附件、引用、线程信息各不相同。

如果核心逻辑直接去处理这些异构数据,代码很快就会变成一团乱麻。

所以 OpenClaw 的第一步,不是让 Agent 去理解任务,而是先做 协议适配。

每个外部渠道都有一个专属适配器插件,把原始消息清洗成统一的内部对象,MsgContext。大概长这样:

interface MsgContext {  Body: string;  BodyForAgent?: string;  BodyForCommands?: string;  RawBody?: string;  SessionKey: string;  Provider: string;  Surface?: string;  ChatType?: "direct" | "group";  SenderId?: string;  SenderName?: string;  SenderUsername?: string;  OriginatingChannel?: string;  OriginatingTo?: string;  AccountId?: string;  MessageThreadId?: string;  CommandAuthorized?: boolean;  MessageSid?: string;  GatewayClientScopes?: string[];}

这里最关键是统一抽象。

也就是说,不管消息是从哪个犄角旮旯来的,进了网关之后,都会变成这个标准格式。后面的流程只需要对着 MsgContext 干活,完全不用操心来源平台。

web端消息适配

这一步把平台差异隔离在了网关入口,而不是污染到整个 Agent 执行链路里。

所以如果以后要接一个新通道,也不需要改核心逻辑,通常只要做四件事:

  1. 在 Registry 中添加通道元数据
  2. 实现对应的 ChannelPlugin
  3. 在插件加载器里注册新插件
  4. 更新配置 Schema 和文档测试

整个过程无需改核心系统代码,这就是插件化设计真正有价值的地方。

PS:这里要特别提一句,OpenClaw很多代码都是工程产物,如果大家真的想了解他的设计核心,只需要做一个渠道就好了,就会少很多工程策略,比如这里的收束信息

信息处理

所有封装好的 MsgContext,最后都会流入一个统一关卡:dispatchInboundMessage。

这一步的作用,不是做复杂业务,而是把所有入站消息的处理入口收敛成同一个总开关。源码大概是这样:

exportasyncfunctiondispatchInboundMessage(params{const finalized = finalizeInboundContext(params.ctx);returnawait withReplyDispatcher({    dispatcher: params.dispatcher,    run: () => dispatchReplyFromConfig({      ctx: finalized,      cfg: params.cfg,      dispatcher: params.dispatcher,      replyOptions: params.replyOptions,      replyResolver: params.replyResolver,    }),  });}

从结构上看,它先做了两件事:

1. 最终化入站上下文

也就是 finalizeInboundContext。这个步骤主要负责:

  1. 补全缺失字段
  2. 标准化格式
  3. 统一上下文表示

它的意义在于,前面虽然已经做了通道适配,但不同通道在细节上仍可能有不一致的地方,所以在真正进入核心处理逻辑前,还要再做一次最终收束。

2. 交给回复分发器继续往下跑

这一步之后,消息才真正开始进入 OpenClaw 的处理主干。

也就是说,到这里为止,系统干的还不是“理解任务”,而是先确保:

这条消息在格式上是可被系统安全处理的。

路由系统

消息一进入主链路,OpenClaw 不会立刻把它扔给模型,而是先做三类关键判断:

  1. 要不要处理
  2. 有没有重复
  3. 该交给哪个 Agent

这一步非常像真实系统里的“前置治理层”。

去重:先防止消息被重复处理

真实生产环境里,消息重复投递是非常常见的。

Webhook 可能重试,平台可能重复推送,网络抖动也可能导致同一条消息被系统收两次。如果不做幂等控制,最坏的结果不是“多回复一句”,而是:

  1. 同一任务被执行两次
  2. 同一工具被调用两次
  3. 同一个外部 API 被重复触发
  4. 同一笔成本被重复消耗

所以 OpenClaw 会为每条消息生成一个幂等键,也就是 idempotencyKey。核心逻辑由 buildInboundDedupeKey 控制:

exportfunction buildInboundDedupeKey(ctx: MsgContext): string | null {  const provider = normalizeProvider(    ctx.OriginatingChannel ?? ctx.Provider ?? ctx.Surface  );  const messageId = ctx.MessageSid?.trim();if (!provider || !messageId) {return null;  }  const peerId = resolveInboundPeerId(ctx);if (!peerId) {return null;  }  const sessionKey = ctx.SessionKey?.trim() ?? "";  const accountId = ctx.AccountId?.trim() ?? "";  const threadId = ctx.MessageThreadId ? String(ctx.MessageThreadId) : "";return [provider, accountId, sessionKey, peerId, threadId, messageId]    .filter(Boolean)    .join("|");}

生成格式大概是:

{provider}|{accountId}|{sessionKey}|{peerId}|{threadId}|{messageId}

比如:

  • whatsapp||main:+1234567890|msg_123
  • discord|default|agent:assistant:123|987654321||11223
  • slack|default|main:default|U12345678||C12345678

都会变成自己的唯一标识。

只要缓存里发现这个键已经处理过,系统就直接返回,避免重复调用昂贵的 LLM API,默认 TTL 是 20 分钟。

拦截

有些消息不是给 Agent 干活的,而是给系统发控制命令的,所以除了去重,系统还会做一些快速拦截。

例如用户输入 /stop,那它就不该继续往下跑任务理解和工具调用,而应该立刻中断对应的 AbortController,强行停止正在执行的 Agent 任务。

这说明 OpenClaw 不是一个单纯的“问一句答一句”的壳,它本质上还是一个长期运行的任务系统,所以控制命令是它必须支持的一类特殊输入。

快速响应

对于 Web 请求,系统通常还会先通过 WebSocket 返回一个 started 状态,然后再异步执行后续处理。

这一步看起来小,但很重要。

因为模型思考、工具调用、网络请求都可能很慢,如果前端一直等最终结果,很容易超时,也很容易让用户产生“卡死了”的感觉。

所以从体验上看,OpenClaw 会先让你知道:任务已经进入执行状态了。

Agent 登场

去重和拦截只是前置治理,真正进入业务之前,系统还得回答一个根本问题:这条消息,应该由谁来处理?这就是路由系统的工作。

OpenClaw 的路由根据通道类型,采用两套不同策略。

Web 内部通道

Web 客户端通常可以直接传 sessionKey,格式一般是:

{agentId}:{scope}

例如:

assistant:main

在这种情况下,系统可以直接使用这个会话键,不需要再查绑定规则。

外部通道

但像 Slack、WhatsApp、Discord 这类外部通道,客户端本身并不知道系统内部的会话组织方式,所以必须通过配置里的绑定规则来决定该交给哪个 Agent。

例如:

{"bindings": [    {"agentId""assistant","match": { "channel""whatsapp""accountId""my_bot" }    },    {"agentId""vip-assistant","match": { "channel""whatsapp""peer": { "id""+1234567890" } }    }  ]}

每个绑定规则都可以根据不同维度匹配:

  1. 通道标识
  2. 账户 ID
  3. 对等体用户或群组
  4. Discord 服务器 ID
  5. 团队 ID
  6. 角色列表

系统会按优先级从高到低匹配:

  1. 精确对等体匹配
  2. Discord 服务器 + 角色匹配
  3. Discord 服务器匹配
  4. 通道账户级匹配
  5. 通道级匹配
  6. 默认 Agent

所以回到我们的例子:

这条 钉钉 消息先被识别为来自哪个通道、哪个账号、哪个用户、哪个频道,然后系统根据配置决定,最终是由 assistant 来处理,还是由某个专门的 support-agent、vip-assistant、coder 来处理。

唯一会话键

一旦 Agent 确定下来,系统就会为这次对话构建 sessionKey,格式一般是:

{agentId}:{scope}

比如:

assistant:mainassistant:whatsapp:direct:+1234567890assistant:discord:channel:987654321support:telegram:group:-1001234567890

这个键非常重要,因为它后面承担两件事:会话隔离 与 并发控制

也就是说,用户看到的只是一句消息,但系统真正管理的,其实是:这句话属于哪个 Agent 的哪一条会话。

PS:这一段处理逻辑其实挺复杂的,我们这里不展开

车道机制

到这里,消息已经完成了路由,知道该交给哪个 Agent,也知道自己属于哪个会话。

但系统仍然不会直接开跑。原因很简单:如果同一会话里两条消息同时跑,很容易出现上下文错乱。

比如用户前一秒说:

帮我整理今天的重要邮件

下一秒又说:

顺便把待办改成按优先级排序

如果两条消息并行处理,就可能出现:

  1. 第二条先完成
  2. 第一条后完成
  3. 上下文互相污染
  4. 输出顺序颠倒
  5. 工具调用状态不一致

所以 OpenClaw 设计了 会话车道机制。

会话级车道

相同 sessionKey 的消息必须串行执行,确保上下文连贯。

这一步本质上是在保证:一个会话在任意时刻,只有一条消息真正占用它的执行上下文。

这样你才能把同一条对话当成“连续对话”来理解,而不是并发乱流。

全局级车道

除了会话级串行,系统还可以配置全局最大并发数。

如果整个系统同时进来太多消息,超出容量,就先进入等待队列。

这相当于两层节流:

  1. 会话层防止同一条对话乱序
  2. 系统层防止整个运行时被打爆

这一步很有 OpenClaw 的工程味道。它不是单纯依赖模型能力,而是在运行时层面主动治理资源。

开始执行

当消息终于排到自己,真正进入 Agent 执行阶段时,最重要的一件事来了:系统要为模型组装完整上下文。

大家马上会发现,要“爆炸”了...

很多人理解 Agent 时,容易想成这样:

  1. 用户输入一句话
  2. 模型理解一下
  3. 调用几个工具
  4. 结束

但实际在 OpenClaw 里,模型看到的不是单独一句用户输入,而是一整套被拼装好的上下文环境。组装顺序大致是:

系统提示词 → 技能提示 → 对话历史 → 当前消息

这个顺序很重要,因为它实际上定义了模型的认知层级:

  1. 先知道自己是谁
  2. 再知道自己能做什么
  3. 再知道之前发生了什么
  4. 最后才看用户刚刚说了什么

系统提示词

系统提示词的职责,是定义 Agent 的角色、行为规则和安全边界。

OpenClaw 这里做得很工程化,它不是把一整段 prompt 硬写死,而是通过 Bootstrap 文件系统来注入。

大概会把这些文件装进上下文:

AGENTS.md:定义 Agent 行为规则和工具使用指南SOUL.md:定义个性和人格TOOLS.md:工具使用说明IDENTITY.md:身份标识信息USER.md:用户偏好HEARTBEAT.md:心跳检测提示BOOTSTRAP.md:初始化引导MEMORY.md / memory.md:长期记忆

这些文件位于工作区目录,默认是 ~/.openclaw/workspace。

也就是说,在真正回答你“整理邮件”之前,模型先会被告知:

我是谁我该遵守什么规则我能用哪些工具这个用户有什么偏好这个系统有哪些长期记忆

这和普通聊天机器人有个本质区别:它不是每次都从零开始聊,而是从一个被预先塑形过的 Agent 身份出发。

记忆召回规则

在 src/agents/system-prompt.ts 里,系统提示词里还会显式包含记忆召回规则:

## Memory RecallBefore answering anything about prior work, decisions, dates, people, preferences, or todos:run memory_search on MEMORY.md + memory/*.md; then use memory_get to pull only needed lines.If low confidence after search, say you checked.

这段话很有意思。

它不是告诉模型“你要尽量记住”,而是告诉模型:

碰到和历史决策、偏好、待办、日期等相关的问题时,先查记忆,再说话。

这就把“记忆”从模型模糊的上下文残留,升级成了一种显式检索机制。

大小限制

因为这些文件每次运行都会消耗 tokens,所以系统会限制:

  1. 单文件最大字符数
  2. 总注入字符数上限
  3. 是否显示截断警告

这说明 OpenClaw 的思路不是把所有东西都喂给模型,原因很简单:系统提示词本身也要受到上下文预算约束。

核心:Skills 载入

系统提示词组装完之后,接下来要做的就是 技能提示注入。这部分很关键,因为很多人谈 OpenClaw 时,最容易误解 Skills。

从源码实现上看,Skills 并不是简单的一堆函数列表。它更像是:

先把一组可用能力的使用说明、调用边界、适用场景告诉模型,再在模型决定调用时,去连接真实的工具实现

也就是说,Skills 首先是让 Agent 知道自己该怎么用工具的方法包。

技能加载流程大致分四步:

发现

系统会从多个来源扫描技能文件:

  1. 工作区
  2. 用户全局目录
  3. 内置目录
  4. 插件目录

过滤

不是所有发现到的技能都能直接用,系统会根据多个条件过滤:

  1. 平台
  2. 消息通道
  3. 发送者权限
  4. 黑白名单配置

安全检查

OpenClaw 对 Skills 做了三层策略管道:

  1. Profile 过滤
  2. Sandbox 隔离
  3. Subagent 继承

也就是说,一个技能能不能被调用,不只是看它存不存在,还要看当前 Agent 有没有权限、安全边界允不允许、子 Agent 是否继承到对应能力。

生成提示词

最后系统会把可用技能描述格式化为文本,注入系统提示词,供 LLM 在需要时调用。

所以在我们的案例里,当用户说“帮我整理今天的重要邮件,提炼待办,并生成给老板的简报”时,模型不是凭空想象自己能做什么,而是会在这套技能描述中判断:

  1. 有没有邮件处理相关能力
  2. 有没有摘要生成能力
  3. 有没有文档组织能力
  4. 是否需要进一步调用子 Agent

这才是 Skills 真正的作用。

记忆系统

系统提示词和 Skills 搞定后,接下来才轮到对话历史和当前消息。

会话历史

OpenClaw 采用双层存储管理会话历史。

一、轻量索引:sessions.json

里面存的是元数据,例如:

会话 ID会话键转录文件路径最后更新时间模型覆盖配置技能快照

位置通常在:

~/.openclaw/agents/{agentId}/sessions/sessions.json

内容如下

二、重度转录:{sessionId}.jsonl

记录完整的对话历史,采用 JSON Lines 格式,每行一个 JSON 对象,便于流式读取和追加。文件同样位于 agents 目录下的 sessions 文件夹中。

历史消息加载

系统从转录文件中读取历史时,会做几件事:

  1. 从最新消息向前读取指定token数量的轮次
  2. 过滤掉不需要的消息类型
  3. 保证时间顺序正确
  4. 估算历史消息占用的 token

所以在真正调用模型前,系统已经在做一件很现实的事情:这次上下文预算里,到底还能放多少历史。

当前消息

当前消息作为上下文最后一部分注入,包括:

用户输入文本发送者信息时间戳元数据通道信息会话键

这也意味着,模型看到这次请求时,不是只看到一句新消息,而是站在一整条会话上下文的尾部,来理解它的语义。

记忆压缩

一旦把系统提示词、技能提示、历史记录、当前消息全塞进去,另一个问题马上就来了:上下文总有一天会爆。

所以 OpenClaw 这里专门做了一整套防爆机制。

历史轮次限制

系统会根据通道配置限制保留的历史轮数,从最新消息开始往前扫描,丢弃更早的部分。

这是一层最简单也最直接的防线。

工具结果截断

工具调用结果可能非常大,例如:

  1. 长文本
  2. 大 JSON
  3. 多页日志
  4. 大段网页内容

如果一股脑全塞回上下文,很容易直接撑爆窗口。

所以系统会自动截断工具输出,并判断是否要保留尾部关键信息。如果尾部有错误信息或 JSON 结构,就会采取“头尾保留”策略,否则只保留开头。

自动压缩

当上下文窗口接近模型限制时,系统会把早期历史分块,然后为每块生成摘要,用摘要替换早期历史,同时保留最近几轮完整对话。

摘要生成时要求保留:

  1. 活跃任务
  2. 操作进度
  3. 用户最后请求
  4. 已做决策
  5. 后续依赖信息

所以这不是机械裁剪,而是一种语义压缩。

容错与降级

如果压缩后仍然超限,系统还会继续尝试:

  1. 切换到上下文更大的模型
  2. 降低 Agent 的 thinking 级别
  3. 最终回退为提示用户重置会话

这说明 OpenClaw 不是把上下文管理交给模型自己,而是把它视为运行时层面的硬约束问题。

PS:从这里大家就可以看出,如果没有最近一年的模型上下文极速增长,根本不可能有 OpenClaw 这类 Agent 啥事

记忆系统怎么工作的

除了会话历史,OpenClaw 还单独维护两类记忆:

一、长期记忆

文件名通常是:

MEMORY.mdmemory.md

用于存常青知识,例如:

  1. 项目规则
  2. API 文档
  3. 设计决策
  4. 长期偏好

这类记忆在 Agent 启动时通过 Bootstrap 系统直接注入系统提示词。

二、每日记忆

存放在:

memory/YYYY-MM-DD.md

用于记录时效性内容,例如:

  1. 每日纪要
  2. 当天待办
  3. 临时决策
  4. 会议记录

这类记忆不直接注入提示词,而是通过记忆搜索工具按需检索,并且会带时间衰减权重,越新的内容权重越高。

三、记忆什么时候写入

长期记忆通常由用户或 Agent 通过编辑工具手动维护。

而每日记忆则会通过 Memory Flush 机制自动触发。

触发条件

  • 会话 token 数接近上下文窗口上限(默认软阈值 4000 tokens)
  • 会话转录文件大小超过阈值(默认 2MB)

当系统发现会话快接近压缩阈值时,会先发一个特殊提示给 Agent:

Pre-compaction memory flush.Store durable memories now (use memory/YYYY-MM-DD.md; create memory/ if needed).IMPORTANT: If file already exists, APPEND new content only and do not overwrite existing entries.

也就是说,在压缩发生前,系统会先提醒 Agent:把这轮对话中值得长期保留的信息,先沉淀进每日记忆。

这相当于在上下文压缩前,先打一层记忆护城河。

真的执行了

到这里,上下文已经准备好,Agent 才真正开始运行。

在我们的例子里,这时候模型可能会判断:

当前任务不是普通问答它是一个执行型任务里面包含邮件整理、待办提炼、简报生成三类需求需要调用相应技能和工具如果任务过于复杂,可能还要拆给子 Agent

这个阶段最核心的三件事是:

流式响应

OpenClaw 使用 SSE 或 WebSocket 做流式输出。

当 LLM 开始生成内容时,系统会把内容块实时推送给客户端,让用户立即看到输出开始,而不是等全部完成后一次性返回。

这能明显降低感知延迟,也更适合长任务。

工具调用

当 LLM 判断需要调用工具时,系统会暂停文本流,执行对应工具,例如:

  1. 读取文件
  2. 调 API
  3. 搜索记忆
  4. 访问邮件
  5. 执行脚本

执行完后,再把结果反馈给 LLM,让它继续推理。

对用户来说,看到的可能只是 正在执行工具 或者一段中断后的继续输出;但对系统来说,这其实是一次完整的推理—执行—再推理循环

错误处理与模型回退

真实生产环境里,出错是常态。所以 OpenClaw 做了多级回退策略:

  1. 限流:指数退避或切换备用模型
  2. 认证错误:轮换多个 API Key
  3. 超时错误:降低 thinking 级别或换更快模型
  4. 上下文溢出:触发压缩或换更大上下文模型

这部分很能说明 OpenClaw 的工程取向:它不假设模型永远稳定,而是假设运行过程随时可能失败,所以提前把兜底路径都铺好。

任务完成

当 Agent 终于处理完

整理今天的重要邮件,提炼待办并生成给老板的简报

这件事后,系统仍然不能立刻算结束。它还要做三类收尾动作:

响应投递

回复分发器会根据消息上下文中的来源通道和目标地址,调用对应通道的出站适配器发送消息。

如果配置了跨通道回复,也可以覆盖原始目标。例如:

  1. 在 钉钉 发起任务
  2. 最终把结果发回 WhatsApp

这一点非常像智能网关的思路,而不只是聊天框里的模型。

但还是那句话,OpenClaw 这块的源码不适合初学者,初学者直接搞一个 IM 渠道就好,OpenClaw 很多工程代码,就是为了兜底,这些代码量全部加剧了学习成本。

会话持久化

系统会把这次交互的完整记录写下来:

  1. 更新 sessions.json 里的元数据
  2. 把用户消息、AI 回复、工具调用等内容追加到 JSONL 转录文件

这样下一次会话继续时,系统才能知道之前发生过什么。

资源释放与清理

执行结束后,还要做:

  1. 释放会话车道锁
  2. 释放全局并发配额
  3. 标记幂等键为已处理
  4. 定期清理过期会话
  5. 归档旧转录
  6. 轮换大文件

这一步很像一个长期运行系统的善后逻辑,没有它,系统迟早会越来越重。

记忆索引

既然记忆是 Markdown 文件,系统就还要解决一个问题:怎么让 Agent 高效查它们。

OpenClaw 的做法是给记忆系统单独建索引,索引数据库通常位于:

~/.openclaw/memory/index.db

里面大致会有这些表:

fileschunkschunks_vecchunks_ftsembedding_cache

也就是同时支持:

  1. 文件元数据管理
  2. 文本分块
  3. 向量检索
  4. 全文搜索
  5. 向量缓存

为了保证文件和索引同步,系统还会做三种同步机制:

  1. 文件监视器自动触发
  2. 定期同步
  3. 增量同步

必要时还会全量重建索引:

  1. 创建临时数据库
  2. 遍历记忆文件
  3. 分块
  4. 生成 embedding
  5. 建全文索引
  6. 最后原子替换旧索引

综上,OpenClaw 的记忆并不是把 Markdown 当备忘录丢在那,而是真把它做成了一层可检索、可维护的知识基座。

多 Agent

前面讲到这里,其实已经是一条完整的单 Agent 执行链路了:

消息进门协议适配去重拦截路由分发会话排队上下文组装技能注入流式执行响应投递状态持久化

如果任务简单,到这里就闭环了。

但现实问题是,很多复杂任务并不适合由一个 Agent 单独完成。还是刚才那个例子:

帮我整理今天的重要邮件,提炼待办并生成一份给老板的简报

看起来一句话,实际上可能包含至少三块工作:

  1. 筛选和归类邮件
  2. 提炼关键待办
  3. 组织成适合老板阅读的简报格式

如果全让一个 Agent 一把抓,它当然也能硬做,但往往会出现:

上下文太重推理链过长工具调用太杂专业能力混在一起中间状态难管理

所以 OpenClaw 在单 Agent 之上,又做了 多 Agent 协作系统。

多 Agent 的核心能力

它实现了几件关键事情:

  1. Agent 隔离
  2. 动态任务分发
  3. 层级协作
  4. 生命周期管理
  5. 安全边界控制

也就是说,主 Agent 可以根据任务需要,临时创建一个或多个子 Agent,把某些子任务交出去,自己做总控和汇总。

一个典型的多 Agent 流程

在我们的案例里,主 Agent 完全可能这么干:

  1. 先创建一个 research-agent 去筛重要邮件
  2. 再创建一个 analysis-agent 去提炼待办和风险点
  3. 最后由主 Agent 自己把这些结果整合成给老板的简报

这时候,多 Agent 就不再是抽象概念,而是变成了一种很具体的任务拆解策略。

创建 subAgent

主 Agent 决定创建子 Agent 时,通常会走 sessions_spawn 工具。

系统先做一轮严格校验:

  1. 嵌套深度检查,防止无限递归
  2. 并发限制检查,防止资源耗尽
  3. 允许列表检查
  4. 沙箱状态检查

通过后,系统会:

生成唯一子会话键例如 agent:{agentId}:subagent:{uuid}应用模型配置和 thinking 级别处理附件和上下文传递为子 Agent 生成专门的系统提示词

例如:

# Subagent ContextYou are a subagent spawned by main agent for a specific task.## Your Role- You were created to handle: ${taskDescription}- Complete this task. That's your entire purpose.- You are NOT main agent. Don't try to be.## Rules1. Stay focused2. Complete task3. Don't initiate4. Be ephemeral

这个提示词不是在强化子 Agent 的人格,而是在强调它的边界:你不是主 Agent,你就是来做这一个子任务的。

结果返回

子 Agent 完成任务后,系统会:

  1. 触发生命周期事件
  2. 读取输出结果
  3. 经过通知队列
  4. 判断目标是谁
  5. 决定注入主 Agent 会话,还是直接发给用户

如果请求者是主 Agent,那么结果通常会以内部事件的方式重新注入主 Agent 会话,供主 Agent 下一轮推理时使用。

于是整条协作链路就变成了:

主 Agent 接收用户任务主 Agent 拆子任务子 Agent 各自执行子 Agent 把结果回传主 Agent 汇总结果主 Agent 最终回复用户

这个过程本质上就是一个层级化协作网络。

配置继承

OpenClaw 在 Agent 配置上采用三级继承机制:

  1. Agent 级配置优先级最高
  2. 全局默认配置次之
  3. 代码默认值兜底

例如某个 coding-agent 可以有自己的:

模型工作区可用工具子 Agent 策略沙箱模式

而没有单独定义的部分,再回落到全局默认值。这种设计让多 Agent 系统既灵活,又不至于配置爆炸。

OpenClaw 强在哪

再回到最初那句话:

帮我整理今天的重要邮件,提炼待办并生成一份给老板的简报

那么它在 OpenClaw 里真正经历的大致过程,其实是这样的:

钉钉原始消息进入系统通道插件把它适配成统一的 MsgContext网关做最终化处理系统检查去重、拦截控制命令、快速响应 started 状态路由系统根据绑定规则找到目标 Agent生成 sessionKey进入会话车道排队,确保同一会话不乱序组装完整上下文:系统提示词、Bootstrap 文件、Skills、历史记录、当前消息模型在技能描述和规则约束下开始推理过程中可能调用工具,也可能 spawn 子 Agent子 Agent 完成后把结果回流给主 Agent主 Agent 生成最终答复回复分发器把结果投递回目标通道会话和转录被持久化记忆被更新,索引同步资源释放,执行闭环结束

这样一看你就会发现,OpenClaw 真正有价值的地方,不是它能不能回答一句话,而是:

它把一条消息从进入系统到完成执行,做成了一条可治理、可扩展、可追踪、可恢复的 Agent Runtime 链路

结语

通过完整追踪一条消息的旅程,我们可以更清楚地看到 OpenClaw 的几个核心设计原则。

第一,它是分层的。

各层职责清晰,从通道适配到执行治理再到基础设施,边界都比较明确。

第二,它是运行时导向的。

去重、会话车道、上下文压缩、错误回退、资源清理,这些都说明它不是一个 把 prompt 包一层 UI 的玩具,而是真在认真处理长期运行中的工程问题。

第三,它是可扩展的。

通道插件、技能系统、子 Agent 机制、记忆索引,都让它更像一个开放的 Agent 网关,而不是一个封闭应用。

第四,它开始具备分布式协作的雏形。

多 Agent 的出现,意味着 OpenClaw 已经不满足于“一个模型处理一切”,而是在朝任务拆解、层级协作、并行执行的方向发展。

所以如果你问,OpenClaw 到底是什么。

我的回答会是:

它不是一个更会聊天的机器人,也不只是一个会调工具的 Agent 壳。它更像是一个把消息入口、会话治理、上下文管理、技能调用、持久化存储和多 Agent 协作缝合在一起的 Agent Runtime + Gateway

而理解它最好的方式,就是去开发一个 Mini-OpenClaw,这也是我们正在做的。

所以,后续我们再基于一个更简单版本,没有那么多工程控制的系统做讨论...

点击上方卡片关注叶小钗公众号,查看下方二维码,添加我个人微信:

往期推荐

《系统性:如何进入AI行业?》

《万字:Agent概述》

《万字:做一个Agent-上》

《万字:做一个Agent-下》

《万字:理解LangChain》

《万字:AI Coding 的真实情况》


《重要:AI学习路线图》

《万字:个人IP,包教包会》

《万字:AI客服实战方法论》

《万字:生产级别的RAG系统》

《万字:RAG实战技巧,包教包会》


《2025年终总结》

《OpenClaw 会不会淘汰 Coze、Dify 这类平台?》

《别被 OpenClaw 带偏了,AI 公司到底该如何组织人才?》