6.4 多代理路由

生成模型:Claude Opus 4.6 (anthropic/claude-opus-4-6) Token 消耗:输入 ~300,000 tokens,输出 ~24,000 tokens(本章合计)


在单代理模式下,所有消息都由同一个 Agent 处理。但在实际使用场景中,用户可能希望不同的通道、账号甚至不同的聊天对象由不同的 Agent 来服务——例如一个"工作"Agent 处理 Slack 消息,一个"生活"Agent 处理 Telegram 消息,一个"编程"Agent 专门处理来自特定 Discord 频道的请求。OpenClaw 的多代理路由(Multi-Agent Routing)机制就是为此而设计的。

6.4.1 多代理配置:agents.list[] 与绑定

多代理的核心配置由两部分组成:代理列表agents.list)和绑定规则bindings)。

代理列表

代理列表定义了系统中存在哪些 Agent 以及各自的属性。每个 Agent 可以拥有独立的模型配置、技能过滤器、身份信息和沙箱策略:

// src/config/types.agents.ts
export type AgentConfig = {
  id: string;            // 代理唯一标识符
  default?: boolean;     // 是否为默认代理
  name?: string;         // 显示名称
  workspace?: string;    // 工作区目录
  agentDir?: string;     // 代理状态目录
  model?: AgentModelConfig;  // 模型配置(主模型 + 回退列表)
  skills?: string[];     // 技能白名单(省略 = 全部技能)
  identity?: IdentityConfig; // 身份配置(名称/头像/主题色)
  groupChat?: GroupChatConfig; // 群聊配置
  subagents?: {          // 子代理配置
    allowAgents?: string[];  // 允许派生的代理 ID 列表
    model?: AgentModelConfig; // 子代理默认模型
  };
  sandbox?: { ... };     // 沙箱隔离策略
  tools?: AgentToolsConfig;  // 工具权限配置
};

export type AgentsConfig = {
  defaults?: AgentDefaultsConfig;  // 全局默认配置
  list?: AgentConfig[];            // 代理列表
};

一个典型的多代理配置示例如下:

默认代理的解析

当系统需要确定"默认代理"时,resolveDefaultAgentId 函数按以下优先级进行选择:

解析逻辑如下:

  1. 如果 agents.list 为空,返回常量 DEFAULT_AGENT_ID(值为 "main"

  2. 如果有一个或多个代理标记了 default: true,取第一个

  3. 如果没有代理标记 default,取列表中的第一个

  4. 多个 default: true 会触发警告,但不会报错

衍生解释normalizeAgentId 函数执行字符串的 trim + lowercase 操作,确保 "Home"" home""HOME" 都被视为同一个代理 ID。这种标准化(Normalization)是分布式系统中避免标识符歧义的常见做法。

绑定类型定义

绑定(Binding)是将消息路由到特定代理的规则:

绑定配置示例:

这三条绑定的含义是:来自 Slack 工作区 T01234567 的消息交给 work 代理处理,来自 Discord 服务器 123456789 的消息交给 code 代理处理,来自 Telegram personal 账号的消息交给 home 代理处理。

6.4.2 基于通道/账号/对等方的路由规则

路由解析是多代理系统中最关键的决策环节。resolveAgentRoute 函数实现了一套多级匹配(Multi-Level Matching)策略,从最精确的匹配条件逐级回退到最宽泛的匹配。

路由输入与输出

路由解析的输入包含消息的完整来源信息:

路由输出包含解析后的代理 ID 和会话键:

六级匹配优先级

resolveAgentRoute 的核心是一个优先级递减的匹配链。下面是完整的匹配流程:

下表总结了六级匹配优先级:

优先级
matchedBy
匹配条件
典型场景

1

binding.peer

channel + accountId + peer(kind + id)

特定用户/群组的专属代理

2

binding.peer.parent

channel + accountId + parentPeer

线程继承父消息的代理绑定

3

binding.guild

channel + accountId + guildId

Discord 服务器级别绑定

4

binding.team

channel + accountId + teamId

Slack 工作区级别绑定

5

binding.account

channel + accountId(精确)

按通道账号绑定

6

binding.channel

channel + accountId=*

整个通道的默认代理

兜底

default

无匹配

使用系统默认代理

预过滤机制

匹配链的第一步不是直接遍历所有绑定,而是先进行预过滤:只保留通道和账号 ID 同时匹配的绑定。这意味着一条绑定只有在 channel 字段与消息来源通道一致时,才会进入后续的精细匹配。

账号 ID 的匹配有三种情况:

  1. 绑定未指定 accountId(或为空)——只匹配默认账号(DEFAULT_ACCOUNT_ID

  2. 绑定指定 accountId: "*"——匹配任意账号

  3. 绑定指定具体 accountId——精确匹配

线程继承

优先级 2 的"父级对等方继承"是一个值得注意的设计。在 Slack 和 Discord 中,用户可以在消息下方创建线程(Thread)。如果线程本身没有直接匹配到绑定规则,系统会检查线程所属的父消息(parentPeer)是否匹配某个绑定。这确保了"在某个频道下创建的线程"继承该频道的代理分配。

代理 ID 验证

choose 辅助函数中调用了 pickFirstExistingAgentId,它会验证绑定中引用的代理 ID 是否确实存在于 agents.list 中:

如果绑定引用了一个不存在的代理 ID,系统不会报错,而是静默回退到默认代理。这是一种防御性设计——配置错误不应导致消息无法处理。

6.4.3 子代理注册表(Subagent Registry)

除了静态的多代理路由外,OpenClaw 还支持动态子代理(Subagent)机制:一个正在运行的 Agent 可以在运行时派生出子代理来执行后台任务,任务完成后自动向父代理汇报结果。

衍生解释:子代理模式类似于操作系统中的 fork() + wait() 模式——父进程(Parent Process)创建子进程执行特定任务,子进程完成后将结果返回给父进程。在 OpenClaw 中,父 Agent 通过 sessions_send 工具派生子 Agent,子 Agent 完成后通过"汇报流程"(Announce Flow)将结果注入父 Agent 的上下文。

SubagentRunRecord 数据模型

子代理注册表是一个内存中的 Map<string, SubagentRunRecord>,用于跟踪所有正在运行的子代理:

注册与生命周期

当父 Agent 调用工具派生子代理时,registerSubagentRun 函数被调用:

这个函数做了四件关键的事:

  1. 注册记录——在内存 Map 中创建条目

  2. 启动监听器——订阅 Agent 生命周期事件,以便在子代理结束时立即感知

  3. 持久化——将注册表写入磁盘,支持进程重启后恢复

  4. 异步等待——通过 Gateway 的 agent.wait RPC 方法等待子代理执行完成

事件监听与完成检测

注册表通过两条路径检测子代理的完成:

路径 1:进程内生命周期事件

路径 2:跨进程 RPC 等待

双路径设计的原因是:子代理可能在同一进程内以嵌入模式运行(路径 1 覆盖),也可能在独立进程中运行(路径 2 覆盖)。两条路径中只有一条会触发汇报——beginSubagentCleanup 函数通过 cleanupHandled 标志保证幂等性。

汇报流程(Announce Flow)

当子代理完成后,runSubagentAnnounceFlow 函数负责将结果注入父代理的上下文:

汇报流程的关键步骤:

  1. 读取子代理回复——从子会话的转录记录中读取最后一条 assistant 消息

  2. 构建统计——包括运行时长、Token 消耗、预估成本等

  3. 构建触发消息——将子代理的输出和统计信息包装成一条发给父代理的消息

  4. 智能注入——如果父代理正在忙碌(有活跃的 Agent 循环),使用队列机制;否则直接发送

  5. 清理——根据 cleanup 策略,可选地删除子代理的会话和转录

子代理系统提示词

每个子代理在创建时都会收到一个专用的系统提示词,明确告知它的角色和限制:

这个系统提示词的设计体现了"最小权限原则"——子代理只被授权完成指定任务,不能发起心跳、主动联系用户或创建持久化状态。

归档与清理

注册表内建了一个清扫器(Sweeper),定期检查已完成子代理的过期状态:

清扫器每 60 秒运行一次。默认的归档过期时间为 60 分钟(可通过 agents.defaults.subagents.archiveAfterMinutes 配置)。当所有子代理记录都已清理完毕,清扫器自动停止,避免不必要的定时器开销。

进程重启恢复

注册表支持持久化到磁盘和重启后恢复:

resumeSubagentRun 会检查每条恢复的记录:如果子代理已经结束但汇报尚未完成,立即触发汇报;如果子代理仍在运行,重新启动 agent.wait 等待。

agents.list RPC 方法

Gateway 提供了 agents.list RPC 方法,供客户端查询当前配置的所有代理:

listAgentsForGateway 函数收集代理列表并附带身份信息(名称、头像 URL、主题色等),返回结构如下:

客户端(如 Web 控制台和原生应用)通过这个 RPC 方法获取代理列表,从而在 UI 中展示代理选择器和对应的身份标识。


本节小结

  1. 多代理配置由两部分组成:agents.list(代理定义)和 bindings(路由规则),每个代理可以拥有独立的模型、技能、身份和沙箱配置。

  2. 路由解析采用六级优先级匹配——从精确的对等方匹配(peer)到通道级别匹配(channel),最后回退到默认代理,确保每条消息都能找到处理者。

  3. 预过滤机制先按 channel + accountId 筛选候选绑定,再进行精细匹配,避免无关绑定干扰路由结果。

  4. 子代理注册表管理动态派生的子代理,通过双路径完成检测(进程内事件 + 跨进程 RPC)确保不遗漏任何完成通知。

  5. 汇报流程将子代理结果自动注入父代理上下文,支持队列化注入(父代理忙碌时)和直接发送两种模式。

  6. 持久化与恢复机制保证进程重启不会丢失子代理状态,归档清扫器自动回收过期记录。

Last updated