芥末
发布于 2026-03-16 / 0 阅读
0
0

OpenClaw Agent Runtime 架构拆解:Gateway、Memory、Skills 与多 Agent 协作

OpenClaw 可以被理解为一套 Agent Runtime(智能体运行时)加 Gateway(网关)的组合系统。

它并不是把用户输入直接交给 LLM(大语言模型),再把模型输出原样返回。真正的运行链路要复杂得多:外部消息进入系统后,需要经过协议适配、去重、路由、会话隔离、上下文组装、技能注入、模型推理、工具调用、结果投递和状态持久化。

用一个典型请求来贯穿整个过程:

在钉钉里发来一句:

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

从用户视角看,这只是一句话;从系统视角看,它是一条需要被治理、调度、执行和保存的任务请求。

完整链路可以抽象成这样:

flowchart LR
    A[钉钉 / 飞书 / Slack / Web] --> B[通道适配器]
    B --> C[MsgContext]
    C --> D[Gateway 入站分发]
    D --> E[去重与命令拦截]
    E --> F[路由到目标 Agent]
    F --> G[生成 sessionKey]
    G --> H[会话车道排队]
    H --> I[组装上下文]
    I --> J[LLM 推理]
    J --> K[工具调用 / 子 Agent]
    K --> J
    J --> L[回复投递]
    L --> M[会话与记忆持久化]

OpenClaw 的工程重点就在这条链路里。它关注的不是“模型能不能回答”,而是“这条消息如何稳定、可追踪、可扩展地被执行”。


整体架构:五层把 Agent 系统拆开

OpenClaw 的整体结构可以拆成五层:用户接口层、Gateway 核心层、消息处理层、扩展插件层和基础设施层。

OpenClaw 分层架构

这张架构图的关键点在于:Agent 并不是系统中唯一重要的部分。真正支撑 Agent 长期运行的是外围的网关、路由、插件、记忆和运行时治理能力。

层级职责解决的问题
用户接口层CLI(命令行界面)、Web UI、移动端、WebSocket API(应用程序编程接口)等入口把不同入口的用户操作收敛成系统请求
Gateway 核心层连接管理、配置加载、健康检查、请求接入让系统能常驻运行、接收消息、维持状态
消息处理层Agent 执行器、路由系统、会话管理、媒体处理、出站投递处理一条消息从进入到返回的主流程
扩展插件层通道插件、Skills、Sub Agent对接新平台、新工具和多 Agent 协作
基础设施层配置、密钥、日志、定时任务、事件总线、记忆检索、沙箱保证系统稳定性、安全性和可维护性

这种分层的直接好处是边界清楚。比如新增一个 Slack 通道,理论上只需要扩展通道插件和配置,不应该改 Agent 执行器;新增一个技能,也不应该改 Gateway 的核心调度逻辑。


消息进门:先把平台差异收敛掉

不同平台的消息格式差异很大。

钉钉、飞书、Slack、Discord、WhatsApp、Telegram、WebSocket 都可能有自己的字段命名、线程结构、附件格式和用户身份体系。比如有的平台叫 message_id,有的平台叫 thread_ts,有的平台把引用消息、频道信息、群组信息嵌在复杂结构里。

如果 Agent 核心逻辑直接处理这些原始数据,很快会变成大量平台判断:

if (provider === "slack") {
  // 处理 Slack 字段
} else if (provider === "dingtalk") {
  // 处理钉钉字段
} else if (provider === "discord") {
  // 处理 Discord 字段
}

这种写法会把平台差异污染到整个系统。

OpenClaw 的做法是把差异挡在入口:每个外部渠道都有对应的 ChannelPlugin,把原始消息转换成统一的内部消息对象 MsgContext

消息协议适配流程

图里的核心思想是协议适配:平台消息只在入口处被解析,进入 Gateway 后就变成统一结构,后续模块只面向 MsgContext 编程。

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[];
}

几个字段比较关键:

