一条飞书消息如何进入 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 前经过什么

可以把链路简化成这样:

flowchart TD A[飞书消息进入 OpenClaw] --> B[解析 channel、account、sender、chat_type] B --> C[根据 bindings 选择 agentId] C --> D{消息类型} D -->|direct 私聊| E[读取 session.dmScope] E --> F[构造 direct session key] D -->|group 群聊| G[按群聊 chat_id 构造 group session key] F --> H[读取该 session 的历史上下文] G --> H H --> I[当前消息 + 历史上下文发送给 LLM] I --> J[LLM 生成回复] J --> K[通过原飞书会话返回]

这里最容易漏掉的是 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、telegram
  • accountId:同一 channel 下的不同账号或 Bot
  • peerId:私聊对方的用户 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 + 飞书时遇到“消息进入了错误上下文”或“看不到某个会话”,建议按这个顺序查:

  1. 确认消息来自哪个 channel 和 account

先判断是不是同一个飞书应用、同一个 Bot account。很多问题其实是 default / work / staging 混用了。

  1. 查看 bindings

确认这个 account 绑定到了哪个 agentId。agentId 是 session key 的第一层隔离。

  1. 判断 chat 类型

是私聊还是群聊?私聊才看 dmScope;群聊通常按 group chat_id 进入独立 session。

  1. 展开 session key

根据 agentId、channel、accountId、peerId 推导 session key。不要只看 chat_id。

  1. 区分 session 和 run

run 结束不代表 session 消失。session 是上下文容器,run 是一次执行。

  1. 注意 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 没列出来”的问题,都会变成可以按层排查的工程问题,而不是凭感觉猜。

posted @ 2026-05-09 17:53  LexLuc  阅读(27)  评论(0)    收藏  举报