第 10 章

消息路由:Bindings 规则引擎与八级优先级匹配算法

第十章:消息路由:Bindings 规则引擎与八级优先级匹配算法

10.1 为什么需要确定性路由

10.1.1 多 Agent 多渠道场景的混乱问题

设想一个典型的企业 OpenClaw 部署:

渠道:
  Discord 服务器 "Engineering"
    ├── #general(通用讨论)
    ├── #code-review(代码审查请求)
    ├── #ops-alerts(运维告警)
    └── 私信(DM)

Agents:
  agent_code_reviewer  - 专门做代码审查
  agent_ops_assistant  - 专门处理运维问题
  agent_general        - 通用对话助手
  agent_vip            - VIP 用户专属助手(更好的模型)

如果没有确定性路由,每条消息可能被任意一个 Agent 处理:

更严重的问题:如果路由规则有歧义,同一条消息可能被多个 Agent 同时处理,导致重复回复、状态冲突。

10.1.2 确定性路由的三个保证

OpenClaw 的 Bindings 路由引擎提供三个核心保证:

  1. 唯一性:每条消息只路由给一个 Agent(即使多条规则匹配,也只选最高优先级)
  2. 确定性:相同的消息在相同的配置下,永远路由到相同的 Agent
  3. 可预测性:通过查看配置即可预测任意消息的路由结果,无需观察运行时行为

10.2 八级优先级匹配算法

Bindings 的优先级从高到低排列,匹配时从 Level 1 开始向下评估,一旦找到匹配即停止:

优先级层级(从高到低):

Level 1: Peer match(精确 DM / 群组 ID 匹配)
Level 2: Parent peer match(线程继承父消息的路由)
Level 3: Guild ID + 角色(Discord 服务器内的角色匹配)
Level 4: Guild ID(Discord 服务器级别匹配)
Level 5: Team ID(团队级别匹配)
Level 6: Account ID(账号级别匹配)
Level 7: Channel 级别(渠道类型匹配)
Level 8: 默认 Agent 回退(兜底路由)

10.2.1 Level 1:Peer Match(精确 ID 匹配)

匹配逻辑: 消息的来源方(peer)与 Binding 配置的精确 ID 完全相同。

peer 的定义:

配置示例:

bindings:
  - name: "VIP Alice - Claude Opus"
    match:
      peer: "user:alice_discord_id_123"   # 精确匹配 Alice 的 Discord ID
    agent: agent_vip
    priority: 1  # 显式标注,确保排在最前

  - name: "Code Review Channel"
    match:
      peer: "channel:code_review_channel_id"  # 精确匹配代码审查频道
    agent: agent_code_reviewer

应用场景:

10.2.2 Level 2:Parent Peer Match(线程继承机制)

匹配逻辑: 消息是某个已有路由的消息的线程回复,继承父消息的路由结果。

为什么需要线程继承?

考虑以下场景:

Discord 对话:

[原始消息] Alice → #general:"帮我审查这段代码"
             └─ 被路由到 agent_code_reviewer

[线程回复] Bob → #general(回复 Alice 的消息):"我也想让它帮我看看"
             └─ 应该路由到哪里?

没有线程继承:Bob 的消息会走通用匹配,可能路由到 agent_general,而不是已经在处理这个代码审查对话的 agent_code_reviewer。

有线程继承:Bob 的消息识别出它是对 Alice 消息的回复,继承 Alice 消息的路由结果,也路由到 agent_code_reviewer。这保证了同一线程内的对话语境一致性

实现机制:

function resolveParentPeerMatch(message: IncomingMessage): Binding | null {
  // 查找父消息 ID
  const parentMessageId = message.replyToId || message.threadParentId;
  if (!parentMessageId) return null;
  
  // 查找父消息的路由结果
  const parentRouting = routingCache.get(parentMessageId);
  if (!parentRouting) return null;
  
  // 继承父消息的 Binding
  return parentRouting.binding;
}

配置: 线程继承是默认行为,无需显式配置。如需禁用:

bindings:
  - name: "General Channel"
    match:
      peer: "channel:general_id"
    agent: agent_general
    threadInheritance: false  # 禁用线程继承

10.2.3 Level 3:Guild ID + 角色

匹配逻辑: 消息来自特定 Discord 服务器(Guild),且发送者具有特定角色。

配置示例:

bindings:
  - name: "Engineering Team - Code Agent"
    match:
      guildId: "guild:my_company_discord"
      roles:
        - "role:engineer"
        - "role:senior_engineer"
        - "role:tech_lead"
    agent: agent_code_reviewer
    
  - name: "Management - Executive Agent"
    match:
      guildId: "guild:my_company_discord"
      roles:
        - "role:manager"
        - "role:director"
        - "role:vp"
    agent: agent_executive_assistant

角色匹配策略:

rolesMatchMode: "any"   # 发送者拥有列表中任意一个角色即匹配(默认)
rolesMatchMode: "all"   # 发送者必须拥有所有列出的角色才匹配

Discord 应用场景: 这是 OpenClaw 在 Discord 生态中最重要的路由机制。一个企业 Discord 服务器可以通过角色实现精细的 Agent 分流:

Discord 服务器 "My Company"
  角色: engineer → agent_code
  角色: sales → agent_crm
  角色: support → agent_helpdesk
  角色: @everyone → agent_general(Level 4 的兜底)

10.2.4 Level 4:Guild ID(服务器级别)

匹配逻辑: 消息来自特定 Discord 服务器,不限制角色。

bindings:
  - name: "Partner Company Discord"
    match:
      guildId: "guild:partner_company_discord"
    agent: agent_partner_support
    
  - name: "Internal Company Discord"
    match:
      guildId: "guild:my_company_discord"
    agent: agent_internal_general

典型用途:

10.2.5 Level 5:Team ID(团队级别)

匹配逻辑: 消息来源归属于特定 Team(OpenClaw 内部的团队概念,跨渠道组织单元)。

bindings:
  - name: "Frontend Team"
    match:
      teamId: "team:frontend"
    agent: agent_frontend
    
  - name: "Backend Team"
    match:
      teamId: "team:backend"
    agent: agent_backend

Team 的概念: Team 是 OpenClaw 中跨渠道的组织单元。一个用户可以属于多个 Team,Team 成员可以来自不同渠道(Discord、Slack、API)。

# config/teams.yaml
teams:
  - id: team:frontend
    name: "Frontend Engineering"
    members:
      - { type: "discord-user", id: "user:alice" }
      - { type: "slack-user", id: "U01234567" }
      - { type: "email", id: "[email protected]" }

10.2.6 Level 6:Account ID(账号级别)

匹配逻辑: 消息来源归属于特定账号(跨渠道身份关联后的统一账号)。

bindings:
  - name: "Enterprise Customer Alice"
    match:
      accountId: "account:enterprise_alice"
    agent: agent_enterprise
    model: "claude-opus-4-5"  # 更好的模型
    
  - name: "Free Tier Users"
    match:
      accountId: "account:free_*"  # 支持通配符
    agent: agent_free
    model: "claude-3-5-haiku"  # 经济型模型

账号 vs 渠道身份的关系:

统一账号(Account)
    ├── Discord: user:123
    ├── Slack: U04567
    └── Email: [email protected]

一个用户可能通过多个渠道联系 OpenClaw,Account ID 将这些身份统一,实现跨渠道一致的路由策略。

10.2.7 Level 7:Channel 级别(渠道类型匹配)

匹配逻辑: 根据消息的渠道类型(而非特定 ID)进行匹配。

bindings:
  - name: "All Discord DMs"
    match:
      channel:
        type: "discord-dm"
    agent: agent_dm_assistant
    
  - name: "All Slack DMs"
    match:
      channel:
        type: "slack-dm"
    agent: agent_slack_assistant
    
  - name: "All API Calls"
    match:
      channel:
        type: "api"
    agent: agent_api

常见渠道类型:

discord-dm          - Discord 私信
discord-channel     - Discord 频道消息
discord-thread      - Discord 线程
slack-dm            - Slack 私信
slack-channel       - Slack 频道
slack-thread        - Slack 线程
api                 - REST/WebSocket API 调用
webhook             - 外部 Webhook 触发

10.2.8 Level 8:默认 Agent 回退

匹配逻辑: 前 7 级均未匹配时的兜底路由。

bindings:
  - name: "Default Fallback"
    match: "*"          # 匹配所有
    agent: agent_general
    isDefault: true     # 标记为默认回退

如果没有配置默认 Agent:

{
  "type": "res",
  "ok": false,
  "error": {
    "code": "NO_ROUTE_FOUND",
    "message": "No binding matched the incoming message and no default agent is configured",
    "details": {
      "messageId": "msg_001",
      "source": { "channelType": "discord-dm", "userId": "user:unknown" }
    }
  }
}

最佳实践: 始终配置一个默认回退,避免路由失败让用户看到错误消息。


10.3 完整的路由匹配流程

收到新消息
      │
      ▼
提取消息属性:
  peer, parentId, guildId, roles[], teamId, accountId, channelType
      │
      ▼
Level 1: Peer Match
  │
  ├── 找到匹配的 Binding ──────────────→ 路由到对应 Agent,结束
  │
  └── 未找到
        │
        ▼
Level 2: Parent Peer Match
  │
  ├── 有父消息 & 父消息有路由记录 ──────→ 继承路由,结束
  │
  └── 无父消息或父消息无记录
        │
        ▼
Level 3: Guild ID + Roles
  │
  ├── 找到匹配(guildId AND role匹配) ─→ 路由到对应 Agent,结束
  │
  └── 未找到
        │
        ▼
Level 4: Guild ID Only
  │
  ├── 找到匹配(仅 guildId 匹配) ──────→ 路由到对应 Agent,结束
  │
  └── 未找到
        │
        ▼
Level 5: Team ID
  │
  ├── 找到匹配 ───────────────────────→ 路由到对应 Agent,结束
  │
  └── 未找到
        │
        ▼
Level 6: Account ID
  │
  ├── 找到匹配 ───────────────────────→ 路由到对应 Agent,结束
  │
  └── 未找到
        │
        ▼
Level 7: Channel Type
  │
  ├── 找到匹配 ───────────────────────→ 路由到对应 Agent,结束
  │
  └── 未找到
        │
        ▼
Level 8: Default Fallback
  │
  ├── 有默认 Agent ──────────────────→ 路由到默认 Agent,结束
  │
  └── 无默认 Agent ──────────────────→ 返回 NO_ROUTE_FOUND 错误

10.4 Bindings 配置示例

10.4.1 按渠道分流

# config/bindings.yaml

bindings:
  # Discord 代码审查频道 → 代码审查 Agent
  - name: "discord-code-review"
    match:
      peer: "channel:${DISCORD_CODE_REVIEW_CHANNEL_ID}"
    agent: agent_code_reviewer
    responseMode: "thread"   # 在线程中回复,保持频道整洁

  # Discord 运维告警频道 → 运维 Agent(静默模式,只在必要时回复)
  - name: "discord-ops-alerts"
    match:
      peer: "channel:${DISCORD_OPS_CHANNEL_ID}"
    agent: agent_ops
    responseMode: "reaction-only"  # 只用表情反应确认

  # Discord 所有 DM → 通用助手
  - name: "discord-dm-general"
    match:
      channel:
        type: "discord-dm"
    agent: agent_general

  # API 调用 → 无状态 API Agent
  - name: "api-requests"
    match:
      channel:
        type: "api"
    agent: agent_api
    sessionMode: "ephemeral"  # 不持久化会话

10.4.2 按账号分流(多层次服务)

