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 它做什么
大致流程是:
-
找到重置前的旧 session -
过滤只提取最近 15 条 user/assistant 消息 -
用 LLM 生成描述性的标识 -
在 <workspace>/memory/ 下创建新的 markdown 记忆文件 -
给用户一个确认
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. 这一篇最重要的结论
-
OpenClaw 的长期记忆不只是“搜已有文件”,还包括“把当前经验沉淀成记忆文件”的路径。 -
session-memory hook 解决的是会话切换时的记忆沉淀。 -
memory flush 解决的是 compaction 前的知识保全。 -
experimental sessions source 解决的是“原始 transcript 直接可检索”,但它和正式 memory markdown 不是同一层东西。 -
当前实现里,新增/更新/恢复记忆分别对应不同机制,不能混成一个“memory sync”。
夜雨聆风