﻿---
title: "OpenClaw 会话管理的得与失"
description: "Gateway 是 OpenClaw 的消息中枢和会话状态管理中心。它的会话管理在小规模场景下优雅得令人舒适，但文件系统架构和平台耦合的领域模型，让规模化成为一道难题。"
pubDate: 2026-02-20
tags: ["开源", "AI", "架构"]
clc: "TP18"
hero: "./hero.svg"
---

Gateway 是 OpenClaw 生态中的常驻网关进程。它以单实例运行，持有所有消息渠道的连接——WhatsApp、Telegram、Slack、Discord、Signal、iMessage——是所有消息进出的唯一入口。用户从任何渠道发来的消息，都先到 Gateway，由它路由到对应的 AI agent，再把响应送回去。控制面客户端（macOS app、CLI、Web UI）通过 WebSocket 连接 Gateway，Gateway 再对接各消息平台和 AI 后端。换句话说，它既是消息的中枢，也是会话状态的管理中心。

这篇文章要聊的就是后者——Gateway 怎么管理会话状态。它最核心的工作不是转发请求，而是把一条条离散的消息串成连贯的对话。OpenClaw 的会话管理设计在小规模场景下优雅得令人舒适，但当用户和任务规模上来之后，它的文件系统架构会成为瓶颈。

---

## 一、会话串联机制

### Session Key：一切的起点

用户从 WhatsApp 发来一条消息，Gateway 要解决的第一个问题是：这条消息属于哪个对话？

OpenClaw 的做法是给每个对话分配一个 **Session Key**——一个结构化的路由标识符。格式长这样：

```
agent:<agentId>:<channel>:dm:<peerId>
agent:<agentId>:<channel>:group:<groupId>
agent:<agentId>:<channel>:channel:<channelId>
```

Channel 是消息来源（WhatsApp、Telegram、Slack），peerId 是发送者的唯一标识，agentId 是处理这条消息的 AI agent。三层组合，精确锁定一个对话上下文。

关键在于，同一个用户从不同渠道发来的消息，可以通过 Identity Links 映射到同一个 canonical identity，进而路由到同一个 Session。也就是说，你在 WhatsApp 上跟 AI 聊了半截，切到 Telegram 继续聊，上下文不丢。

### 两层持久化模型

Session 找到了，接下来要把历史上下文加载出来。OpenClaw 用了两层存储：

**第一层：Session Store（`sessions.json`）**

一个 JSON 文件，存在 `~/.openclaw/agents/<agentId>/sessions/sessions.json`，是所有 session 的索引。每个 entry 记录了 session 的元数据——最近活跃时间、token 用量、关联的 transcript 文件路径。

读取时走内存缓存（TTL 45 秒），避免每次请求都读磁盘。写入时用文件锁 + 临时文件原子重命名，保证并发安全。

**第二层：Transcript 文件（`*.jsonl`）**

每个 session 对应一个 JSONL 文件，存完整的对话历史。格式是追加写入的树结构——每条记录有 `id` 和 `parentId`，支持对话分叉和回溯。

```
~/.openclaw/agents/<agentId>/sessions/
├── sessions.json          # session 索引
├── abc123.jsonl           # session abc123 的完整对话
├── def456.jsonl           # session def456 的完整对话
└── ...
```

### 一次请求的完整链路

把流程拉直了看：

```
1. 消息进来 → 提取 channel + peerId + agentId
2. 构造 Session Key → "agent:bot1:whatsapp:dm:user42"
3. 查 Session Store → 命中缓存或读 sessions.json
4. 加载 Transcript → 读 JSONL，重建对话树
5. 构建上下文 → 从树中提取最近的对话路径
6. 发给模型 → 带着完整上下文调 Claude/GPT
7. 追加响应 → 写入 JSONL，更新 sessions.json
8. 返回给用户
```

步骤 4 和 5 是精髓。对话不是一个扁平的消息列表，而是一棵树。当对话太长触发压缩时，旧的消息会被 AI 总结成摘要，保留在树的根部。新消息挂在摘要节点下面，模型看到的是"摘要 + 最近的消息"——既不丢上下文，又控制了 token 用量。

这就是 Gateway 把前后串起来的方式：**用 Session Key 路由，用 JSONL 存历史，用树结构管理上下文窗口**。

---

## 二、架构优势

### 1. 跨渠道的上下文连续性

用户不需要关心自己在哪个平台。WhatsApp 上问了一半的问题，切到 Slack 上继续追问，AI 记得之前说了什么。

这在实际使用中的体验提升是巨大的。特别是对于需要在手机和电脑之间切换的场景——手机上用 WhatsApp 快速发个指令，回到电脑上在 Slack 里看详细结果，对话不会断。