字段作用
Body标准化后的消息正文
BodyForAgent给 Agent 使用的正文,可能已经过滤命令或特殊标记
BodyForCommands给命令系统使用的正文
SessionKey会话标识,后续用于会话隔离和并发控制
Provider / OriginatingChannel消息来源平台
SenderId / SenderName发送者身份
MessageThreadId线程消息 ID,适合 Slack、Discord 等有线程概念的平台
MessageSid消息唯一 ID,用于去重

新增一个外部通道时,核心任务通常是四件事:

  1. 在 Registry 中声明通道元数据。
  2. 实现对应的 ChannelPlugin
  3. 把插件注册到插件加载器。
  4. 补充配置 Schema、文档和测试。

核心系统不需要知道这个平台的所有细节,只需要相信插件会输出标准的 MsgContext


入站分发:把所有消息收敛到统一入口

消息被适配后,会进入统一的入站分发函数。这个入口的意义不是处理复杂业务,而是让所有消息从同一个主开关进入运行链路。

核心结构可以简化成这样:

export async function dispatchInboundMessage(params) {
  const finalized = finalizeInboundContext(params.ctx);

  return await withReplyDispatcher({
    dispatcher: params.dispatcher,
    run: () =>
      dispatchReplyFromConfig({
        ctx: finalized,
        cfg: params.cfg,
        dispatcher: params.dispatcher,
        replyOptions: params.replyOptions,
        replyResolver: params.replyResolver,
      }),
  });
}

这里做了两件关键动作。

finalizeInboundContext 会对上下文做最终收束,包括补全字段、统一格式、修正不同插件之间的细节差异。通道插件已经做过一次适配,但进入主流程前仍然需要一道兜底标准化,避免某个插件输出的边缘字段影响后续模块。

withReplyDispatcher 则把后续执行包进回复分发器里。这样无论后面是同步返回、流式返回,还是执行过程中多次发送中间状态,都能走统一投递机制。

到这个位置,系统还没有让模型理解任务。它只是在确认:消息格式可靠,可以进入运行时主链路。


路由前置治理:去重、拦截和快速响应

消息进入主链路后,不会立即交给模型。OpenClaw 会先判断三件事:

  1. 这条消息有没有重复处理过。
  2. 它是不是控制命令。
  3. 是否需要先给前端返回开始执行的状态。

这些动作看起来不像 Agent 能力,却是生产系统必须处理的问题。

去重:防止同一条消息被执行两次

真实的消息平台经常会重复投递。Webhook(回调通知)可能因为超时重试,网络抖动也可能让同一条消息被系统收到两次。

如果没有幂等控制,后果可能不只是多回复一句话,而是重复调用工具、重复写数据库、重复触发外部 API,甚至重复扣费。

OpenClaw 会根据消息上下文生成一个幂等键:

export function 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

只要缓存里已经存在这个键,系统就直接停止后续处理。默认 TTL(生存时间)是 20 分钟,用来覆盖平台重试和短时间重复投递场景。

命令拦截:控制指令不应该进入模型推理

并不是所有消息都应该交给 Agent。比如:

/stop

这类消息更像运行时控制命令。它要做的是中断当前任务,而不是让模型解释“stop 是什么意思”。

OpenClaw 会在前置阶段识别控制命令,并触发对应的 AbortController。这说明它不是一个简单的聊天壳,而是一个长期运行的任务系统。长期任务需要启动、取消、超时、清理等控制能力。

快速响应:先告诉用户任务已经开始

Web 请求和 WebSocket 场景下,模型推理和工具调用可能持续几秒甚至几十秒。如果前端一直等最终结果,用户会以为系统卡住了。

OpenClaw 通常会先返回一个 started 状态,再异步执行后续任务:

sequenceDiagram
    participant U as 用户界面
    participant G as Gateway
    participant R as Runtime

    U->>G: 发送消息
    G-->>U: started
    G->>R: 异步执行 Agent 任务
    R-->>G: 流式输出 / 最终结果
    G-->>U: 返回结果

这个状态不负责回答问题,只负责确认任务已进入执行队列。


路由系统:决定这条消息交给哪个 Agent

前置治理完成后,系统要确定目标 Agent。OpenClaw 对内部 Web 通道和外部通道采用不同策略。

Web 内部通道:直接使用 sessionKey

Web 客户端通常可以传入明确的 sessionKey,格式一般是:

{agentId}:{scope}

例如:

assistant:main

这种场景下,系统可以直接知道消息属于哪个 Agent、哪个会话范围,不需要再根据外部平台绑定规则反查。

外部通道:根据绑定规则匹配 Agent

Slack、WhatsApp、Discord、钉钉这类外部平台并不知道 OpenClaw 内部的 Agent 组织方式,所以要靠配置规则做绑定。

配置可以像这样:

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

匹配维度通常包括:

维度示例
通道whatsappslackdiscord
账户 ID同一平台下的不同机器人账号
用户或群组某个私聊用户、某个群
Discord 服务器指定 guild
团队 IDSlack workspace 或企业组织
角色列表Discord 角色、企业内部角色

匹配优先级可以理解为从精确到宽泛:

优先级匹配方式
1精确用户或群组匹配
2Discord 服务器 + 角色匹配
3Discord 服务器匹配
4通道账户级匹配
5通道级匹配
6默认 Agent

回到钉钉消息的例子,系统会先识别消息来自哪个通道、哪个账号、哪个用户或群,再根据绑定规则决定交给 assistantsupport-agentvip-assistant 还是其他专用 Agent。


sessionKey:会话隔离和并发控制的基础

Agent 确定后,系统会构造 sessionKey。常见格式仍然是:

{agentId}:{scope}

一些例子:

assistant:main

assistant:whatsapp:direct:+1234567890

assistant:discord:channel:987654321

support:telegram:group:-1001234567890

sessionKey 后面承担两个核心职责:

  1. 标识这条消息属于哪个 Agent 的哪条会话。
  2. 控制同一会话内消息的执行顺序。

用户看到的是一句话,系统管理的是一个带身份、来源、会话范围和执行顺序的任务。


会话车道:同一会话串行,全局并发受控

路由完成后,系统仍然不会马上执行。原因是同一会话里多条消息如果并行跑,容易造成上下文错乱。

比如用户连续发送两条消息:

帮我整理今天的重要邮件
顺便把待办改成按优先级排序

如果两条消息同时执行,可能出现第二条先完成、第一条后完成的情况。这样模型看到的历史、工具调用状态和最终回复顺序都会乱。

OpenClaw 使用会话车道机制解决这个问题:

flowchart TD
    A[入站消息] --> B{根据 sessionKey 分配车道}

    B --> C[sessionKey = assistant:main]
    B --> D[sessionKey = support:telegram:group]

    C --> C1[消息 1]
    C1 --> C2[消息 2]
    C2 --> C3[消息 3]

    D --> D1[消息 A]
    D1 --> D2[消息 B]

    C3 --> E[全局并发控制]
    D2 --> E

    E --> F[Agent Runtime 执行]

会话车道有两层约束:

控制层作用
会话级车道相同 sessionKey 的消息串行执行,避免同一对话乱序
全局级车道限制整个系统同时执行的任务数量,防止运行时被打满

这是一种运行时治理能力。它不依赖模型“自己记住顺序”,而是在系统层面保证同一会话只有一条消息占用执行上下文。


上下文组装:模型看到的不是一句话

进入 Agent 执行阶段后,最关键的步骤是组装上下文。

很多人理解 Agent 时,会把流程想成:

用户输入 -> 模型理解 -> 工具调用 -> 输出结果

实际运行时,模型看到的内容远不止用户刚刚输入的一句话。OpenClaw 会按顺序拼出一整套上下文:

系统提示词 -> Skills 提示 -> 对话历史 -> 当前消息

OpenClaw 上下文组装流程

这张图表达的是模型输入的层级关系:模型先知道自己的身份和规则,再知道自己有哪些技能,然后读取历史,最后处理当前请求。

上下文部分作用
系统提示词定义 Agent 身份、行为边界、安全规则
Skills 提示告诉模型可用能力、使用方式和限制
对话历史让模型理解当前消息之前发生了什么
当前消息用户本次请求及其元数据

Bootstrap 文件:把 Agent 身份文件化

OpenClaw 没有把系统提示词完全硬编码在程序里,而是通过工作区里的 Bootstrap 文件注入。

默认工作区通常位于:

~/.openclaw/workspace

常见文件包括:

文件作用
AGENTS.mdAgent 行为规则和工具使用指南
SOUL.md个性、语气或人格设定
TOOLS.md工具使用说明
IDENTITY.md身份标识信息
USER.md用户偏好
HEARTBEAT.md心跳检测提示
BOOTSTRAP.md初始化引导
MEMORY.md / memory.md长期记忆入口

模型在处理“整理邮件、提炼待办、生成简报”之前,会先被告知:

你是谁
你应该遵守什么规则
你能调用哪些工具
用户偏好是什么
系统长期记忆里有哪些信息

这使得 OpenClaw 中的 Agent 不是每次从零开始聊天,而是从一个被配置和记忆塑造过的身份出发。

记忆召回不是“让模型自己想起来”

系统提示词里会明确规定记忆检索规则。类似这样的规则会被注入:

## Memory Recall
Before 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.

这段规则很重要。它没有要求模型“尽量记住”,而是把记忆变成显式动作:

涉及历史工作、决策、日期、人物、偏好、待办时,
先执行 memory_search,
再用 memory_get 拉取必要内容。

这种设计把记忆从模型上下文里的模糊残留,变成可检索、可控制的外部机制。

提示词注入要受上下文预算约束

Bootstrap 文件、记忆文件、工具说明都会消耗 token。OpenClaw 会限制:

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

这说明系统不会把所有内容无脑塞给模型。上下文窗口是有限资源,系统提示词本身也必须被预算管理。


Skills:不是函数列表,而是能力说明加调用边界

Skills 是 OpenClaw 里最容易被误解的部分。它不是简单的一组函数,也不是普通工具列表。更准确地说,Skills 是一套“告诉 Agent 如何使用能力”的方法包。

OpenClaw Skills 加载流程

图里的流程可以理解为:系统先发现技能,再根据当前环境过滤和检查权限,最后把可用技能描述注入上下文。模型在推理时根据这些描述决定是否调用真实工具。

Skills 的加载通常经历四步。

发现技能

系统会从多个位置扫描技能文件:

来源含义
工作区目录当前项目或当前 Agent 专属技能
用户全局目录用户级可复用技能
内置目录OpenClaw 默认提供的技能
插件目录插件扩展出来的技能

过滤技能

发现到的技能不一定都能用。过滤条件可能包括:

  1. 当前平台。
  2. 当前消息通道。
  3. 发送者权限。
  4. 黑名单和白名单配置。
  5. Agent Profile 配置。

例如,某个技能只允许 Web 内部通道使用,不应该暴露给外部群聊;某个邮件技能只允许授权用户触发,不应该被普通群成员调用。

安全检查

OpenClaw 对 Skills 有多层安全策略:

策略作用
Profile 过滤根据 Agent 配置决定技能是否可见
Sandbox 隔离把高风险执行放进受限环境
Sub Agent 继承子 Agent 是否能继承主 Agent 的能力由配置决定

一个技能能不能用,不只取决于它是否存在,还取决于当前 Agent、当前通道、当前用户和当前安全边界。

生成技能提示词

通过检查后的技能会被格式化成提示文本,注入模型上下文。

一个技能描述可以抽象成这样:

## skill: mail_summary

适用场景:
- 读取当天邮件
- 提取重要事项
- 汇总待办和责任人

使用限制:
- 只能读取授权邮箱
- 涉及敏感邮件时需要返回摘要,不暴露原始全文

可调用工具:
- mail.search
- mail.get
- doc.create

当用户说“整理今天的重要邮件,提炼待办,并生成给老板的简报”时,模型会根据 Skills 判断:

  1. 是否存在邮件检索能力。
  2. 是否存在摘要和待办提取能力。
  3. 是否存在文档生成能力。
  4. 是否需要拆分任务交给子 Agent。

Skills 的价值不只是“能调用工具”,而是让模型知道工具什么时候该用、怎么用、有什么边界。


Memory 和会话历史:轻量索引加重度转录

OpenClaw 的记忆系统分成两类:会话历史和长期记忆。会话历史记录对话过程,长期记忆保存跨会话可复用的信息。