bindings:
  # 企业账号 → 高端 Agent + 最佳模型
  - name: "enterprise-tier"
    match:
      accountId: "account:enterprise_*"
    agent: agent_enterprise
    model: "claude-opus-4-5"
    rateLimit:
      requestsPerHour: 500
      
  # 专业账号 → 标准 Agent
  - name: "pro-tier"
    match:
      accountId: "account:pro_*"
    agent: agent_pro
    model: "claude-3-5-sonnet"
    rateLimit:
      requestsPerHour: 100
      
  # 免费账号 → 轻量 Agent + 速度优先模型
  - name: "free-tier"
    match:
      accountId: "account:free_*"
    agent: agent_free
    model: "claude-3-5-haiku"
    rateLimit:
      requestsPerHour: 20
      
  # 默认回退
  - name: "default"
    match: "*"
    agent: agent_general
    model: "claude-3-5-haiku"

10.4.3 多 Agent 的 Discord 场景

bindings:
  # CTO 专属(最高优先级)
  - name: "cto-personal"
    match:
      peer: "user:${CTO_DISCORD_ID}"
    agent: agent_executive
    model: "claude-opus-4-5"
    
  # 工程角色(Level 3:guildId + role)
  - name: "engineers-code-agent"
    match:
      guildId: "guild:${COMPANY_DISCORD}"
      roles: ["role:engineer", "role:senior-engineer"]
    agent: agent_code
    
  # 产品角色
  - name: "product-team"
    match:
      guildId: "guild:${COMPANY_DISCORD}"
      roles: ["role:product-manager", "role:designer"]
    agent: agent_product
    
  # 公司 Discord 所有成员(Level 4:仅 guildId)
  - name: "company-general"
    match:
      guildId: "guild:${COMPANY_DISCORD}"
    agent: agent_company_general
    
  # 公开 Discord 服务器
  - name: "public-discord"
    match:
      guildId: "guild:${PUBLIC_COMMUNITY_DISCORD}"
    agent: agent_community
    
  # 默认回退
  - name: "default"
    match: "*"
    agent: agent_general

10.5 Session Freshness 评估

路由到 Agent 后,系统需要决定是使用现有 Session 还是创建新 Session。这由 Session Freshness 评估决定。

10.5.1 Freshness 评估标准

function isSessionFresh(session: Session, config: FreshnessConfig): boolean {
  const now = Date.now();
  
  // 标准 1:空闲超时
  const idleMs = now - session.lastActiveAt;
  if (idleMs > config.idleTimeoutMs) {
    return false;  // 会话已超时
  }
  
  // 标准 2:每日重置(默认凌晨 4 点)
  const sessionDate = new Date(session.lastActiveAt);
  const currentDate = new Date(now);
  
  const resetHour = config.dailyResetHour ?? 4;  // 默认凌晨 4 点
  
  // 判断是否跨越了今天的重置时间点
  const sessionDateResetTime = new Date(sessionDate);
  sessionDateResetTime.setHours(resetHour, 0, 0, 0);
  if (sessionDateResetTime < sessionDate) {
    // 重置时间在昨天,加一天
    sessionDateResetTime.setDate(sessionDateResetTime.getDate() + 1);
  }
  
  if (now > sessionDateResetTime.getTime()) {
    return false;  // 已经过了今天的重置时间
  }
  
  return true;  // 会话仍然新鲜
}

10.5.2 Freshness 配置

# config/sessions.yaml
sessionFreshness:
  idleTimeoutMs: 3600000    # 1 小时空闲超时(毫秒)
  dailyResetHour: 4         # 每天凌晨 4 点重置
  
  # 按 Agent 覆盖
  agentOverrides:
    agent_code:
      idleTimeoutMs: 7200000  # 代码任务允许 2 小时空闲
    agent_general:
      idleTimeoutMs: 1800000  # 通用对话 30 分钟空闲超时

10.5.3 Freshness 失效后的处理

Session Freshness 评估:

现有 Session 存在?
       │
      是 ──→ Session 是否 fresh?
      │            │
      │           是 ──→ 复用现有 Session(携带历史上下文)
      │            │
      │           否 ──→ 归档旧 Session,创建新 Session
      │
      否 ──→ 创建新 Session

