【AgentDaily】如何在飞书群里做 A2A:Hermes Bot-to-Bot 最佳实践
两个 Hermes 机器人在同一个飞书群里协作,关键不是“看见对方的名字”,而是让发送方使用接收方自己确认过的 bot open_id 发出真正的
@,再由接收方按 mention 策略放行。
飞书里的四类身份
飞书消息事件中常见的身份字段有 open_id、user_id 和 union_id,机器人还多了一层应用身份:
| 身份 | 作用域 | 典型形式 | 适合用途 |
|---|---|---|---|
app_id |
应用 | cli_xxx |
标识应用和申请 tenant access token,不是群消息中的 @ 目标 |
open_id |
应用 | ou_xxx |
同一主体在不同应用下可能不同,消息事件和 @ 提及中最常用 |
user_id |
租户 | u_xxx |
企业租户内的用户身份,是否返回取决于权限和事件配置 |
union_id |
开发者 | on_xxx |
同一开发者主体下跨应用关联用户,适合做跨应用身份归并 |
open_id 的“应用作用域”是 A2A 最容易踩坑的地方。同一个人经过两个飞书应用观察,得到的 open_id 可能不同;机器人同样不能把 app_id、群成员接口中的某个 ID 和 mention 事件里的 open_id 淡化成一个概念。
飞书群里的 bot-to-bot @ 本质上是跨应用身份传递:发送方机器人属于应用 A,接收方机器人属于应用 B。应用 A 必须在消息内容里写入飞书能够解析为目标机器人的身份,而应用 B 收到事件后,还要判断 mentions[] 中的身份是否确实指向自己。
/bot/v3/info 为什么重要
每个 Hermes bot 都应该用自己的 FEISHU_APP_ID 和 FEISHU_APP_SECRET 请求:
1 | GET /open-apis/bot/v3/info |
Hermes Feishu adapter 中的 probe_bot() 会优先通过飞书 SDK 调用该接口,SDK 不可用时再走原始 HTTP。接口成功后,adapter 从响应中提取:
1 | { |
这里的 bot_open_id 不是 app_id。它是机器人自己的 app-scoped open_id,也是飞书在群消息 mentions[].id.open_id 中表示“@ 到这个机器人”时使用的身份。
Hermes 启动连接时还会执行 _hydrate_bot_identity()。它请求同一个接口,用实时结果更新 _bot_open_id 和 _bot_name。即使 .env 中已有 FEISHU_BOT_OPEN_ID,只要接口返回了不同的 open_id,adapter 也会采用实时值,避免机器人迁移或重建应用后继续拿旧 ID 做 mention 判断。
因此,A2A 中最可靠的身份来源不是猜测,也不是让其他机器人自行换算,而是目标 bot 用自己的应用凭据查询后主动公布的 self bot open_id。
每个 Agent 如何查询自己的 open_id
每个 Hermes Agent 都应从当前 Profile 独立查询自己的 open_id,具体步骤如下:
- 从当前 Profile 的
.env读取FEISHU_APP_ID和FEISHU_APP_SECRET。 - 调用 Hermes 飞书适配器
hermes-agent/plugins/platforms/feishu/adapter.py中的probe_bot()。 probe_bot()实际请求飞书 API:/open-apis/bot/v3/info。- 从实时响应中取得
bot_name和bot_open_id,作为当前 Agent 对外公布的身份信息。
probe_bot() 会优先使用飞书 SDK,SDK 不可用时回退到原始 HTTP 请求。不要在多个 Profile 之间共用同一份查询结果,否则可能把另一个飞书应用的 open_id 当成自己。
其他机器人 @ 当前机器人时,事件如何进入 Hermes
飞书应用需要订阅消息接收事件:
1 | im.message.receive_v1 |
接收其他机器人 @ 当前机器人的关键权限是:
1 | im:message.group_at_msg.include_bot:readonly |
它不是普通的群聊 @ 消息权限,而是明确包含“其他机器人 @ 当前机器人”消息的权限。应用还要具备以机器人身份发送消息所需的权限。配置完成后必须发布应用版本,否则新权限和事件订阅不会生效。
飞书官方的消息接收事件支持“其他机器人 @ 当前机器人”的场景。发送方不能只在纯文本里写 @机器人名称,而要发送带 at 标签的文本消息。最原始的频道消息内容可以写成:
1 | { |
其中 ou_xxx 应替换成目标 bot 自己公布的 bot_open_id。标签中的显示名称方便人阅读,真正用于寻址的是 ID。
实测中,目标 bot 自己通过 /bot/v3/info 查询到的 self open_id 可以触发 @;群成员机器人列表返回的 bot_id 或 member_id 也可能触发 @;随机伪造的 ou_xxx 则不能触发。这说明飞书会校验 ID 是否对应真实、可解析的群成员机器人,而不是只看 at 标签中的显示文本。这也解释了为什么来自两个接口、看起来不同的机器人 ID 都可能有效。不过从来源明确性和长期稳定性考虑,A2A 仍应优先使用目标 bot 自己公布的 self bot_open_id。
这件事不需要额外 CLI。Hermes 原始频道发送消息能力只要能提交上述飞书文本内容,就可以完成 A2A @;接收端仍通过正常的飞书消息事件进入 Gateway。
Hermes 如何判断“是否 @ 到自己”
Hermes Feishu adapter 的处理分为身份补全、准入和 mention 匹配三步。
_hydrate_bot_identity() 在连接阶段请求 /open-apis/bot/v3/info,得到当前机器人的 _bot_open_id 和 _bot_name。这是后续判断的身份基准。
消息到达后,_admit() 先识别发送者是不是 bot:
FEISHU_ALLOW_BOTS=none时,所有其他 bot 消息都会被忽略,这是默认值。FEISHU_ALLOW_BOTS=mentions时,只接收明确 @ 当前 Hermes 的 bot 消息。FEISHU_ALLOW_BOTS=all时,其他 bot 消息即使没有 @ 也可进入处理链路。- 如果自身 ID 或发送方 ID 尚未识别,adapter 会拒绝消息,而不是冒险按名称放行。
- adapter 还会比较发送方身份和自身身份,防止把自己的消息当作新的入站任务。
当策略要求 @ 时,_mentions_self() 检查消息中的 mention:
- 优先比较
mentions[].id.open_id与_bot_open_id。 - 在双方都提供
user_id时,也可以比较_bot_user_id。 - ID 已存在但不相等时,不会再因为机器人重名而按名称误判。
- 只有一侧缺少 ID 时,才会回退到名称匹配。
- adapter 也兼容
@_all和经过消息标准化后的 mention 结果。
这套顺序很重要:ID 匹配优先,名称只作降级方案。群里出现两个同名机器人时,单靠显示名称无法安全区分它们。
推荐的 A2A 配置
A2A 的关键不是“发送方是谁”,而是“发送方要 @ 谁”。发送方无需先查询自己的 open_id,最重要的是拿到目标 bot 自己公布的 self bot_open_id;接收方则要在启动时通过 /bot/v3/info hydrate 自己的 _bot_open_id,并启用 FEISHU_ALLOW_BOTS=mentions。
接收方 Hermes 建议使用下面的策略:
1 | FEISHU_ALLOW_BOTS=mentions |
如果应用事件使用 sender_id_type=user_id,还应按实际情况设置 FEISHU_BOT_USER_ID。正常情况下,Hermes 会在启动时自动查询自身 bot_open_id;只有网络或飞书 API 不可达、自动探测失败时,才需要手工设置:
1 | FEISHU_BOT_OPEN_ID=ou_xxx |
一套稳定的 A2A 交互流程如下:
- 每个 Hermes bot 启动后都用自身 app credentials 查询
/open-apis/bot/v3/info。 - bot 保存并公布查询得到的 self
bot_open_id,同时标注对应的机器人名称和环境。 - 发送方使用目标 bot 公布的 self
bot_open_id生成<at user_id="...">...</at>标签。 - 接收方订阅
im.message.receive_v1,开通im:message.group_at_msg.include_bot:readonly,并将FEISHU_ALLOW_BOTS设为mentions。 - 接收方通过
_admit()和_mentions_self()确认消息来自其他 bot 且确实 @ 到自己。 - 回复时继续显式 @ 目标 bot,并带上任务 ID、会话 ID 或幂等键,方便双方限制轮次和去重。
群成员列表中的 bot_id、member_id 可以帮助发现候选机器人,但不应当成为 A2A @ 的首选身份来源。不同接口的字段可能处在不同身份作用域,最稳妥的做法仍是让目标 bot 自己调用 /bot/v3/info,再把实时得到的 self open_id 交给发送方。
为什么不建议 FEISHU_ALLOW_BOTS=all
all 会让接收方处理群里所有 bot 消息。只要两个机器人都在自动回复,就可能形成这样的循环:
1 | Bot A 发消息 → Bot B 回复 → Bot A 再回复 → Bot B 再回复 |
多人群里再加入第三个机器人后,循环会更难控制。因此默认使用 mentions,并同时增加以下约束:
- 每条 A2A 消息只 @ 明确的目标 bot,不使用广播触发协作。
- 消息携带任务 ID,并在接收端做幂等去重。
- 为一次协作设置最大往返轮数和超时。
- 回复内容不要无条件复述原始
at标签。 - 对可执行高风险操作的机器人增加群聊范围、命令和权限限制。
mentions 不是完整的防循环方案,但它把触发条件收窄到明确寻址,明显比 all 更适合作为生产环境默认值。
排查清单
A2A @ 没有触发时,可以按这条链路检查:
- 目标 bot 调用
/open-apis/bot/v3/info后是否拿到了非空的bot_open_id。 - 发送方消息中的
<at user_id="...">是否使用了目标 bot 自己公布的 open_id,而不是app_id。 - 接收方应用是否订阅
im.message.receive_v1,并已发布包含最新权限与事件配置的版本。 - 接收方应用是否开通
im:message.group_at_msg.include_bot:readonly,而不是只有普通的群聊 @ 消息权限。 - 接收方是否设置
FEISHU_ALLOW_BOTS=mentions,而不是默认的none。 - Hermes 日志里是否出现
self_ids_unknown、bot_not_mentioned、bots_disabled或group_policy_rejected对应的拒绝原因。 - 群聊规则是否仍要求 mention,以及该群是否被全局或单群策略禁用。
.env中手工填写的FEISHU_BOT_OPEN_ID是否已经过期;应以启动时实时查询结果为准。
排查时不要把 FEISHU_APP_SECRET、tenant access token 或完整私密响应写进日志和文章。验证身份只需要记录机器人名称、脱敏后的 open_id 和接口是否成功。
参考资料
- 飞书官方用户身份介绍(open_id、user_id、union_id): https://open.feishu.cn/document/home/user-identity-introduction/introduction
- 飞书官方获取机器人信息接口(GET /open-apis/bot/v3/info): https://open.feishu.cn/document/client-docs/bot-v3/obtain-bot-info
- 飞书官方接收消息事件(包含其他机器人 @ 当前机器人的说明): https://open.feishu.cn/document/server-docs/im-v1/message/events/receive
- 飞书官方发送消息内容结构(
<at user_id="...">): https://open.feishu.cn/document/server-docs/im-v1/message-content-description/create_json - Hermes 官方 Feishu/Lark 文档(Bot-to-Bot Messaging): https://hermes-agent.nousresearch.com/docs/user-guide/messaging/feishu#bot-to-bot-messaging
- Hermes Feishu adapter 源码: https://github.com/NousResearch/hermes-agent/blob/main/plugins/platforms/feishu/adapter.py

