← 返回博客
OpenClaw 多 Agent:谁该管什么事

一台 Gateway,多个 Agent:谁该管谁的事

凡是用 OpenClaw 的人,迟早要面对一个问题:一个 agent 不够用了。

工作消息要一个 agent 处理,家庭群要另一个 agent 处理,还有个 agent 专门跑 Opus 做深度分析。三个 agent,三种人格,三套工作区,三份认证信息,三段会话历史。彼此之间,互不干扰。

这个需求合不合理?完全合理。一个人的生活本来就有不同的面,凭什么只给一张脸?

OpenClaw 原生支持多 agent。不需要跑多个实例,不需要反向代理,不需要任何额外的基础设施。一个 Gateway 进程,想挂多少 agent 就挂多少。

这篇文章从头到底把多 agent 的设置讲透。

先搞清楚一个 agent 到底是什么东西

很多人把 agent 理解成一段 prompt,或者一个模型。这个理解是错的。

一个 agent 是一个完整的、独立的作用域。它有三样家当:

  • 工作区:文件、AGENTS.md、SOUL.md、USER.md、本地笔记、人设规则。路径在 ~/.openclaw/workspace-<agentId>
  • 状态目录(agentDir):认证配置、模型注册表、每 agent 的配置文件。路径在 ~/.openclaw/agents/<agentId>/agent
  • 会话存储:聊天历史和路由状态。路径在 ~/.openclaw/agents/<agentId>/sessions

这里有一条铁律必须记住:认证配置是每个 agent 独立的。每个 agent 从自己的位置读:

~/.openclaw/agents/<agentId>/agent/auth-profiles.json

主 agent 的凭证不会自动共享给其他 agent。你要是图省事,让两个 agent 共用一个 agentDir,认证冲突、会话串线,早晚出事。想让两个 agent 用同样的凭证,老老实实把 auth-profiles.json 复制一份过去。

说"不共享",其实没那么绝对

上面说的"不会自动共享",在文件系统层面是对的。但运行时行为比这更微妙。

源码里的 ensureAuthProfileStore 实际上做了一次合并加载:先读 main agent 的 auth-profiles.json 作为 base,再读目标 agent 的作为 override,两者 merge。这意味着:

  • 非 main agent 自己没配某个 provider 的凭证,它会回退到 main agent 的凭证
  • 非 main agent 配了同名 profile,它的凭证覆盖 main 的
  • usage stats 和 profile order 也会合并

所以实际行为是:main agent 的凭证作为兜底层对所有 agent 可见,每个 agent 可以选择性地覆盖特定 profile。比"完全隔离"宽松,比"完全共享"安全。一个折中,而且是个合理的折中。

Agent ID 不是随便起的

源码里的校验规则是:

  • 合法 ID 需要匹配 /^[a-z0-9][a-z0-9_-]{0,63}$/i——字母数字开头,最长 64 字符,只允许字母、数字、下划线、连字符
  • 不合法的字符会被自动替换为连字符,首尾的连字符会被去掉
  • 所有 ID 统一转小写存储
  • 空 ID 回退到 "main"
  • openclaw agents add 命令拒绝 main 这个保留 ID

什么都不配的时候:单 agent 模式

你装完 OpenClaw,什么都不动,跑的就是单 agent:

  • agentId 默认 main
  • 会话 key 为 agent:main:main
  • 工作区默认在 ~/.openclaw/workspace
  • 状态目录默认在 ~/.openclaw/agents/main/agent

这是开箱即用的状态。后面讲的所有东西,都是在这个基础上往外扩展。

添加 agent 最快的路:CLI 向导

不需要手写 JSON。内置向导帮你办:

openclaw agents add work

向导一步步引导你设置工作区、绑定规则。完成后验证一下:

openclaw agents list --bindings

所有 agent 和它们的路由绑定,一目了然。

向导背后干了什么

源码里有几个值得注意的行为:

  • 只要命令带了 --workspace--name 之类的 flag,向导直接跳过交互流程,用 flag 值。有 flag 就认为你知道自己要什么
  • 交互模式下,向导会问你要不要把 default agent 的 auth-profiles.json 复制一份到新 agent。这是目前最安全的凭证初始化方式
  • agents list --bindings 不仅列出绑定关系,还会标注哪些 agent 没有显式绑定、靠 default 回退接收消息

手写配置:openclaw.json

全部多 agent 配置集中在 ~/.openclaw/openclaw.json(JSON5 格式)。核心两块:agents.list 定义 agent,bindings 定义消息往哪走。

