一条飞书消息如何进入 OpenClaw 的 LLM 上下文:从 chat_id 到 session key
说明:本文由 OpenClaw 助手根据一次真实配置与排障过程辅助整理,内容经过人工确认。
文中涉及的用户 ID、群 ID、应用 ID、内部路径和具体租户信息均已脱敏或泛化。重点不是某个环境的私有配置,而是 OpenClaw 中“外部 IM 会话如何映射到内部 session 上下文”的机制。
接入飞书机器人以后,一个很容易混淆的问题是:
“飞书里的一个会话,是否就等于 OpenClaw 里的一个 session?”
答案是:不等于。
飞书的会话是消息入口和出口,OpenClaw 的 session 是 LLM 执行时使用的上下文容器。两者之间有映射关系,但这个映射不是简单的一对一,而是由 channel、account、agent binding、chat 类型和 dmScope 共同决定。
这篇笔记记录一次真实排障中我们如何把这条链路拆清楚。
1. 两种“会话”不是一回事
先定义两个概念。
飞书会话,是飞书平台上的聊天容器,用 chat_id 标识。它可能是:
- 用户和 Bot 的私聊
- 群聊
- 话题或 threaded reply 所在的消息上下文
OpenClaw session,是 OpenClaw 内部的对话上下文容器,用 session key 标识。它保存的是:
- 历史 user / assistant messages
- 工具调用结果
- 当前 agent 的运行上下文
- 后续发给 LLM 的上下文材料
所以,chat_id 决定消息从哪里来、发回哪里去;session key 决定这条消息进入哪段 LLM 历史上下文。
一句话:飞书是消息管道,OpenClaw session 是上下文容器。
2. 一条飞书消息进入 LLM 前经过什么
可以把链路简化成这样:
这里最容易漏掉的是 bindings。
很多人会直接盯着 dmScope 看,以为它决定了所有隔离关系。但实际上,在 dmScope 生效之前,OpenClaw 已经先通过 bindings 决定了这条消息属于哪个 agent。
如果两个飞书 Bot 分别绑定到不同 agent,那么它们的 session key 从第一段 agentId 就已经不同了;即使 dmScope 一样,也不会共享上下文。
3. 源码视角:先取 dmScope,默认是 main
OpenClaw 路由时会读取配置中的 session.dmScope:
const dmScope = input.cfg.session?.dmScope ?? "main";
这意味着如果没有显式配置,默认行为是 main。
main 的含义比较强:所有 direct message 会落到同一个 agent 主 session。它对单用户本地助手很方便,但如果一个 Bot 同时服务多个用户,就有上下文串线风险。
所以在多人或多渠道场景里,通常需要显式设置更细的 dmScope。
4. session key 是怎么生成的
对 direct message,OpenClaw 会调用类似 buildAgentPeerSessionKey 的逻辑,根据 dmScope 生成不同形态的 session key。
核心规则可以概括为:
main
→ agent:<agentId>:***
per-peer
→ agent:<agentId>:direct:<peerId>
per-channel-peer
→ agent:<agentId>:<channel>:direct:<peerId>
per-account-channel-peer
→ agent:<agentId>:<channel>:<accountId>:direct:<peerId>
其中:
agentId:由 bindings 决定channel:比如 feishu、slack、telegramaccountId:同一 channel 下的不同账号或 BotpeerId:私聊对方的用户 ID
这就解释了为什么同一个飞书用户,在不同配置下可能进入完全不同的 OpenClaw session。
5. dmScope 四种模式的区别
main
所有私聊共享一个主 session:
agent:main:***
优点是连续性强,适合只有一个使用者的本地助手。
缺点是多用户时很危险:A 用户说过的话可能进入 B 用户的上下文。
per-peer
按用户隔离,但不区分渠道:
agent:main:direct:<user_id>
如果不同 IM 平台上的 peerId 可能相同,就有跨渠道碰撞的可能。实际多渠道环境里一般不如 per-channel-peer 稳妥。
per-channel-peer
按渠道 + 用户隔离:
agent:main:feishu:direct:<user_id>
这是多数单账号、多渠道场景下比较合理的默认值。它能保证飞书用户、Slack 用户、Telegram 用户不会因为 peerId 相同而混到一起。
per-account-channel-peer
按账号 + 渠道 + 用户隔离:
agent:main:feishu:default:direct:<user_id>
agent:main:feishu:work:direct:<user_id>
它真正有价值的场景是:同一个 OpenClaw agent 同时接入多个同渠道账号或 Bot,并且希望这些 Bot 的上下文互相隔离。
6. 多 Bot 场景里,accountId 什么时候有意义
这次排障里最关键的一个误区是:
“既然有多个 Feishu Bot,是不是一定要用 per-account-channel-peer?”
不一定。
要看 bindings。
假设两个 Bot 分别绑定到不同 agent:
feishu account default → agent main
feishu account work → agent work
那么即使用 per-channel-peer,session key 也已经不同:
agent:main:feishu:direct:<user_id>
agent:work:feishu:direct:<user_id>
这时 accountId 即使加进去,也只是多一层标识,对隔离效果没有本质增强,因为 agentId 已经隔离了。
但如果两个 Bot 都绑定到同一个 agent:
feishu account default → agent main
feishu account work → agent main
这时区别就非常明显。
per-channel-peer 会让两个 Bot 的同一用户共享上下文:
agent:main:feishu:direct:<user_id>
per-account-channel-peer 则会让两个 Bot 分开:
agent:main:feishu:default:direct:<user_id>
agent:main:feishu:work:direct:<user_id>
所以结论是:per-account-channel-peer 的价值,不取决于“有没有多个 Bot”,而取决于“多个 Bot 是否绑定到同一个 agent,并且是否需要隔离上下文”。
7. 群聊和私聊的映射逻辑不同
上面讨论的 dmScope 只影响 direct message。
群聊通常不会走 dmScope 的私聊隔离逻辑,而是按群的 chat_id 形成独立 session,例如:
agent:<agentId>:feishu:group:<chat_id>
如果启用了 topic/thread 级别的配置,还可能进一步把同一个群里的不同话题拆成不同上下文。
所以排查时要先分清楚:当前消息是 direct 还是 group。不要拿私聊的 dmScope 解释群聊行为。
8. session 没有“完成态”,run 才有
另一个容易误解的点是 sessions_list。
一开始我们以为它只列“活跃 session”,没有列“已完成 session”。后来查源码后发现这个说法不准确。
OpenClaw 里 session 本身是持久化上下文容器,不是一次性任务,所以它没有“完成态”。有状态的是单次 run,常见状态包括:
running
done
failed
killed
timeout
done / failed / killed / timeout 是 run 的终止态,不是 session 的终止态。
换句话说,一个 session 可以长期存在,今天有一次 run,明天又有一次 run。run 会结束,但 session 不会因为一次 run 结束就“完成”。
9. sessions_list 看不到,不一定是 session 不存在
这次排障还暴露了 sessions_list 的另一个误区。
当 sessions_list 没列出某个用户的会话时,不能立刻判断“这个 session 不存在”。还要看 session visibility。
OpenClaw 的 session 工具会做权限可见性过滤。默认 tree 模式下,一个 session 通常只能看到:
- 当前 session 自己
- 当前 session spawn 出来的子 session
这是一种隔离设计,避免任意会话随便枚举其他用户的上下文。
如果要做系统级排障,可以查看持久化的 sessions 索引文件或日志。但这属于本机运维视角,不等同于普通工具视角。
因此:
sessions_list 没看到
≠ session 不存在
可能只是当前 session 没有 visibility
10. 排障顺序建议
如果你在接入 OpenClaw + 飞书时遇到“消息进入了错误上下文”或“看不到某个会话”,建议按这个顺序查:
- 确认消息来自哪个 channel 和 account
先判断是不是同一个飞书应用、同一个 Bot account。很多问题其实是 default / work / staging 混用了。
- 查看 bindings
确认这个 account 绑定到了哪个 agentId。agentId 是 session key 的第一层隔离。
- 判断 chat 类型
是私聊还是群聊?私聊才看 dmScope;群聊通常按 group chat_id 进入独立 session。
- 展开 session key
根据 agentId、channel、accountId、peerId 推导 session key。不要只看 chat_id。
- 区分 session 和 run
run 结束不代表 session 消失。session 是上下文容器,run 是一次执行。
- 注意 visibility
工具看不到某个 session,不一定代表它不存在。可能只是当前会话没有权限看到。
11. 实战配置建议
如果是单用户、本地使用,可以保留 main,它最省心。
如果是多人使用的 Bot,建议至少使用:
{
"session": {
"dmScope": "per-channel-peer"
}
}
如果是多个同渠道 Bot 绑定到同一个 agent,并且希望它们互不共享上下文,则使用:
{
"session": {
"dmScope": "per-account-channel-peer"
}
}
如果多个 Bot 已经通过 bindings 分别绑定到了不同 agent,那么 agentId 本身已经提供了隔离,此时 per-account-channel-peer 不一定带来额外收益。
12. 最后的模型
可以用一句话记住这条链路:
一条飞书消息最终进入哪个 LLM 上下文,不是由飞书 chat_id 单独决定的,而是由 bindings 选出的 agentId、消息的 chat 类型,以及 dmScope 共同生成的 OpenClaw session key 决定的。
或者更短一点:
飞书 chat_id 决定消息通道;OpenClaw session key 决定 LLM 记忆。
理解这点以后,很多“为什么它没看到那段对话”“为什么两个 Bot 上下文不共享”“为什么 sessions_list 没列出来”的问题,都会变成可以按层排查的工程问题,而不是凭感觉猜。

浙公网安备 33010602011771号