乐于分享
好东西不私藏

OpenClaw源码解析(十九):记忆系统完结篇

OpenClaw源码解析(十九):记忆系统完结篇

核心文件:

– src/hooks/bundled/session-memory/HOOK.md
– src/hooks/bundled/session-memory/handler.ts
– src/auto-reply/reply/memory-flush.ts
– src/auto-reply/reply/agent-runner-memory.ts
– src/memory/session-files.ts
– src/agents/memory-search.ts


1. 这一篇不是“怎么搜”,而是“记忆怎么被生成和刷新”

前面几篇讲的是:

  • 记忆放哪
  • 怎么索引
  • 怎么检索

但一个完整的记忆系统还必须回答:

新记忆从哪里来?什么时候落盘?什么时候更新?什么时候会把旧会话经验沉淀成长期记忆?

当前实现里,主要有三条路径。


2. 路径一:用户显式维护 MEMORY.md / memory/*.md

这是最直接的一条。

特点:

  • 用户手写
  • agent 也可能在明确指令下写
  • watcher / sync 会把这些变更索引进长期记忆系统

这是最传统的长期记忆路径:

写 markdown -> 同步索引 -> 可被 memory_search 检索

3. 路径二:/new / /reset 的 session-memory hook

这是当前实现里最明确的“自动沉淀会话记忆”路径。

根据 src/hooks/bundled/session-memory/HOOK.md,它会在以下运行

  • command:new
  • command:reset

3.1 它做什么

大致流程是:

  1. 找到重置前的旧 session
  2. 过滤只提取最近 15 条 user/assistant 消息
  3. 用 LLM 生成描述性的标识
  4. 在 <workspace>/memory/ 下创建新的 markdown 记忆文件
  5. 给用户一个确认

3.2 它为什么重要

因为它回答了一个非常现实的问题:

用户主动开新会话时,旧会话里可能有值得保留的经验,应该怎样顺手沉淀下来?

这不是检索层,而是 capture 层。

3.3 它生成的东西是什么

是标准 markdown 文件,后续会被 builtin 或 qmd 后端当作普通长期记忆源继续索引。

也就是说:

session-memory hook = 会话 -> markdown 记忆文件 的转换器

4. 路径三:pre-compaction memory flush

memory-flush.ts 和 agent-runner-memory.ts 实现的是另一条沉淀链:

当 session 快接近 compaction 时,会先尝试把值得长期保留的内容写入 memory 文件。

4.1 为什么在 compaction 前做

因为 compaction 的本质是:

  • 压缩当前工作记忆

一旦压完,某些细节就可能只剩摘要,不再适合直接沉淀成长期记忆。

所以 memory flush 的设计思路是:

趁上下文还完整,把 durable memory 先落盘,再允许后续压缩。

4.2 默认 prompt 在说什么

默认 flush prompt 的核心意图非常明确:

  • 这是 pre-compaction memory flush
  • 把 durable memories 存进 memory/YYYY-MM-DD.md
  • 如果文件已存在,只 append,不覆盖
  • 如果没东西可存,就返回静默 token

这说明 flush 的目标不是生成对用户的答复,而是:

借助模型做一次“值得长期保存的信息抽取与落盘”。

4.3 什么时候触发 flush

当前判断至少考虑:

  • 当前上下文 token 逼近 context window
  • reserve tokens floor
  • soft threshold
  • 当前 compaction cycle 是否已经 flush 过
  • transcript 字节数是否达到强制阈值

所以 memory flush 不是每轮都跑,而是接近压缩时的专门保护动作。


5. hasAlreadyFlushedForCurrentCompaction(…) 说明 flush 是按 compaction cycle 去重的

这点非常关键。

系统不是只记录“最近 flush 过没有”,而是记录:

  • 当前 compactionCount
  • 上次 memoryFlushCompactionCount

这样它能判断:

同一个 compaction 周期里不要反复触发 flush。

这样设计的作用是:

  • 上下文已经接近极限时
  • 又因为 flush 自己反复触发更多 flush

6. resolveEffectivePromptTokens(…) 暗示 flush 的判断不是只看历史,还投影了下一轮输入

在 agent-runner-memory.ts 里,flush gating 会把:

  • base prompt tokens
  • 上一轮 output tokens
  • 当前用户 prompt 估计 tokens

合起来看。也就是说,它在判断的不是“此刻 transcript 多大”,而是:

如果马上继续下一轮,输入上下文会不会顶到边界。这是非常典型的前瞻性上下文保护逻辑。


7. experimental sources: [“sessions”] 和 session-memory hook 的区别

这两个很容易混淆,但本质不同。

7.1 sources: [“sessions”]

含义:

  • 直接把 session transcript 索引成可搜索来源

特点:

  • 更自动
  • 更原始
  • 更新频繁

7.2 session-memory hook

含义:

  • 在 /new / /reset 时把旧会话提炼成 markdown 记忆文件

特点:

  • 更人工筛选
  • 更稳定
  • 更像正式长期笔记

所以它们的区别可以压成一句话:

sessions source = 原始会话可检索session-memory hook = 会话经验沉淀成正式记忆文档

8. “恢复记忆”在当前实现里具体指什么

如果你问“记忆怎么恢复”,源码里至少有三种不同含义。

8.1 检索恢复

也就是:

  • 记忆库中已有内容
  • 通过 memorysearch / memoryget 在当前 turn 被找回来

8.2 索引恢复

比如:

  • watcher 发现文件改动
  • session source 达到 delta 阈值
  • onSearch / onSessionStart / interval 触发 sync

这时系统是在“把最新文件状态恢复进索引”。

8.3 经验沉淀恢复

比如:

  • /new / /reset hook
  • pre-compaction memory flush

这时系统是在“把本来只存在于当前工作记忆里的东西,恢复/沉淀成以后可搜索的长期记忆”。

所以“恢复记忆”不是一个动作,而是三层。


9. 为什么 memory flush 不等于 context engine compaction

因为两者目标不同。

9.1 compaction

目标:

  • 减轻当前工作记忆负担

9.2 memory flush

目标:

  • 在减轻负担前,先把 durable 信息保存到长期记忆

所以 memory flush 的角色更像:

compaction 之前的知识保全步骤。

这正是 memory system 和 context engine 的典型交界面。


10. 这一篇最重要的结论

  1. OpenClaw 的长期记忆不只是“搜已有文件”,还包括“把当前经验沉淀成记忆文件”的路径。
  2. session-memory hook 解决的是会话切换时的记忆沉淀。
  3. memory flush 解决的是 compaction 前的知识保全。
  4. experimental sessions source 解决的是“原始 transcript 直接可检索”,但它和正式 memory markdown 不是同一层东西。
  5. 当前实现里,新增/更新/恢复记忆分别对应不同机制,不能混成一个“memory sync”。