场景一:两个 WhatsApp 号,两个 agent

一个人用私人号处理生活,用商务号处理工作。两个号的消息分别路由到两个 agent:

{
  agents: {
    list: [
      {
        id: "home",
        default: true,
        name: "Home",
        workspace: "~/.openclaw/workspace-home",
        agentDir: "~/.openclaw/agents/home/agent",
      },
      {
        id: "work",
        name: "Work",
        workspace: "~/.openclaw/workspace-work",
        agentDir: "~/.openclaw/agents/work/agent",
      },
    ],
  },

  bindings: [
    { agentId: "home", match: { channel: "whatsapp", accountId: "personal" } },
    { agentId: "work", match: { channel: "whatsapp", accountId: "biz" } },

    // 把私人号下的某个工作群也发到 work agent
    {
      agentId: "work",
      match: {
        channel: "whatsapp",
        accountId: "personal",
        peer: { kind: "group", id: "1203630...@g.us" },
      },
    },
  ],

  channels: {
    whatsapp: {
      accounts: {
        personal: {},
        biz: {},
      },
    },
  },
}

每个 accountId 对应一个 WhatsApp 登录实例。凭证默认存在 ~/.openclaw/credentials/whatsapp/<accountId>/

场景二:按渠道分割,WhatsApp 日常,Telegram 深度

WhatsApp 走快模型处理日常闲聊,Telegram 走 Opus 干正事:

{
  agents: {
    list: [
      {
        id: "chat",
        name: "Everyday",
        workspace: "~/.openclaw/workspace-chat",
        model: "anthropic/claude-sonnet-4-5",
      },
      {
        id: "opus",
        name: "Deep Work",
        workspace: "~/.openclaw/workspace-opus",
        model: "anthropic/claude-opus-4-5",
      },
    ],
  },
  bindings: [
    { agentId: "chat", match: { channel: "whatsapp" } },
    { agentId: "opus", match: { channel: "telegram" } },
  ],
}

有多个 WhatsApp 账户的话,绑定里要加 accountId

场景三:同一个渠道,某个人走不同的 agent

和场景二同样的 agent 定义(chat + opus),只改 bindings。大部分 WhatsApp 消息走快速 agent,但某个特定联系人走 Opus:

{
  bindings: [
    // peer 绑定更具体,自动优先
    { agentId: "opus", match: { channel: "whatsapp", peer: { kind: "dm", id: "+15551234567" } } },
    { agentId: "chat", match: { channel: "whatsapp" } },
  ],
}

peer 绑定始终优先于渠道级规则,数组顺序无所谓。但写在前面更好读。

场景四:一个号,多个人用

你和合伙人共用一个 WhatsApp 号码,各自要有独立的 agent。按发送者手机号分割:

{
  agents: {
    list: [
      { id: "alex", workspace: "~/.openclaw/workspace-alex" },
      { id: "mia", workspace: "~/.openclaw/workspace-mia" },
    ],
  },
  bindings: [
    { agentId: "alex", match: { channel: "whatsapp", peer: { kind: "dm", id: "+15551230001" } } },
    { agentId: "mia", match: { channel: "whatsapp", peer: { kind: "dm", id: "+15551230002" } } },
  ],
  channels: {
    whatsapp: {
      dmPolicy: "allowlist",
      allowFrom: ["+15551230001", "+15551230002"],
    },
  },
}

要注意:回复仍然来自同一个 WhatsApp 号,没有每 agent 的发送者身份。私信的访问控制(allowlist)也是全局的,不是按 agent 独立的。

消息往哪走:路由的优先级

绑定是确定性的,最具体的规则自动赢。

