文档版本:v2.3 | 更新日期:2026-03-12 | 分析对象:openclaw/openclaw
前置阅读:02-agent-runtime.md(Agent Loop、Context 组装)。本文聚焦 Session 生命周期与 Memory 持久化,Context 组装的 Token 预算与修剪策略详见 02 §6。
功能实现状态
1. 定位
Session 管理"短期记忆"(单次对话上下文),Memory 管理"长期记忆"(跨会话持久知识)。两者配合让 Agent 既能连贯对话,又能"记住"用户的偏好和历史。
2. Session 管理
2.1 Session 数据结构
~/.openclaw/agents/<agentId>/sessions/<sessionId>/ ├── meta.json Session 元数据 ├── messages.jsonl 消息历史(追加写入, JSONL 格式) └── context.json 上下文快照(修剪后的缓存) meta.json: { "sessionId": "s-abc123", "agentId": "main", "channel": "telegram", "chatId": "123456789", "userId": "123456789", "createdAt": "2026-02-04T10:00:00Z", "lastActiveAt": "2026-02-26T14:30:00Z", "messageCount": 47, "totalInputTokens": 65200, "totalOutputTokens": 20000, "totalCost": 0.085, "status": "active", "model": "moonshot/kimi-k2.5", "summaryCheckpoint": 20 // Summary Compression 的最后压缩位置 }2.2 消息存储格式
messages.jsonl (每行一条消息, 追加写入): {"idx":0,"role":"user","content":"你好","ts":"2026-02-04T10:00:00Z","tokens":5} {"idx":1,"role":"assistant","content":"你好!有什么可以帮你的?","ts":"...","tokens":15} {"idx":2,"role":"user","content":"帮我清一下邮件","ts":"...","tokens":8} {"idx":3,"role":"assistant","content":"","toolCalls":[{"id":"tc-1","name":"gmail.list","args":{}}],"ts":"...","tokens":20} {"idx":4,"role":"tool","toolCallId":"tc-1","content":"{...邮件列表...}","ts":"...","tokens":350} {"idx":5,"role":"assistant","content":"你有5封新邮件...","ts":"...","tokens":80} 为什么用 JSONL 而不是 JSON 数组? • 追加写入: 不需要读取全部 → 修改 → 重写 • 崩溃安全: 写到一半最多丢最后一条 • 流式友好: 可以按行读取,不需要全部加载 并发安全: • 单 Agent 串行处理消息 (Agent Loop 内无并发写入同一 Session) • 跨 Agent 写不同 Session → 无冲突 • 极端场景: 同一 chatId 在 idle_timeout 前后快速连发 → SessionManager 内部用 per-key mutex 保护 getOrCreate → 保证同一时刻只有一个 Agent Loop 持有该 Session2.3 Session 生命周期
各状态说明:
• 创建:新用户首次消息、显式 session.create、或/session new命令• active:持续有消息交互,每条消息更新 lastActiveAt,追加到messages.jsonl• dormant:序列化到磁盘,释放内存(5MB → 1KB),保留 meta.json• archived:消息历史压缩归档, meta.json标记status: archived,关键事实已持久化到 Memory
2.4 Session 查找策略
查找逻辑:
• key 构建: channel:chatId,例telegram:123456789• 磁盘查找:扫描 sessions/*/meta.json,匹配channel + chatId + status ≠ archived• 新建: sessionId = s- + uuidv7(),创建目录 + 写入meta.json
优化:启动时加载所有
meta.json到内存 Map,不加载 messages(按需加载)。Group Chat:群聊场景下
chatId为群组 ID,同一群内所有用户共享一个 Session。消息中的sender.userId区分发言人,记忆提取时按 userId 分别写入各自的 User Memory。详见 02-agent-runtime.md §5 中的 DM/Group 策略差异。
2.5 会话历史修剪与 Summary Compression
meta.json 中的 summaryCheckpoint 字段标记 Summary Compression 的最后压缩位置。修剪策略由 Agent 配置 context.pruneStrategy 决定,详细算法见 02-agent-runtime.md §6.3,此处仅说明 Session 侧的存储行为:
┌────────────────────────────────────────────────────────────┐ │ Sliding Window (默认, 已实现): │ │ • Session 不做任何预处理 │ │ • Context 组装时从 messages.jsonl 尾部按 Token 预算截取 │ │ • messages.jsonl 保留全量(归档前不删除) │ │ │ │ Summary Compression (规划中): │ │ • 消息数 > threshold (默认 30) 时触发 │ │ • 对索引 0 到 summaryCheckpoint 的消息生成摘要 │ │ • 摘要写入 context.json,更新 summaryCheckpoint │ │ • Context 组装: [摘要] + [checkpoint 之后的原始消息] │ │ • 原始 messages.jsonl 不删除(审计可追溯) │ │ │ │ Importance Scoring (规划中): │ │ • 每条消息附带重要性评分 → 优先保留高分消息 │ │ • 适合长周期任务对话 │ └────────────────────────────────────────────────────────────┘3. Memory 系统(长期记忆)
3.1 记忆 Schema
OpenClaw 的记忆分为 User Memory 和 Agent Memory:
合并策略总结:
关键规则:档案/偏好 → 总是合并(新覆盖旧);事件/案例 → 永远不合并(每条独立)。搞反了 = 灾难(偏好丢失 or 事件重复)。
3.2 三级内容模型 (L0/L1/L2)
各层职责:
• L0:每次检索都加载到 Context。例: 用户偏好 TypeScript,不喜欢 Java• L1:模型需要更多细节时展开。例: 用户多次表达对 TS 的偏好• L2:包含原始对话片段和时间戳,仅 memory.get(id)时按需加载
为什么分层? 10,000 条记忆 × L2 全文 = 几十万 Token 检索成本。分层后:索引只用 L0 → 命中后按需展开 → 节省 90%+。Context 中通常注入 Top-K 条 L0 摘要(~100 tokens × 10 = 1K),特别相关时自动展开为 L1。
4. 混合检索引擎
支持 BM25 + 向量 + Reranking 三路混合搜索(架构自 v2026.2.2 引入)。
存储后端演进:v2026.2.2 初版使用自建 JSON 倒排索引 + hnswlib-node HNSW 索引(零外部依赖);v2026.2.22 起引入 sqlite-vec 作为可选后端(
package.json已列为依赖),可统一管理向量和元数据。两种后端通过memory.backend配置切换,默认仍为文件索引方案。
4.1 检索流程
各步骤说明:
• [1] 提取关键词、生成 Embedding、语言检测(如 zh-CN) • [2a] BM25:倒排索引 + jieba-wasm 分词,返回 Top-50 • [2b] Vector:cosine 相似度 + HNSW 索引,返回 Top-50 • [3] RRF 公式: score = 0.3/(k + rank_bm25) + 0.7/(k + rank_vec),k=60• [4] Cross-Encoder 对 RRF Top-K 候选重排序,最终排名以 Reranker 分数为准 • [5] final_score = fusion_score × retrievability• [6] 返回 Top-K 记忆条目,含 L0 摘要 + 元数据
4.2 Embedding 模型选型
默认策略:
• 有 OPENAI_API_KEY→ text-embedding-3-small(最佳质量)• 无 API Key → bge-small-en-v1.5(本地推理, 零成本) • 多语言需求 → bge-m3(中英日韩全覆盖)
{ "memory": { "embedding": { "provider": "openai", "model": "text-embedding-3-small", "localModel": "bge-small-en-v1.5", "dimensions": 1536, "batchSize": 100 } }}API 限流与降级:
批量写入记忆时可能一次生成数十条 Embedding,需要处理 API 限流:
batchSize | |
localModel(本地 ONNX),本次 Session 内不再尝试 API | |
embeddingPending: true,下次网络恢复时由 openclaw memory index --reembed 统一用 API 模型重新生成 |
4.3 索引实现
~/.openclaw/agents/<agentId>/memory/index/ ├── bm25.idx BM25 倒排索引 (JSON) ├── vector.idx HNSW 向量索引 (二进制) ├── metadata.json 记忆元数据 (id, category, fsrs, ts) └── config.json 索引配置 (维度, 参数) BM25 倒排索引结构: { "vocab": { "部署": 0, "方案": 1, "typescript": 2, ... }, "df": { "0": 15, "1": 8, "2": 42, ... }, "postings": { "0": [{"id":"mem-001","tf":2}, {"id":"mem-015","tf":1}], "1": [{"id":"mem-001","tf":1}, {"id":"mem-023","tf":3}] }, "totalDocs": 2847, "avgDocLength": 45 } HNSW 向量索引: • 基于 hnswlib-node (C++ 绑定) • 参数: M=16, efConstruction=200, ef=100 • 支持增量插入 (无需重建全量索引) • 内存占用: ~6KB/向量 (1536 维 × float32) 384 维 (bge-small) → ~1.5KB/向量 • 2847 条记忆 (1536 维) → ~18MB 索引 文件索引方案 vs sqlite-vec 后端: ┌──────────────────┬────────────────────┬────────────────────┐ │ │ 文件索引 (默认) │ sqlite-vec │ ├──────────────────┼────────────────────┼────────────────────┤ │ 依赖 │ 零 (纯 JS/WASM) │ sqlite-vec native │ │ BM25 调优 │ 完全可控 │ FTS5 内置 │ │ 可观测性 │ cat 直接查看 │ 需 SQL 查询 │ │ 原子性 │ WAL 自实现 │ SQLite 事务 │ │ 10K+ 条性能 │ 开始变慢 │ 稳定 │ │ 适用场景 │ 个人轻量使用 │ 大量记忆/多 Agent │ └──────────────────┴────────────────────┴────────────────────┘ 配置: { "memory": { "backend": "file" | "sqlite-vec" } }4.4 索引一致性保障
BM25 和 HNSW 是两份独立索引,需确保与磁盘上的记忆记录保持一致:
┌──────────────────────────────────────────────────────────────┐ │ 索引一致性机制 │ │ │ │ [1] 写入时: 原子化三步写入 │ │ ├─ WAL (Write-Ahead Log) 先记录写入意图 │ │ ├─ 写 records/{user|agent}/<id>.json (源数据) │ │ ├─ 追加 BM25 posting │ │ └─ 追加 HNSW 向量 │ │ 写入失败时, 下次启动回放 WAL 补齐缺失索引条目 │ │ │ │ [2] 启动时: 完整性校验 │ │ ├─ 扫描 records/ 获取文件计数 │ │ ├─ 对比 metadata.json 中的 totalDocs │ │ └─ 不一致 → 触发增量重建 (仅补齐差异部分) │ │ │ │ [3] 定期维护: openclaw memory index │ │ ├─ 全量校验 BM25 / HNSW / metadata 三者一致 │ │ └─ 需要重建时,执行重新索引流程 │ └──────────────────────────────────────────────────────────────┘4.5 多语言全文搜索
v2026.2.22 新增:
混合语言处理:
• 自动检测文本语言(franc 库) • 同一查询可包含多语言关键词 • 每种语言独立分词后合并 • BM25 索引按语言分桶(避免跨语言干扰)
5. FSRS-6 遗忘算法
FSRS-6(Free Spaced Repetition Scheduler v6)借鉴自 Anki 记忆卡片系统。
5.1 核心思路: "软遗忘"
传统遗忘 (硬删除): FSRS-6 (软遗忘): ┌────────────────────┐ ┌────────────────────┐ │ 30天没访问 → 删除 │ │ 30天没访问 → 降级 │ │ │ │ │ │ 记忆消失了 │ │ 记忆还在,排名很低 │ │ 永远找不回来 │ │ 如果再次被触发 │ │ │ │ → 优先级立刻回升 │ │ 风险: 删掉关键信息 │ │ → "想起来"了 │ └────────────────────┘ │ │ │ 安全: 不丢信息 │ └────────────────────┘5.2 算法机制
每条记忆条目携带 FSRS 字段: ┌─────────────────────────────────────────────┐ │ memory_record.fsrs = { │ │ stability: 12.5, // 记忆稳定性(天) │ │ difficulty: 0.3, // 记忆难度 [0,1] │ │ lastReview: "2026-02-20T10:00:00Z", │ │ nextReview: "2026-03-04T22:00:00Z", │ │ reps: 5, // 被访问次数 │ │ lapses: 1 // 遗忘次数 │ │ } │ └─────────────────────────────────────────────┘ 优先级计算: FSRS 定义 stability S = 记忆保持率降到 90% 所需的天数 retrievability R(t) = 0.9^(t / S) • 刚被访问 (t≈0) → R ≈ 1.0 → 排名靠前 • t = S 天后 → R = 0.9 → 仍在阈值上 • t = 3S 天后 → R = 0.73 → 开始衰减 • 长时间没访问 → R → 0 → 排名靠后(但不删除) • 被重新访问 → stability 增加 → 衰减变慢 更新规则 (检索命中时): ┌────────────────────────────────────────────────┐ │ [1] reps += 1 │ │ [2] stability = stability × (1 + factor) │ │ factor 取决于 difficulty、elapsed、R(t) │ │ [3] difficulty 微调 (根据检索排名) │ │ [4] lastReview = now │ │ [5] nextReview = now + new_stability │ │ 即: 以更新后的 stability 为间隔安排下次复习 │ │ 例: stability=12.5d → 12.5 天后 R 降至 90% │ └────────────────────────────────────────────────┘5.3 与 Generative Agents 的三维评分对比
维度映射对比:
5.4 记忆注入 Context 的格式
检索命中的记忆条目在 Context 组装阶段(见 02-agent-runtime.md §6.2 步骤 3)被格式化为 Markdown 块,拼接在 System Prompt 尾部:
注入模板: ## 关于此用户的记忆 以下是你过去与此用户交互中积累的记忆,按相关性排序: - [偏好, 记于 2026-02-15] 用户偏好 TypeScript,不喜欢 Java - [档案, 记于 2026-01-20] 前端工程师,32 岁,在杭州 - [洞察, 记于 2026-02-28] 用户对视觉体验要求高,倾向简洁暗色设计 - [事件, 记于 2026-02-14] 用户提交了 PR #342,修复了登录 bug 格式规则: • 每条 = [category, 记于 createdAt] + L0 摘要 • 高相关性条目自动展开为 L1(~500 tokens) • 洞察条目(insight)排在同分事实条目之前 • 带时间戳 → 让模型自行判断时效性(见去重陷阱 4) • 总 Token 预算由 memoryMaxTokens 控制(默认 3000)6. 记忆写入流程
6.1 自动提取
Agent Loop 结束后异步触发。为避免高频对话产生冗余提取,实际有两层限流:(1) 连续 Agent Loop 间隔 < 30s 时合并为一次提取;(2) 同一 Session 每小时最多触发 10 次提取。
各步骤说明:
• [2] LLM 提取:用轻量模型提取 facts / preferences / events / lessons,~500-1000 tokens • [3] 分类:facts → profile / event,preferences → preference,lessons → lesson • [4] 确定性去重: hash(normalized(L0))完全匹配则跳过• 语义去重三区间: < 0.7新条目 /0.7-0.92灰区走 LLM 判断 /> 0.92近似重复• [4c] 合并策略: no-merge(event/case)跳过;overwrite/latest-wins/append合并• [5] Embedding:L0 → Embedding 模型 → Float32Array,批量处理 • [6] 持久化:写 records/ 文件 + 更新 BM25 / HNSW 索引 + metadata
错误处理与降级:
提取流程中任一步骤失败时的降级策略:
pending,下次 openclaw memory index 补齐 | ||
memory:error 事件通知上层 | ||
cosine > 0.85 视为重复跳过,≤ 0.85 视为新条目写入 |
6.2 去重陷阱
陷阱 1:重复判为矛盾 (Mem0 #1674)
• 存: "我喜欢咖啡"→ 再说:"我喜欢咖啡"• 预期 NOOP,实际 LLM 判为"矛盾" → DELETE → 偏好丢失 • 防御:先用确定性去重(hash + cosine),LLM 只在 cosine 0.7-0.92 灰区介入
陷阱 2:First Write Wins (cognee #1831)
• 存: "张三是工程师"→ 更新:"张三升了经理"• 预期合并更新,实际新属性被静默丢弃 • 防御:profile 类用 overwrite策略,新值总是覆盖旧值
陷阱 3:语义扭曲
• 用户说: "我讨厌西兰花"→ LLM 重述:"用户喜欢蔬菜"• 记忆不是丢了,是被扭曲了 • 防御:L2 始终保留原文;L0 摘要仅用于索引,不用于回忆;模型回忆时优先引用 L1/L2
陷阱 4:时间混淆
• 用户说: "我上周开始学 Rust"→ 提取:"用户在学 Rust"• 3 个月后检索: "用户在学 Rust"→ 仍在学?• 防御:记忆条目始终带 createdAt时间戳,Context 注入时显示"记于 2026-02-01"
7. 反思机制
定期对累积的事实记忆进行反思,提炼高层洞察:
反思触发条件:
1. 累积到 MEMORY_REFLECTION_THRESHOLD(50)条未反思的新记忆2. 定时触发(每 24 小时) 3. 手动触发:通过管理端触发反思任务
各步骤说明:
• [1] 分组:按 profile / event / preference 等类型分组 • [2] 反思:每组独立调用 LLM,提炼高层洞察(最多 5 条),含置信度 + 支撑证据 • [3] 存储: category: insight,mergeStrategy: latest-wins• [4] 标记:原始记忆 FSRS priority 降低,洞察获得更高初始 stability
两层结构示例:
• 洞察层: 张三是项目核心贡献者,提交频率约每周 2 次/用户对视觉体验要求高,倾向简洁暗色设计• 事实层: 2/14 张三提交了 PR #342/用户说他更喜欢暗色主题
洞察如何反哺检索:
┌──────────────────────────────────────────────────────────┐ │ [1] 初始 stability 加成 │ │ 洞察条目 stability = 原始条目平均 stability × 3 │ │ → 衰减更慢,在检索结果中长期保持高排名 │ │ │ │ [2] 原始条目 FSRS 降权 │ │ 已被反思的事实条目 stability × 0.5 │ │ → 检索时优先返回洞察而非零散事实 │ │ → 但原始条目不删除,L2 原文可追溯 │ │ │ │ [3] Context 注入排序 │ │ 同分时 insight 类型排在 event/preference 之前 │ │ → 模型优先看到高层结论,再看细节 │ │ │ │ [4] 洞察的自我迭代 │ │ 下一轮反思可能产生更新的洞察 (latest-wins 合并) │ │ 旧洞察被新洞察覆盖,避免洞察层膨胀 │ └──────────────────────────────────────────────────────────┘8. 跨 Agent 记忆
默认每个 Agent 有独立的 Memory(agents/main/memory/、agents/coding/memory/)。问题:用户在 "main" 说过的偏好,"coding" Agent 不知道。
存储路径:
• 共享层: agents/_shared/memory/user/,所有 Agent 共同读写• 独立层: agents/<agentId>/memory/agent/,每个 Agent 私有• 检索合并: sharedMemory.retrieve+agentMemory.retrieve,去重时共享记忆优先
配置:{ "memory": { "shareUserMemory": true } }
9. 记忆索引与迁移
┌──────────────────────────────────────────────────────┐ │ 记忆导入/导出 │ │ │ │ 迁移前建议: │ │ 1) 备份 ~/.openclaw/agents/<agent>/memory/ │ │ 2) 迁移后执行 openclaw memory index │ │ │ │ 典型记录格式: │ │ { │ │ "version": "1.0", │ │ "agent": "main", │ │ "exportedAt": "2026-02-26T10:00:00Z", │ │ "records": [ │ │ { │ │ "id": "mem-001", │ │ "category": "preference", │ │ "content": { "l0": "...", "l1": "...", "l2": "..." },│ │ "fsrs": { ... }, │ │ "createdAt": "..." │ │ }, │ │ ... │ │ ] │ │ } │ │ │ │ 迁移后处理: │ │ → 去重: 按 id 跳过已存在的记忆 │ │ → 重建 Embedding (模型可能不同 → 维度不兼容) │ │ → 重建 BM25 + HNSW 索引 │ │ → FSRS 字段原样保留 (不重置) │ │ │ │ 用途: │ │ • 迁移到新机器 │ │ • 在多台设备间同步记忆 │ │ • 备份/恢复 │ │ • 从旧版本升级 (version 字段做格式兼容) │ │ │ │ 注意事项: │ │ • Embedding 模型变更 → 必须全量重建向量索引 │ │ (bge-small 384 维 ↔ openai 1536 维 不兼容) │ │ • 导出文件不含 embedding 字段 (体积原因) │ │ → 导入时自动重新生成 │ │ • 共享 User Memory 和 Agent Memory 分开导出/导入 │ │ • Session 历史 (messages.jsonl) 不在此导出范围内 │ │ → 用 cp -r sessions/ 直接复制 │ └──────────────────────────────────────────────────────┘格式版本升级路径:
导出文件的 version 字段用于处理跨版本兼容。升级规则:
tags、source) | ||
content 从 string 改为 {l0, l1, l2} 对象) | openclaw memory migrate --from 1.x --to 2.0 | |
embedding.model 与导出时不同 | openclaw memory index --reembed |
自动升级流程:
1. 导入时读取 version字段,与当前运行版本比较2. 小版本差异 → 自动补齐缺失字段,静默完成 3. 大版本差异 → 拒绝导入,提示用户先运行 openclaw memory migrate4. 迁移脚本保留原文件备份( *.bak),可回滚
10. 存储位置
~/.openclaw/agents/<agentId>/ ├── sessions/ │ ├── s-abc123/ │ │ ├── meta.json Session 元数据 │ │ ├── messages.jsonl 消息历史 (JSONL) │ │ └── context.json 修剪后上下文快照 │ └── s-def456/ │ └── ... └── memory/ ├── index/ 检索索引 (文件后端 / sqlite-vec) │ ├── bm25.idx BM25 倒排索引 │ ├── vector.idx HNSW 向量索引 │ ├── metadata.json 记忆元数据 │ └── config.json 索引配置 ├── records/ 记忆条目原文 │ ├── user/ User Memory (可配置共享) │ │ ├── mem-001.json │ │ └── ... │ └── agent/ Agent Memory (独立) │ ├── mem-100.json │ └── ... └── reflections/ 反思洞察 └── insights.json 共享 User Memory (如果启用): ~/.openclaw/agents/_shared/memory/user/ ├── index/ └── records/11. 性能特征
测试环境参考:M2 MacBook Pro / Node.js 22 / SSD / ~3K 条记忆 / text-embedding-3-small。实际数值因硬件和记忆规模而异。
| 单 Agent 记忆容量 | 100,000+ 条 |
| 向量索引内存占用 (10K条, 1536维) | ~60MB |
| 向量索引内存占用 (10K条, 384维) | ~4MB |
| BM25 索引大小 (10K条) | ~2MB |
12. 隐私与数据安全
记忆系统存储高度敏感的用户信息(偏好、事件、档案),安全设计直接影响用户信任。
┌──────────────────────────────────────────────────────────┐ │ [1] 存储安全 │ │ • 所有数据存在用户本地 (~/.openclaw/),不上传云端 │ │ • 文件权限: 目录 700, 文件 600 (仅 owner 可读写) │ │ • 加密: 当前版本明文存储; 规划中支持 AES-256-GCM │ │ 静态加密,密钥由用户 passphrase 派生 │ │ │ │ [2] 数据删除 │ │ • openclaw memory delete <id> │ │ → 删除 records/ 文件 + 从 BM25/HNSW 索引中移除 │ │ • openclaw memory purge --user <userId> │ │ → 批量删除某用户的所有记忆("被遗忘权") │ │ • 删除后: 索引标记 tombstone,下次 index 时物理清除 │ │ │ │ [3] Embedding 隐私 │ │ • 使用 OpenAI API 生成 Embedding 时,L0 摘要会发送到 │ │ 外部服务器(已在 L0 层做脱敏/摘要化) │ │ • 本地 ONNX 模型: 零数据外泄 │ │ • 配置建议: 高敏感场景使用本地 Embedding 模型 │ │ │ │ [4] LLM 提取隐私 │ │ • 记忆提取和反思需调用 LLM,对话片段会发送到模型 │ │ • 缓解: 使用本地模型 (Ollama) 做提取; 或配置 │ │ memory.extractModel 使用低成本小模型减少暴露面 │ └──────────────────────────────────────────────────────────┘
夜雨聆风