### 2. 对话树而非对话列表

JSONL 的树结构允许对话分叉。你可以从对话的某个中间节点重新开始，不需要清掉后面的内容。这对调试和实验特别有用——"从第三轮重来，换个方向试试"。

append-only 的写入模式也意味着不会丢数据。即使进程崩溃，最多丢当前这一轮的响应，历史对话完好无损。

### 3. 自动压缩保持上下文窗口可控

当对话超过 token 阈值时，自动触发 compaction：旧消息被总结成摘要，释放 token 空间。默认预留 16,384 token 给新的对话轮次。

这个设计让长对话成为可能。你不需要手动"清理上下文"或者担心"聊太多了模型忘了前面的"——系统自动处理。代价是压缩过程中可能丢失一些细节，但对大部分场景来说是可接受的 trade-off。

### 4. Session Scope 的灵活配置

Gateway 提供了四档 DM 隔离级别：

| 级别 | 说明 | 适用场景 |
|------|------|----------|
| `main` | 所有 DM 共享一个 session | 个人使用，只有自己在聊 |
| `per-peer` | 按发送者隔离 | 多人共用一个 agent |
| `per-channel-peer` | 按渠道+发送者隔离 | 多渠道多人 |
| `per-account-channel-peer` | 按账号+渠道+发送者隔离 | 多账号多渠道多人 |

一个配置项就能调整隔离粒度。个人用选 `main`，团队用选 `per-channel-peer`，SaaS 用选最细粒度。

### 5. 零外部依赖

不需要 Redis，不需要 PostgreSQL，不需要任何外部存储。一个文件系统就够了。部署时不需要操心数据库连接池、Redis sentinel、存储集群——装好 Node.js 就能跑。

对于个人开发者和小团队，这是一个巨大的优点。运维复杂度低，调试方便（直接看 JSON 文件），备份简单（`cp -r` 就行）。

---

## 三、规模化瓶颈

### 1. 文件系统架构的天花板

所有 session 的写操作都通过 `withSessionStoreLock()` 序列化——同一时刻只有一个写入能执行。当并发写入多了，后面的请求就在队列里排队，超时设定是 10 秒。

来算一笔账。假设一个 gateway 实例服务 100 个活跃用户，每人每分钟发 2 条消息。每次写入涉及更新 `sessions.json` + 追加 JSONL 文件。如果单次写入耗时 5ms，每秒 3.3 次写入，看起来还好。

但实际上，文件 I/O 的耗时波动很大。磁盘繁忙时一次写入可能飙到 100ms 甚至更高。再加上 sessions.json 会随着 session 数量增长变大（上限 10MB），读写时间也会增长。当写入队列开始积压，用户感知到的就是"发了消息，AI 很久不回"。

```
正常情况：消息 → 5ms 锁等待 → 处理 → 响应
高负载时：消息 → 800ms 锁等待 → 处理 → 响应（用户已经发了第二条催促消息）
极端情况：消息 → 10s 超时 → 写入失败 → session 状态不一致
```

文件锁的瓶颈还只是纵向的问题。横向上，Gateway 的架构是**单进程、单主机**——一个实例持有所有 messaging surface 的连接（WhatsApp session、Telegram bot token、Slack WebSocket），session 数据存在本地文件系统。这意味着不能启动第二个实例来分担负载（WhatsApp 只允许一个活跃 session），不能把 session 数据迁移到另一台机器（没有分布式存储），也没有内置的集群方案、session 复制或 failover。当你的用户从 50 人涨到 500 人，唯一的出路是升级这台机器的配置。垂直扩展总有尽头。

### 2. 默认配置下的上下文泄露

`dmScope` 的默认值是 `main`——所有 DM 共享同一个 session。这在个人使用时完全没问题，但当多个用户同时跟 agent 对话时，A 能看到 B 的对话上下文。

官方文档明确警告了这个问题：

> *If your agent receives DMs from multiple people, you MUST enable secure DM mode. Without it, all users share the same conversation context, which can leak private information between users.*

但默认值就是不安全的。对于不看文档的开发者（多数人），部署上线后才发现用户之间能看到彼此的对话——这是一个严肃的隐私事故。

### 3. Session 膨胀与可观测性缺失

每个对话一个 JSONL 文件。文件维护策略是：默认保留 500 个 session entry，超过 30 天的自动清理。听起来还行？

但实际场景中，session 的增长速度往往超出预期：

- 一个 Slack workspace 里 50 个人跟 agent 聊天 → 50 个 session
- 每个人同时在 DM 和 3 个 channel 里跟 agent 互动 → 200 个 session
- 再加上定时任务（cron job）触发的 session → 每天多几十个
- 一个月下来轻轻松松破千