归档的会话保留在 data/transcripts/ 中,用户可以通过 TUI 浏览历史会话。


10.6 路由失败的处理

10.6.1 无匹配规则

{
  "type": "event",
  "event": "session.error",
  "payload": {
    "code": "NO_ROUTE_FOUND",
    "message": "No binding matched the incoming message",
    "suggestions": [
      "Check if a default binding is configured",
      "Verify the channel ID is correct in your bindings config"
    ]
  }
}

10.6.2 Agent 不可用

{
  "type": "event",
  "event": "session.error",
  "payload": {
    "code": "AGENT_UNAVAILABLE",
    "message": "The matched agent 'agent_code' is not running",
    "fallbackAgent": "agent_general",
    "usedFallback": true
  }
}

10.6.3 路由调试工具

# 模拟路由决策(不实际发送消息)
openclaw route simulate \
  --peer "user:alice_discord_123" \
  --guild "guild:company_discord" \
  --roles "engineer,senior-engineer" \
  --channel-type "discord-dm"

# 输出:
# Level 1 (Peer Match):    agent_vip  ← 匹配!停止评估
# Final Route: agent_vip
# Session: sess_01HXYZ (fresh, last active 5 minutes ago)

# 查看所有 Binding 的匹配统计
openclaw bindings stats --period 24h

10.7 Bindings 的高级特性

10.7.1 条件路由(Context-based Routing)

bindings:
  - name: "Code Review - Working Hours"
    match:
      peer: "channel:code_review_id"
    conditions:
      # 只在工作时间(9-18点)路由到专业 Agent
      - timeRange:
          start: "09:00"
          end: "18:00"
          timezone: "Asia/Shanghai"
    agent: agent_code_reviewer
    
  - name: "Code Review - After Hours"
    match:
      peer: "channel:code_review_id"
    # 下班后路由到轻量模型
    agent: agent_code_reviewer_lite
    model: "claude-3-5-haiku"

10.7.2 负载均衡路由

bindings:
  - name: "Load Balanced Agents"
    match:
      channel:
        type: "api"
    agents:
      - agent: agent_api_1
        weight: 50
      - agent: agent_api_2
        weight: 50
    loadBalancing: "weighted-random"

10.7.3 路由优先级数值化

当多个 Level 相同的 Binding 同时匹配时,通过 priority 数值决定最终选择:

bindings:
  - name: "Alice VIP"
    match:
      peer: "user:alice"
    agent: agent_vip
    priority: 100    # 越高越优先(同 Level 内)
    
  - name: "Alice Normal"
    match:
      peer: "user:alice"
    agent: agent_general
    priority: 10     # 这条永远不会被选中(Alice 会被 priority:100 的规则匹配)

本章小结

Bindings 规则引擎是 OpenClaw 多 Agent 编排能力的基础:

  1. 确定性路由保证每条消息只路由给一个 Agent,消除多 Agent 竞争和重复响应
  2. 8 级优先级算法 从精确到模糊逐级降级,覆盖所有可能的路由场景
  3. Level 1(Peer Match) 用于 VIP 用户和特定频道的精确路由
  4. Level 2(Parent Peer Match) 通过线程继承保证对话语境的一致性
  5. Level 3(Guild ID + 角色) 是 Discord 场景中最重要的路由维度
  6. Level 4-6(Guild/Team/Account) 提供从服务器到团队到个人的分层路由
  7. Level 7(Channel 类型) 按渠道形态(DM/频道/API)分流
  8. Level 8(默认回退) 兜底防止路由失败
  9. Session Freshness 通过空闲超时和每日重置决定是复用现有会话还是创建新会话
  10. 路由失败处理 包括无匹配错误、Agent 不可用回退、调试工具支持

至此,《OpenClaw 完全指南》第六章至第十章已完整呈现了从 Gateway 控制平面、命令队列、Pi 框架到消息路由的完整技术链路。这五章构成了理解 OpenClaw 架构的核心知识骨干。

本章评分
4.5  / 5  (38 评分)

💬 留言讨论