源码里的实现不是给每条 binding 算一个分数然后排序,而是定义了 8 个有序 tier,从最具体到最宽泛逐层扫描。每个 tier 只在上一个 tier 没命中时才被评估:

  1. binding.peer(精确匹配某个人或某个群)
  2. binding.peer.parent(线程/话题的父级 peer 匹配)
  3. binding.guild+roles(Discord guild + 角色交叉匹配)
  4. binding.guild(Discord)
  5. binding.team(Slack)
  6. binding.account(渠道 accountId 匹配)
  7. binding.channel(渠道级匹配,accountId: "*"
  8. 回退到默认 agent(agents.list[].default: true,没标的话就取第一个)

同 tier 内的顺序有没有关系

跨 tier 不用纠结——peer 级 binding 永远赢 channel 级的,无论写在数组哪个位置。但同一个 tier 内如果有多条 binding 都匹配,第一个(按 bindings 数组原始顺序)胜出。两条 peer binding 指向同一个群但路由到不同 agent,写在前面的赢。

子线程的消息去哪了

第 2 层 binding.peer.parent 容易被忽视。Discord 论坛帖子或 Telegram 话题里的消息,即使帖子本身没有 binding,也会继承父 peer 的路由。你只需要给父级(频道/群组)配 binding,下面所有线程自动跟着走。

想看更多实战拆解?

AI、工程与实验,每月 1–2 封。

无垃圾邮件,随时取消。

groupchannel 写哪个都行

源码里 peerKindMatchesgroupchannel 视为同义词——binding 里写 kind: "group" 也能匹配 kind: "channel" 的 peer,反之亦然。不同平台对"群组"叫法不同,OpenClaw 替你兜了底。不用纠结。

路由引擎有两层缓存,性能不是问题。

Session Key:你以为的和实际的

你可能以为会话 key 就是 agent:<agentId>:main,但这只是默认情况。完整的 session key 取决于 dmScope 配置:

dmScopeDM 的 Session Key 格式
main(默认)agent:<agentId>:main
per-peeragent:<agentId>:direct:<peerId>
per-channel-peeragent:<agentId>:<channel>:direct:<peerId>
per-account-channel-peeragent:<agentId>:<channel>:<accountId>:direct:<peerId>

群组/频道消息的 key 始终包含完整的 channel 和 peer 信息:agent:<agentId>:<channel>:group:<groupId>

默认的 dmScope: "main" 意味着所有 DM 共享同一个会话——跟张三聊的上下文,李四发消息过来也能看到。想要每人一个独立会话,设 dmScope: "per-peer"

同一个人,不同平台,合并成一个会话

session.identityLinks 可以把不同平台的同一个人映射到同一个规范 ID:

{
  session: {
    dmScope: "per-peer",
    identityLinks: {
      "canonical-alice": ["telegram:12345678", "whatsapp:+15551234567"],
    },
  },
}

这样 Alice 在 Telegram 和 WhatsApp 上跟你聊天时,会路由到同一个会话 key,共享上下文。

不同 agent,不同信任:沙箱和工具权限

从 v2026.1.6 开始,每个 agent 可以有自己独立的沙箱模式和工具限制。

个人 agent 你放心让它在主机上跑,家庭群的 agent 呢?它面对的是你的家人、你的朋友,你不确定他们会发什么消息、触发什么操作。信任程度不同,权限自然不同。

个人 agent 全权限,家庭 agent 只读

{
  agents: {
    list: [
      {
        id: "personal",
        workspace: "~/.openclaw/workspace-personal",
        sandbox: {
          mode: "off",
        },
      },
      {
        id: "family",
        workspace: "~/.openclaw/workspace-family",
        sandbox: {
          mode: "all",
          scope: "agent",
        },
        tools: {
          allow: ["read"],
          deny: ["exec", "write", "edit", "apply_patch", "process", "browser"],
        },
      },
    ],
  },
}

scope 的默认值,文档说的和源码不一样

文档上常见的说法是 scope 默认为 "session",但源码告诉了一个不同的故事。resolveSandboxScope 的逻辑是:

  1. 显式设了 scope,用你设的
  2. 设了旧版的 perSession: true(布尔值),转换为 sessionperSession: false 转换为 shared
  3. 都没设的话,默认是 agent

实际上有三个 scope 选项:

scope含义scope key
session每个会话一个容器完整的 session key
agent每个 agent 一个容器(默认)agent:<agentId>
shared所有 agent 共享一个容器固定字符串 "shared"

容器名是 scope key 经过 slugify + SHA256 前 8 位哈希的结果,保证唯一且文件系统安全。

怎么把家庭 agent 钉死在一个群里

把家庭 agent 钉在一个 WhatsApp 群里,限制工具,设好提及模式:

{
  agents: {
    list: [
      {
        id: "family",
        name: "Family",
        workspace: "~/.openclaw/workspace-family",
        identity: { name: "Family Bot" },
        groupChat: {
          mentionPatterns: ["@family", "@familybot", "@Family Bot"],
        },
        sandbox: {
          mode: "all",
          scope: "agent",
        },
        tools: {
          allow: ["exec", "read", "sessions_list", "sessions_history",
                  "sessions_send", "sessions_spawn", "session_status"],
          deny: ["write", "edit", "apply_patch", "browser", "canvas", "nodes", "cron"],
        },
      },
    ],
  },
  bindings: [
    {
      agentId: "family",
      match: {
        channel: "whatsapp",
        peer: { kind: "group", id: "120363999999999999@g.us" },
      },
    },
  ],
}

allow/deny 列表限制的是工具,不是 skill。skill 如果需要跑可执行文件,确保 exec 在 allow 列表里,而且那个可执行文件在沙箱容器里存在。

工具过滤是怎么叠起来的

工具过滤是一层套一层的。每一层只能收窄,不能放宽:

  1. 工具 profile(tools.profileagents.list[].tools.profile
  2. Provider 工具 profile
  3. 全局工具策略(tools.allow / tools.deny
  4. Provider 工具策略
  5. Agent 级工具策略(agents.list[].tools.allow/deny
  6. Agent Provider 策略
  7. 群组工具策略
  8. 沙箱工具策略
  9. Subagent 工具策略

一旦上层 deny,下层加不回来。这是铁规矩。

有一个保护机制值得注意:如果你在 allow 列表里只写了插件的工具名,系统不会因此把所有核心工具(read、write、exec 等)干掉。代码检测到"插件-only allowlist"会自动忽略它对核心工具的影响。这个设计防止了一类常见的配置失误。

Skills:每 agent 独立还是全局共享

多 agent 环境下,每个 agent 有自己的工作区。所以 skills 的可见性规则很清楚:

  • 每 agent 独立的 skill:放在该 agent 的 <workspace>/skills 目录,只有这个 agent 看得到
  • 全局共享的 skill:放在 ~/.openclaw/skills,同一台机器上所有 agent 都能用
  • 额外的共享目录可以通过 skills.load.extraDirs 配置

同名 skill 出现在多个位置时,优先级是:工作区 > managed/local > bundled。

Agent 之间能不能互相说话

默认不能。Agent 之间是完全隔离的,除非你显式打开:

{
  tools: {
    agentToAgent: {
      enabled: false,  // 默认关闭
      allow: ["home", "work"],
    },
  },
}

这是安全设计上的选择。没有明确打开,agent 之间就是密不透风的两堵墙。

Allow 支持通配符

allow 列表支持 glob 模式。"*" 允许所有 agent 之间通信,"work-*" 允许所有以 work- 开头的 agent。同 agent 之间的通信(自己发给自己的另一个会话)始终允许,不受 enabled 开关影响。

不只是发消息:A2A 对话协议

A2A 通信不是单向发消息。底层是一套 ping-pong 协议:

  1. requester agent 通过 sessions_send 向 target agent 发消息
  2. 两个 agent 可以来回对话,最多 5 轮(硬上限)
  3. 任意一方回复 REPLY_SKIP 即可提前终止
  4. 对话结束后,系统可以把结果发到外部渠道。agent 回复 ANNOUNCE_SKIP 则静默结束
  5. timeout=0 时 fire-and-forget,发了就走

谁能看到谁的会话

Agent 对其他会话的可见性分四档:

级别含义
self只能看自己当前会话
tree(默认)能看自己 spawn 出来的子会话树
agent能看同一个 agent 下的所有会话
all能看所有 agent 的所有会话

沙箱环境会进一步收窄——默认限制到 spawned(等效于 tree),沙箱里的 agent 越不了界。

从单 agent 搬到多 agent

你已有的配置如果长这样:

{
  agents: {
    defaults: {
      workspace: "~/.openclaw/workspace",
      sandbox: { mode: "non-main" },
    },
  },
}

迁移很简单。把原来的单 agent 变成 agents.list 的第一个条目,标记为 default: true,然后往后加新 agent:

{
  agents: {
    list: [
      {
        id: "main",
        default: true,
        workspace: "~/.openclaw/workspace",
        sandbox: { mode: "off" },
      },
      {
        id: "work",
        workspace: "~/.openclaw/workspace-work",
      },
    ],
  },
}

旧的 agent.* 配置可以用 openclaw doctor 自动迁移。迁移后统一用 agents.defaults + agents.list 的格式。

默认 agent 怎么选

源码里 resolveDefaultAgentId 的逻辑很有防御性:

  1. agents.list 为空,返回 "main"
  2. 多个 agent 标了 default: true,只警告一次(不会反复刷日志),然后用第一个
  3. 没有任何 agent 标 default: true,用 agents.list 的第一个
  4. 提取出的 ID 为空字符串,回退到 "main"

工作区路径的匹配使用最长前缀优先——当根据工作区路径反查 agent ID 时,路径最长匹配的 agent 胜出。

配完了怎么验证

配置完了,检查四样东西:

  1. Agent 解析和绑定:
openclaw agents list --bindings
  1. 沙箱容器状态(用了沙箱的话):
docker ps --filter "name=openclaw-sbx-"
  1. 工具限制验证:给受限 agent 发一条消息,故意触发被 deny 的工具,确认被拦住了。

  2. 看日志:

tail -f "${OPENCLAW_STATE_DIR:-$HOME/.openclaw}/logs/gateway.log" | grep -E "routing|sandbox|tools"

路由调试可以开 verbose 日志。源码里每一步路由匹配都会输出 [routing] resolveAgentRoute[routing] match: matchedBy=... 的调试信息,帮你确认消息走的是哪条规则。注意:verbose 模式下路由缓存会被禁用,调完记得关。

改了配置要不要重启

大部分情况下不用。但不是所有配置都一样。

源码里有一套精细的热更新规则系统。每个配置路径被归类为三种之一:

类型行为涉及的配置
none(无需操作)修改即时生效agentsbindingstoolssessionidentityskillsroutingmessages
hot(热重载)重启对应子系统hookscron、各渠道连接、心跳、健康监控
restart(完整重启)需要 Gateway 进程重启pluginsgateway 本身、discoverybrowser

好消息是:多 agent 最常改的配置——agentsbindingstoolssession——全部属于第一类,真正的零开销即时生效。改完就是改完,不需要碰 Gateway。

需要重启的配置走 SIGUSR1 优雅重启,等当前队列处理完再动。每个渠道插件可以注册自己的热更新规则,WhatsApp 的配置改了只重启 WhatsApp 连接,不影响 Telegram。

踩过的坑

设了 mode: "all" 但 agent 没被沙箱?查一下是不是全局的 agents.defaults.sandbox.mode 在覆盖。Agent 级配置优先级更高,直接在 agents.list[].sandbox.mode 写死。

工具在 deny 列表里但仍然可用?检查工具过滤链的顺序。每一层只能收窄。看日志里的 [tools] filtering tools for agent:${agentId} 确认。另外检查 allow 列表是否只包含了插件工具名——源码会检测到"插件-only allowlist"并自动忽略它对核心工具的影响。

容器没有按 agent 隔离?之前说过,scope 默认值其实是 "agent"。如果没按预期隔离,检查是否有旧版 perSession 配置在干扰。

non-main 模式不符合预期?agents.defaults.sandbox.mode: "non-main" 判断的是 session.mainKey(默认 "main"),不是 agent id。源码逻辑很直白:session key 不等于 main session key 就沙箱。群组和频道的会话天然有自己的 key(格式是 agent:<agentId>:<channel>:group:<groupId>),永远不等于 agent:<agentId>:main,所以群组会话在 non-main 模式下一定被沙箱。你想让某个 agent 永远不沙箱,直接写 mode: "off",别绕弯子。

DM 消息上下文串了?默认 dmScope: "main" 下所有 DM 共享一个会话。张三跟你聊的内容,李四发消息时也能被 agent 看到。不是 bug,是默认行为。设成 dmScope: "per-peer" 就能隔离。

非 main agent 居然用了 main 的凭证?这是 ensureAuthProfileStore 的合并加载行为——main agent 的凭证作为兜底层存在。不希望回退,在非 main agent 的 auth-profiles.json 里显式覆盖对应 profile(哪怕覆盖为空)。

归结

回到开头那个问题:谁该管谁的事?

一个 OpenClaw Gateway 跑多个 agent,说到底就三件事:

  1. agents.list 里定义每个 agent(工作区、状态目录、沙箱模式、工具权限)
  2. bindings 定义消息路由规则(按渠道、按账户、按联系人或群组)
  3. 各 agent 的认证、会话、skills 独立运作,除非你显式共享

配置改完即时生效。路由引擎是确定性的分层匹配,不是模糊打分。认证隔离比文档说的宽松一点,main agent 是兜底层。A2A 通信有完整的对话协议和可见性控制,不只是一个开关。

这三件事,本质上是在回答一个更根本的问题:你怎么划分信任边界。哪个 agent 值得信任到什么程度,哪些消息该被谁看到,哪些工具该被谁碰到。技术上的 binding、scope、allow/deny,都是这个判断的表达形式。

工具不复杂。复杂的是你自己有没有想清楚。

新文章,直接发到你的邮箱。

AI、工程与实验,每月 1–2 封。

无垃圾邮件,随时取消。