session 文件的 JSONL 格式是 append-only，长对话的文件会持续膨胀。一个活跃用户一天几百轮对话，JSONL 文件几 MB。sessions.json 作为索引文件也在膨胀，接近 10MB 时触发 rotation——但 rotation 本身就是一个耗时操作。文件系统层面，一个目录下几千个文件，`ls` 都开始变慢，更别说 Gateway 启动时要扫描这个目录了。

更棘手的是，**这种膨胀几乎是悄无声息的**。当前 Gateway 没有提供 session 级别的指标仪表板（活跃 session 数、平均对话长度、压缩频率），没有 session 生命周期事件的结构化日志，也没有异常 session 的告警机制（如某个 session 的 JSONL 文件异常膨胀）。运维人员想回答"现在有多少活跃对话"这个问题，只能去数文件；想知道"哪些 session 占用了最多 token"，只能自己写脚本解析 JSONL。

在小规模使用时这不是问题——出了事直接看文件。但当 session 数以千计时，缺乏可观测性意味着你只能等问题爆发，而不是提前发现。Session 膨胀本身是可以管理的，前提是你能看见它——而这套系统恰恰让你看不见。

### 4. 压缩是有损的

对话压缩看起来很美好——自动总结旧消息，释放 token 空间。但总结本身就是有损压缩。

被压缩掉的内容包括：
- 具体的数字和参数（"上次你说预算是 50 万" → 压缩后可能变成"之前讨论了预算"）
- 对话中的细微转折（"你先说要 A，后来改主意要 B" → 可能只保留了 B）
- 工具调用的详细参数和返回值
- 用户纠正 AI 错误的过程

在任务型对话中，这些细节可能很关键。用户说"按上次说的那个方案来"，但"上次说的方案"已经被压缩成一句摘要了——AI 只能猜。

更麻烦的是，**压缩触发时机不透明**。用户不知道哪些上下文还在、哪些已经被压缩了。当 AI 突然"忘了"之前讨论过的细节，用户会困惑：是模型的问题还是上下文丢了？

### 5. 多 Agent 协作与长时任务

OpenClaw 支持多个 agent，每个 agent 有独立的 session 空间。但 agent 之间没有共享上下文的机制。一个典型场景：Agent A 负责客服，Agent B 负责技术支持，用户先跟 A 聊了问题背景，A 判断需要转给 B——转过去之后，B 对之前的对话一无所知，用户要重新描述一遍。没有 session 共享、没有 session 转移、没有跨 agent 的上下文传递，每个 agent 是一个孤岛。

跨 agent 协作的缺失只是表象，更深层的问题是系统缺乏高于 session 的任务抽象。OpenClaw 的典型任务——代码生成、文档分析、多轮工具调用——常常运行数十分钟，但系统没有一等公民的 Task 概念。当前的做法是用 sub-agent session 隐式承担 task 角色：`sessions_spawn()` 创建一个独立的 session，完成后通过 announce queue 通知主 session。这在简单场景下够用，但 sub-agent session 与真正的 Task 之间有明显的 gap：

- **无显式状态机**——session 没有 `pending → running → succeeded/failed` 的生命周期，只有"存在"和"不存在"
- **无进度追踪**——主 session 不知道 sub-agent 执行到哪一步了，只能等最终结果
- **无重试机制**——sub-agent 失败了就失败了，没有自动重试或断点续跑
- **无持久化队列**——如果 Gateway 进程崩溃，正在运行的 sub-agent 任务全部丢失

有趣的是，CronJob 反而有最完整的任务生命周期管理——`runningAtMs` 追踪运行状态、`lastRunStatus` 记录成功失败、还有卡住检测机制。但这套逻辑仅限定时触发场景，没有泛化成通用的 Task 原语。

