Day 3 |OpenClaw Agent 运行时:会话管理与上下文持久化
如果说 Gateway 是 AI Agent 框架的"神经网络",负责消息的传递与路由,那么 Agent 运行时(Runtime)就是这个框架的"大脑"——它决定了 AI 如何记忆过去、理解现在、响应未来。会话(Session)是什么?如何定义一次对话的边界?上下文如何持久化?进程重启后,对话历史怎么恢复?如何在有限的上下文窗口内,塞进最有价值的信息?本文将深入 OpenClaw 的 Agent
Day 3 | OpenClaw Agent 运行时:会话管理与上下文持久化
系列:《从 0 到 1 拆解 AI Agent 框架:OpenClaw 技术深度解析》
前言
如果说 Gateway 是 AI Agent 框架的"神经网络",负责消息的传递与路由,那么 Agent 运行时(Runtime)就是这个框架的"大脑"——它决定了 AI 如何记忆过去、理解现在、响应未来。
一个生产级 AI Agent 必须解决三个核心问题:
- 会话(Session)是什么? 如何定义一次对话的边界?
- 上下文如何持久化? 进程重启后,对话历史怎么恢复?
- 如何在有限的上下文窗口内,塞进最有价值的信息?
本文将深入 OpenClaw 的 Agent 运行时,逐一拆解这三个问题的工程实现。
一、Session:对话的"容器"
1.1 什么是 Session?
在 OpenClaw 中,Session 是一次完整对话的抽象单元。它不仅仅是消息列表,还包含:
- 身份信息:这个 Session 属于哪个 Agent?从哪个渠道发起?
- 上下文状态:当前对话历史、工具调用记录、系统提示词
- 元数据:创建时间、最后活跃时间、消息数量
- 配置:使用哪个模型?思维链是否开启?
可以把 Session 理解为一个"对话房间":用户和 AI 在这个房间里交流,房间有自己的记忆和规则,不同房间互不干扰。
1.2 Session Key:唯一身份标识
每个 Session 都有一个全局唯一的 sessionKey,格式如下:
{agentId}:{channelId}:{userId}
例如:
main-agent:telegram:user_12345678
main-agent:discord:guild_abc:channel_xyz
main-agent:webchat:session_abcdef
这个设计的精妙之处在于:Key 本身编码了路由信息。从 Key 就能知道这条消息从哪个平台来、属于哪个 Agent,路由层无需额外查表。
1.3 Session 的生命周期
创建 → 激活 → 持续对话 → 超时休眠 → 按需唤醒 → 归档
OpenClaw 的 Session 是懒加载的:首次收到消息时创建,长时间不活跃后从内存中卸载(但历史记录保留在磁盘),下次消息到来时再从磁盘恢复。
这个设计对资源效率至关重要——一个多渠道 Agent 可能同时维护数百个 Session,不可能全部常驻内存。
二、上下文持久化:JSONL 文件存储
2.1 为什么选择 JSONL?
上下文持久化是 AI Agent 最容易"踩坑"的地方。常见方案对比:
| 方案 | 优点 | 缺点 |
|---|---|---|
| SQLite | 查询灵活,支持索引 | Schema 升级麻烦,消息格式嵌套复杂 |
| Redis | 读写极快 | 重启丢失,需额外持久化配置 |
| PostgreSQL | 强一致性,功能完备 | 部署复杂,运维成本高 |
| JSONL 文件 | 简单、可读、无依赖、原生支持追加写 | 查询慢,不适合大规模检索 |
OpenClaw 选择 JSONL(JSON Lines) 格式:每行一条消息,文件路径即 Session ID。
~/.openclaw/sessions/
main-agent:telegram:user_12345678.jsonl
main-agent:webchat:session_abcdef.jsonl
每行格式示例:
{"role":"user","content":"帮我分析一下这段代码","timestamp":1709123456789}
{"role":"assistant","content":"好的,这段代码实现了...","timestamp":1709123460123,"model":"claude-sonnet-4"}
{"role":"tool","name":"read","input":{"path":"main.py"},"output":"def main():...","timestamp":1709123461000}
2.2 追加写的原子性保证
JSONL 最大的优势是天然支持追加写:
// 每次新消息,只需在文件末尾追加一行
await fs.appendFile(sessionPath, JSON.stringify(message) + '\n');
追加写(append)是操作系统级别的原子操作,不会出现写一半导致文件损坏的情况。相比覆盖写整个文件,性能和可靠性都更好。
2.3 会话恢复:从磁盘重建内存状态
当 Session 从磁盘"唤醒"时,需要重建完整的对话上下文:
async function loadSession(sessionKey: string): Promise<Session> {
const filePath = getSessionPath(sessionKey);
if (!existsSync(filePath)) {
return createNewSession(sessionKey);
}
// 逐行读取,重建消息列表
const lines = readFileSync(filePath, 'utf-8').split('\n').filter(Boolean);
const messages = lines.map(line => JSON.parse(line));
return {
key: sessionKey,
messages,
createdAt: messages[0]?.timestamp,
lastActiveAt: messages[messages.length - 1]?.timestamp,
};
}
这个过程几乎是零成本的——读文件、解析 JSON、构建内存对象,不需要任何网络请求或外部依赖。
三、上下文窗口管理:如何在有限空间里塞进最有价值的信息
这是 Agent 运行时最核心也最复杂的问题。
3.1 上下文窗口的本质限制
大语言模型的上下文窗口是有限的(通常 128K~200K tokens),而一次长期对话很容易超出这个限制。更关键的是:token 越多,推理越慢,成本越高。
朴素做法是"只保留最近 N 条消息",但这会丢失重要的早期上下文(比如用户在第1条消息里说的需求)。
3.2 OpenClaw 的分层上下文策略
OpenClaw 采用分层上下文设计,按优先级组装发送给模型的消息列表:
[系统提示词] ← 最高优先级,永远存在
[记忆文件内容] ← 用户的长期记忆(MEMORY.md 等)
[工作区文件] ← 项目上下文(按需注入)
[压缩摘要] ← 历史对话的摘要(可选)
[最近 N 条消息] ← 最新的对话历史
这个设计的核心思想是:不同信息有不同的"半衰期"。系统提示词永远有效,最近的消息最相关,中间的历史可以用摘要替代。
3.3 上下文压缩(Compaction)
当对话历史超过一定阈值时,OpenClaw 会触发上下文压缩:
- 取出最近 K 条消息之前的所有历史
- 调用模型生成一段摘要:“以上对话的关键信息是……”
- 将摘要替换原始历史消息,存入 Session
- 后续对话携带摘要 + 最新消息,而不是完整历史
原始:[msg1, msg2, msg3, ..., msg100, msg101, msg102]
压缩后:[摘要(msg1-msg90), msg91, msg92, ..., msg102]
压缩的触发条件通常是:
- 消息数量超过阈值(如 80 条)
- 估算 token 数超过窗口限制的 70%
3.4 Token 估算:不依赖 Tokenizer
精确计算 token 数需要调用模型的 tokenizer,开销不小。OpenClaw 使用启发式估算:
function estimateTokens(text: string): number {
// 英文:约 4 字符/token
// 中文:约 1.5 字符/token(每个汉字约 1-2 token)
const chineseChars = (text.match(/[\u4e00-\u9fff]/g) || []).length;
const otherChars = text.length - chineseChars;
return Math.ceil(chineseChars / 1.5 + otherChars / 4);
}
估算值偏保守(略高于实际),宁可早压缩也不超窗口——这是一个合理的工程取舍。
四、Sub-Agent:Session 的隔离沙箱
OpenClaw 支持 Sub-Agent——在主 Session 内启动一个隔离的子会话来完成特定任务。
4.1 为什么需要 Sub-Agent?
想象一个场景:用户让 Agent “帮我重构这个项目的代码”。这个任务可能需要:
- 读取几十个文件
- 多轮迭代修改
- 运行测试验证
如果在主 Session 里做,会迅速消耗上下文窗口,还会让对话历史变得杂乱。
Sub-Agent 的解法是:开一个新的 Session 专门干这件事,主 Session 只关注任务结果。
4.2 Sub-Agent 的生命周期
主 Session 调用 sessions_spawn()
→ 创建子 Session(独立 sessionKey)
→ 子 Agent 执行任务(有自己的上下文、工具调用)
→ 任务完成,推送结果给主 Session
→ 子 Session 按需保留或删除
子 Session 的 JSONL 文件独立存储,不污染主 Session 的历史。这是"关注点分离"原则在 AI Agent 架构中的直接体现。
五、工程细节:我踩过的坑
5.1 并发写入的竞态问题
多个请求同时向同一个 Session 追加消息时,可能出现:
进程A: 读取文件末尾位置 → 写入消息A
进程B: 读取文件末尾位置 → 写入消息B(覆盖了消息A!)
解决方案:Session 级别的写入锁。每个 Session 维护一个 Promise 队列,写操作串行化执行。
class SessionWriter {
private writeQueue: Promise<void> = Promise.resolve();
async append(message: Message): Promise<void> {
this.writeQueue = this.writeQueue.then(() =>
fs.appendFile(this.path, JSON.stringify(message) + '\n')
);
return this.writeQueue;
}
}
5.2 JSONL 文件损坏恢复
进程崩溃可能导致最后一行 JSON 不完整(写到一半):
{"role":"user","content":"你好"}
{"role":"assistant","content":"你好!有什么 ← 截断了
恢复策略:加载时跳过解析失败的行,并在日志中记录警告。宁可丢失最后一条消息,也不让整个 Session 崩溃。
5.3 Session 缓存的内存管理
内存中维护 Session 缓存需要设置上限,避免 OOM。OpenClaw 使用 LRU(最近最少使用) 缓存:
- 缓存上限:N 个 Session(可配置)
- 当缓存满时,淘汰最久未访问的 Session
- 被淘汰的 Session 数据仍在磁盘,下次访问时重新加载
六、设计哲学:简单是最大的优雅
回顾 OpenClaw 的 Session 设计,有几个一以贯之的原则:
零依赖原则:JSONL 文件不需要数据库、不需要 Redis,任何环境都能跑。对于一个面向个人开发者的工具,降低部署门槛比极致性能更重要。
可观测原则:Session 文件是纯文本,随时可以打开看内容。调试时直接 cat session.jsonl 就能看到完整对话历史,比查数据库方便太多。
渐进式复杂度:小对话用简单的追加写,大对话用压缩,极大对话用 Sub-Agent 分流。复杂度随需求增长,不强迫用户从一开始就承受高复杂度。
小结
本文拆解了 OpenClaw Agent 运行时的三个核心机制:
| 机制 | 实现方案 | 核心思想 |
|---|---|---|
| Session 管理 | sessionKey 编码路由信息,懒加载+LRU缓存 | 身份即路由 |
| 上下文持久化 | JSONL 追加写,逐行解析恢复 | 简单可靠 |
| 上下文窗口管理 | 分层优先级 + 压缩摘要 | 有限空间,最大价值 |
| Sub-Agent | 独立 Session 隔离 | 关注点分离 |
下一篇,我们将聚焦流式输出——从模型吐出第一个 token,到用户在手机上看到文字,中间发生了什么?
作者:一个在折腾 AI Agent 框架的工程师
系列索引:[Day 1 架构概览] | [Day 2 Gateway] | Day 3 Agent 运行时 | Day 4 流式输出 → 敬请期待
如果这篇文章对你有帮助,欢迎点赞收藏 🎯
更多推荐



所有评论(0)