会话历史采用双层存储结构。

OpenClaw 会话存储结构

图里展示的是轻量索引和完整转录的分工:sessions.json 负责快速找到会话元数据,具体对话内容放在对应的 JSON Lines 文件里。

sessions.json:保存会话元数据

sessions.json 通常位于:

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

它保存的是轻量元数据,例如:

{
  "sessions": [
    {
      "sessionId": "01HRX8Y2...",
      "sessionKey": "assistant:main",
      "transcriptPath": "01HRX8Y2.jsonl",
      "updatedAt": "2026-03-16T08:00:00.000Z",
      "modelOverride": null,
      "skillsSnapshot": ["mail_summary", "doc_writer"]
    }
  ]
}

这些信息适合快速检索、恢复会话和定位转录文件。

JSONL 转录:保存完整对话过程

完整会话内容放在 {sessionId}.jsonl 里。JSON Lines(逐行 JSON)的特点是每一行都是一个独立 JSON 对象,适合追加写入和流式读取。

示例:

{"role":"user","content":"帮我整理今天的重要邮件","createdAt":"2026-03-16T08:01:00.000Z"}
{"role":"assistant","content":"我会先检索今天的邮件,再提取待办。","createdAt":"2026-03-16T08:01:01.000Z"}
{"role":"tool","name":"mail.search","content":"找到 18 封邮件,其中 5 封高优先级","createdAt":"2026-03-16T08:01:03.000Z"}

这种设计把“会话列表”和“完整内容”拆开,避免每次加载会话索引时读入大量历史文本。

历史加载:按 token 预算取最近上下文

模型调用前,系统会从转录文件中加载历史消息。加载过程通常会做几件事:

  1. 从最新消息向前读取。
  2. 控制历史轮数或 token 数量。
  3. 过滤不需要放回模型的消息类型。
  4. 恢复正确时间顺序。
  5. 估算历史内容占用的 token。

真正送进模型的历史不是“全部聊天记录”,而是在上下文预算内选出的最近有效片段。


上下文压缩:防止提示词和历史把窗口撑爆

当系统提示词、Skills、记忆、历史记录、当前消息和工具结果都进入上下文后,窗口迟早会接近模型限制。

OpenClaw 需要一套防爆机制。

OpenClaw 上下文压缩机制

图里关注的是上下文窗口管理:历史轮次限制、工具结果截断、摘要压缩和降级策略共同保证模型调用不会因为输入过大而失败。

历史轮次限制

最直接的方式是限制保留历史轮数。系统从最新消息向前扫描,超过限制的早期消息不再注入。

这种策略简单可靠,但缺点是可能丢失早期的重要决策,所以只适合做第一层保护。

工具结果截断

工具调用结果经常比用户输入大得多,例如:

  1. 多页网页内容。
  2. 大段日志。
  3. 长 JSON。
  4. 邮件全文。
  5. 搜索结果列表。

如果把工具输出完整塞回模型,很容易直接超出上下文窗口。

OpenClaw 会对工具结果做截断。常见策略有两类:

策略适合内容
只保留开头普通长文本、网页正文
头尾保留日志、错误堆栈、JSON 结构、命令输出

头尾保留很实用,因为日志和错误信息经常出现在末尾,只保留开头可能会丢掉真正重要的失败原因。

自动摘要压缩

当上下文仍然接近上限时,系统会把早期历史分块摘要,用摘要替代完整历史,同时保留最近几轮原文。

摘要需要保留的信息包括:

  1. 当前仍在进行的任务。
  2. 已经完成的操作。
  3. 用户最后的明确请求。
  4. 已作出的决策。
  5. 后续步骤依赖的信息。
  6. 工具调用产生的关键结果。

这不是机械裁剪,而是语义压缩。它尽量保留“继续完成任务所需的信息”,丢掉不影响执行的细节。

容错与降级

如果压缩后仍然超限,系统还可以继续降级:

  1. 减少历史轮数。
  2. 缩短工具结果。
  3. 降低 Bootstrap 文件注入量。
  4. 只保留必要 Skills。
  5. 要求模型先基于已有信息继续,必要时再检索记忆或调用工具。