GitHub issue 也反映了这些痛点：
- [#6042](https://github.com/nicepkg/openclaw/issues/6042)：sub-agent 达到 context 上限后直接终止，没有优雅降级或任务移交
- [#10334](https://github.com/nicepkg/openclaw/issues/10334)：announce 机制阻塞主 session，长时任务拖慢整个对话
- [#10467](https://github.com/nicepkg/openclaw/issues/10467)：缺乏 lane 隔离，多个长时任务互相干扰

本质上，**OpenClaw 选择了"渐进增强 session"而非引入独立的 Task 原语**。这与其"两个抽象解释一切"的设计哲学一致——Session 和 Agent 已经足够解释系统中的大部分行为，加入 Task 会打破这种简洁性。但无论是跨 agent 协作还是长时任务管理，用 session 硬扛更高层抽象的职责，付出的都是可靠性的代价。

---

## 四、根源与出路

上面列的这些缺点不是因为代码写得烂。恰恰相反，OpenClaw 的 session 管理代码质量很高——原子写入、文件锁、缓存失效、优雅降级，每一个细节都处理得很用心。

问题出在**架构假设**上——不止一个，而是两个层面。

**基础设施层的假设**

这套系统是为"一个人或一个小团队在一台机器上运行一个 AI 助手"设计的。在这个假设下，文件系统是最好的选择——简单、可靠、零依赖。

但当使用场景从"Peter 的个人 WhatsApp 助手"变成"100 人团队的 AI 基础设施"时，这个假设就不再成立了。文件锁扛不住高并发，单机扛不住大流量，本地存储扛不住多实例，默认配置扛不住多用户。

**领域模型层的假设**

还有一个更隐蔽的问题：Session 的抽象直接耦合了外部平台的概念，而不是建立在平台无关的内部模型上。

回头看 Session Key 的格式：`agent:<agentId>:<channel>:dm:<peerId>`。这里的 `channel`——WhatsApp、Telegram、Slack——是一个外部平台标识，被硬编码进了路由层的最核心位置。系统的会话路由、隔离粒度、甚至 agent 协作，都建立在"消息来自哪个平台"这个外部概念上。

这带来了几个连锁效应：

- **Identity Links 是补丁，不是地基。** 跨平台的身份关联通过 Identity Links 实现——把 WhatsApp 的 user42 和 Telegram 的 user42 映射到同一个 canonical identity。但这是一层事后补上的映射关系，不是系统从一开始就内建的统一身份。如果系统有一个平台无关的 Person 概念，跨平台身份就不是"需要额外配置的特性"，而是"默认行为"。

- **dmScope 的隔离粒度用平台维度定义。** `per-channel-peer`、`per-account-channel-peer`——这些隔离级别的命名方式本身就说明问题：隔离粒度是按"渠道"和"账号"切分的，而不是按"这是谁"和"这是哪个群"切分的。如果系统内建 Person 和 Group 概念，隔离粒度是天然内建的——一个 Person 就是一个对话上下文，不需要额外配置 scope。

- **跨 agent 协作困难，部分原因在这里。** 第三章提到 Agent A 转给 Agent B 时上下文全丢。Session 绑定在 `agent + channel + peerId` 上，换一个 agent 就是换一个 session。但如果 session 绑定在 Person 上，agent 只是服务这个 Person 的执行者——换 agent 不需要换对话上下文，因为上下文属于 Person，不属于 agent。

本质上，OpenClaw 把"消息来自 WhatsApp 还是 Telegram"当成了一等公民，而把"消息来自张三还是李四"当成了需要额外配置才能识别的东西。这个优先级是反的。在一个 IM 网关系统里，Person 和 Group 才应该是一等公民，外部平台应该只是适配器。

**两层假设叠加，形成了经典困境**

基础设施层假设文件系统够用，领域模型层假设外部平台概念可以直接作为内部抽象。两个假设在小规模场景下都合理——一个人在一台机器上通过一两个平台聊天，文件系统够用，平台概念和内部模型也确实没什么区别。但当规模增长，这两个假设同时失效：文件系统扛不住并发，平台耦合扛不住多样性。

这是一个经典的架构演进困境：**让你快速起步的设计，恰恰是阻碍你规模化的那个东西。**

**出路**

如果 OpenClaw 要认真地解决规模化问题，需要在以下方向上做出改变：

1. **引入外部 session 存储**——Redis 或 PostgreSQL，支持多实例共享 session 状态
2. **Gateway 集群化**——支持多实例部署，session 数据通过外部存储共享
3. **默认安全的隔离策略**——`dmScope` 的默认值应该是 `per-channel-peer` 而不是 `main`
4. **session 的可观测性**——内置 metrics endpoint，暴露 session 级别的指标
5. **跨 agent session 传递**——允许 agent 之间共享或转移对话上下文
6. **引入 Task 抽象**——为长时运行的操作提供显式的生命周期管理（状态机、进度追踪、重试、持久化队列），而不是继续用 session 隐式承载
7. **引入平台无关的内部会话模型**——以 Person 和 Group 为一等公民构建 session 路由，外部平台（WhatsApp、Telegram、Slack）降级为适配器层；Session Key 不再编码 channel，Identity Links 从可选配置变成默认行为，隔离粒度自然内建于 Person/Group 的边界中

但这些改变中的每一项都会增加复杂度，牺牲当前架构"零依赖、开箱即用"的核心优势。这又回到了那个永恒的 trade-off：简单和可扩展，你只能选一个。

OpenClaw 选了简单。对它的目标用户来说，这可能是对的。
