OpenClaw路由源码深度解析:基本概念与实现机制
摘要
OpenClaw路由通过BindingTable按tier优先级匹配消息,tier越小越优先,次序为peer、guild、account、c
上节课我们梳理了通道的概念——OpenClaw 能够接收来自不同平台的消息,统一封装成 InboundMessage 格式,再交给 run_agent_turn 去调用 Agent。今天聊路由。简单说,路由就是给消息找个对口的 Agent:不同来源、不同层级的消息,该由谁响应?有了路由表,这件事就清晰了。
1. 概念解析
1.1. 路由表(BindingTable)
核心单位叫 Binding,它把一条入站消息和对应的 Agent 规则绑定在一起。每条规则定义了:什么条件下、由哪个 Agent 出场。BindingTable 在匹配消息时,会按照 tier 从小到大的顺序依次检查——越具体的规则越优先。
@dataclass
class Binding:
agent_id: str
tier: int # 1-5, 越小越具体
match_key: str # "peer_id" | "guild_id" | "account_id" | "channel" | "default"
match_value: str # 例如 "telegram:12345", "discord", "*"
priority: int = 0 # 同层内, 越大越优先
参数含义很直观:agent_id 目标 Agent 的唯一 ID;tier 表示匹配层级,从 1 到 5,数值越小越优先;match_key 指定按什么字段匹配(peer_id、channel、default、guild_id、account_id);match_value 是具体匹配值;priority 在同层内决定谁的优先级更高。
举个例子,看下面这组绑定:
bt = BindingTable()
bt.add(Binding(agent_id="luna", tier=5, match_key="default", match_value="*"))
bt.add(Binding(agent_id="sage", tier=4, match_key="channel", match_value="telegram"))
bt.add(Binding(agent_id="sage", tier=1, match_key="peer_id",
match_value="discord:admin-001", priority=10))
它们的匹配逻辑是这样的:
luna被设为 tier=5 的默认兜底——如果前 1~4 层都没有命中,那就由 luna 响应。sage的 tier=4 规则匹配 channel 为 telegram——只要消息来自 Telegram,且没有更具体的 1~3 层规则,就由 sage 处理。sage还有一条 tier=1 规则,专门匹配用户 ID 为discord:admin-001的消息——这是最具体的层级,一旦命中,直接使用 sage。
1.2. 路由解析
解析的过程就是按 tier 从小到大遍历 BindingTable,第一个匹配的就是最终结果。具体实现如下:
def resolve(self, channel: str = "", account_id: str = "",
guild_id: str = "", peer_id: str = "") -> tuple[str | None, Binding | None]:
"""遍历第1-5层, 第一个匹配的获胜。返回 (agent_id, matched_binding)。"""
for b in self._bindings:
if b.tier == 1 and b.match_key == "peer_id":
if ":" in b.match_value:
if b.match_value == f"{channel}:{peer_id}":
return b.agent_id, b
elif b.match_value == peer_id:
return b.agent_id, b
elif b.tier == 2 and b.match_key == "guild_id" and b.match_value == guild_id:
return b.agent_id, b
elif b.tier == 3 and b.match_key == "account_id" and b.match_value == account_id:
return b.agent_id, b
elif b.tier == 4 and b.match_key == "channel" and b.match_value == channel:
return b.agent_id, b
elif b.tier == 5 and b.match_key == "default":
return b.agent_id, b
return None, None
可以看到,tier 1 是 peer 级匹配(最具体),tier 2 是 guild 级,tier 3 是 account 级,tier 4 是 channel 级,tier 5 是默认兜底。整个流程像漏斗一样,从最精确到最泛化。
1.3. 会话隔离
通过 dm_scope 参数,就能控制私聊会话的隔离粒度。说白了,就是决定不同用户、不同通道的聊天历史是否共享——粒度越细,越能实现“互不干扰”。
底层逻辑是:根据 dm_scope 构建不同的 session key,生成对应的 session.json 文件,序列化后发给 LLM。
# ---------------------------------------------------------------------------
# 会话键构建
# ---------------------------------------------------------------------------
# dm_scope 控制私聊隔离粒度:
# main -> agent:{id}:main
# per-peer -> agent:{id}:direct:{peer}
# per-channel-peer -> agent:{id}:{ch}:direct:{peer}
# per-account-channel-peer -> agent:{id}:{ch}:{acc}:direct:{peer}
def build_session_key(agent_id: str, channel: str = "", account_id: str = "",
peer_id: str = "", dm_scope: str = "per-peer") -> str:
aid = normalize_agent_id(agent_id)
ch = (channel or "unknown").strip().lower()
acc = (account_id or "default").strip().lower()
pid = (peer_id or "").strip().lower()
if dm_scope == "per-account-channel-peer" and pid:
return f"agent:{aid}:{ch}:{acc}:direct:{pid}"
if dm_scope == "per-channel-peer" and pid:
return f"agent:{aid}:{ch}:direct:{pid}"
if dm_scope == "per-peer" and pid:
return f"agent:{aid}:direct:{pid}"
return f"agent:{aid}:main"
从代码可以直观看到:main 是所有用户共享一个对话;per-peer 是按私聊对象隔离;per-channel-peer 则把通道也纳入区分;per-account-channel-peer 粒度最细,连账号都考虑进去了。
2. 流程设计
下面这张流程图梳理了整个消息处理链路。入口有两个:CLI 和 WebSocket 客户端。消息进来后,如果指定了 force_agent,就直接用那个 Agent 响应该消息;否则进入路由表解析——通过 BindingTable 找到匹配的 Agent,再用 build_session_key 生成会话键,决定不同用户/通道的消息是否共享同一段历史记录。

以上就是路由的核心逻辑。配合上节课的通道概念,你已经能理解 OpenClaw 如何根据消息来源灵活调度不同的 Agent 了——既支持细粒度的私聊隔离,也能用默认 Agent 兜底,整个设计相当精炼。
来源:互联网
本网站新闻资讯均来自公开渠道,力求准确但不保证绝对无误,内容观点仅代表作者本人,与本站无关。若涉及侵权,请联系我们处理。本站保留对声明的修改权,最终解释权归本站所有。