上下文管理的目标不是让模型看到所有东西,而是让模型看到完成当前任务所需的最小充分信息。


Runtime 执行:流式输出、工具调用和持久化闭环

上下文准备好后,Agent Runtime 才真正开始执行任务。

执行过程可以看成一个循环:

sequenceDiagram
    participant U as 用户
    participant G as Gateway
    participant L as 会话车道
    participant R as Agent Runtime
    participant M as LLM
    participant T as Tool / Skill
    participant S as Storage

    U->>G: 发送消息
    G->>L: 根据 sessionKey 入队
    L->>R: 获得执行权
    R->>S: 读取 Bootstrap / Memory / History
    R->>M: 发送完整上下文
    M-->>R: 生成中间输出或工具调用意图
    R->>T: 执行工具
    T-->>R: 返回工具结果
    R->>M: 注入工具结果继续推理
    M-->>R: 最终回复
    R->>G: 流式投递结果
    R->>S: 追加转录与更新记忆
    G-->>U: 返回响应

Runtime 负责的不只是“调用一次模型”。

Runtime 职责说明
上下文读取加载系统提示词、Skills、历史、记忆
模型调用向 LLM 提交完整上下文
流式输出把生成过程中的 token 或阶段性状态投递给前端
工具调用根据模型请求调用真实工具
子 Agent 调度复杂任务可拆给子 Agent
中断控制响应 /stop 等控制命令
结果持久化记录会话转录、工具结果和最终回复
资源清理释放执行状态、取消信号、临时文件等

对于“整理邮件并生成简报”这个任务,Runtime 可能会经历这样的步骤:

1. 模型判断需要读取邮件。
2. Runtime 调用邮件检索工具。
3. 工具返回当天邮件列表。
4. 模型判断哪些邮件重要。
5. Runtime 根据需要读取若干邮件详情。
6. 模型提取待办、负责人、截止时间。
7. Runtime 调用文档生成工具。
8. 模型生成给老板的简报。
9. Gateway 把结果投递回钉钉。
10. 会话历史和必要记忆被保存。

这套机制让 Agent 不再是一次性问答,而是一个可持续执行、可中断、可恢复的任务运行过程。


多 Agent 协作:主 Agent 负责任务组织,子 Agent 负责局部执行

复杂任务不一定适合由一个 Agent 从头处理到尾。

比如“整理邮件、提炼待办、生成老板简报”可以拆成几个子任务:

子任务更适合的执行者
检索和筛选邮件邮件分析 Agent
提取待办和风险任务管理 Agent
写老板简报写作 Agent
检查格式和语气审校 Agent

OpenClaw 的多 Agent 协作可以理解为:主 Agent 负责判断任务结构,必要时 spawn 子 Agent;子 Agent 在受限上下文和受控技能集内完成局部任务,再把结果返回给主 Agent 汇总。

flowchart TD
    A[用户请求] --> B[主 Agent]

    B --> C{是否需要拆分任务}

    C -- 否 --> D[主 Agent 直接完成]
    C -- 是 --> E[创建子 Agent 任务]

    E --> F[邮件分析 Agent]
    E --> G[待办提取 Agent]
    E --> H[简报写作 Agent]

    F --> I[邮件摘要]
    G --> J[待办列表]
    H --> K[简报草稿]

    I --> L[主 Agent 汇总]
    J --> L
    K --> L

    L --> M[最终回复]

多 Agent 协作最重要的不是“多开几个模型实例”,而是边界控制。

子 Agent 需要独立上下文

子 Agent 不应该无脑继承主 Agent 的全部上下文。它通常只需要:

  1. 当前子任务描述。
  2. 必要的背景信息。
  3. 被授权的 Skills。
  4. 对应的安全规则。
  5. 输出格式要求。

这样可以减少 token 消耗,也能避免子 Agent 接触不该使用的工具或记忆。

子 Agent 的配置可以继承,也可以覆盖

多 Agent 系统里,如果每个子 Agent 都完整复制一份配置,会导致配置爆炸。更合理的方式是:未单独定义的配置回落到全局默认值,特定子 Agent 只覆盖自己需要的部分。

可以抽象成这样:

{
  "defaults": {
    "model": "gpt-4.1",
    "temperature": 0.2,
    "maxTokens": 8000,
    "skills": ["memory_search", "memory_get"]
  },
  "subagents": {
    "mail-agent": {
      "skills": ["mail.search", "mail.get", "memory_search"]
    },
    "writer-agent": {
      "temperature": 0.5,
      "skills": ["doc.create", "style.check"]
    }
  }
}

mail-agent 只覆盖技能列表,模型等配置继承默认值;writer-agent 覆盖了温度和技能,用来适配写作任务。

多 Agent 适合和不适合的场景

场景是否适合多 Agent原因
单轮简单问答不适合拆分成本高于收益
明确分工的复杂任务适合检索、分析、写作、审校可以并行或串行协作
高风险工具执行适合可以把危险能力隔离到专用 Agent
强依赖统一上下文的推理谨慎使用拆分后可能丢失全局语义
多渠道客服分流适合不同 Agent 可以处理不同客户、语言或业务线

多 Agent 的价值在于任务拆解和权限隔离,而不是为了形式上“看起来更智能”。


一条钉钉消息在 OpenClaw 里的完整旅程

把所有模块串起来,那句钉钉消息会经历这样的路径:

用户在钉钉发送消息
        ↓
钉钉通道插件接收原始事件
        ↓
插件把平台消息转换成 MsgContext
        ↓
Gateway 对入站上下文做最终化处理
        ↓
系统生成幂等键,检查是否重复
        ↓
命令系统判断是否为 /stop 等控制命令
        ↓
WebSocket 或前端通道收到 started 状态
        ↓
路由系统根据绑定规则找到目标 Agent
        ↓
系统生成 sessionKey
        ↓
消息进入对应会话车道排队
        ↓
Agent Runtime 获得执行权
        ↓
加载 Bootstrap、Memory、Skills、历史记录和当前消息
        ↓
上下文预算检查,必要时压缩历史和工具结果
        ↓
LLM 开始推理
        ↓
Runtime 根据模型意图调用工具或子 Agent
        ↓
工具结果回填给模型继续推理
        ↓
主 Agent 生成最终答复
        ↓
回复分发器把结果投递回钉钉
        ↓
会话转录写入 JSONL
        ↓
sessions.json 更新元数据
        ↓
必要信息进入长期记忆或索引
        ↓
运行时释放资源

这条链路体现出 OpenClaw 的核心设计:把一次用户输入变成一个可治理的任务生命周期。


OpenClaw 的工程取舍

OpenClaw 的设计价值可以从几个角度看。

设计点解决的问题代价
通道插件隔离平台差异,新增通道不用改核心逻辑插件规范要设计清楚
MsgContext统一内部消息模型适配阶段需要补齐大量边缘字段
幂等键防止重复投递导致重复执行依赖平台提供可靠消息 ID
会话车道保证同一会话上下文不乱序同一会话吞吐会被串行限制
Bootstrap 文件Agent 身份和规则可配置需要控制提示词大小
Skills 系统让模型理解能力边界和调用方式技能描述质量会影响调用效果
Memory 检索把历史记忆变成显式动作需要维护索引和召回质量
上下文压缩防止窗口爆掉摘要可能丢失细节
多 Agent支持任务拆解和权限隔离调度复杂度和 token 成本会上升

OpenClaw 更像一个 Agent 网关和运行时,而不是一个单纯的聊天应用。它把消息入口、会话治理、上下文管理、技能调用、持久化存储和多 Agent 协作放进同一条工程链路里。

如果要自己实现一个 Mini-OpenClaw,最小闭环可以从这几块开始:

flowchart LR
    A[统一消息结构 MsgContext] --> B[路由到 Agent]
    B --> C[sessionKey 会话隔离]
    C --> D[上下文组装]
    D --> E[LLM 调用]
    E --> F[工具调用]
    F --> E
    E --> G[回复投递]
    G --> H[会话持久化]

先把这条最小链路跑通,再逐步加入幂等、车道、Skills 过滤、Memory 检索、上下文压缩和多 Agent 调度,系统会比一开始就堆满复杂功能更容易维护。


评论