详解 OpenClaw 的记忆实现:文件、检索、主动召回和 Memory Flush 是怎么接起来的
这篇我想直接回答一个很具体的问题:OpenClaw 里的“记忆”到底是怎么实现的。
最开始我以为它就是一个向量库加检索。
但对着源码看一圈以后,感觉不能这么概括。OpenClaw 现在这套实现,至少要拆成四层看:
- 工作区里的记忆文件怎么组织
- 记忆怎么被索引和检索
- 记忆怎么主动插进 prompt
- 上下文太长的时候,记忆又是怎么被“刷回”磁盘的
它不只是 search,还带了 prompt 注入、跨会话召回、session transcript 纳管、memory flush 这些运行时动作。
1. 先说结论:OpenClaw 的“记忆”不是一个点功能,而是一条链
我现在会把它先记成下面这条链:
工作区里的 MEMORY.md / memory/*.md / 额外路径
→ memory backend 建索引(builtin 或 qmd)
→ 需要时通过 memory_search / active-memory 做检索
→ 检索结果进 prompt 或返回给工具调用
→ 对话太长时再通过 memory flush 把当前上下文沉淀回文件这个顺序一旦立起来,后面源码就比较好看了。
因为它说明:
OpenClaw 的记忆不是“存一下再搜一下”,而是贯穿输入、检索、注入、压缩回写的完整回路。
2. 记忆的最底层起点,还是文件
只看运行时,很容易把“记忆”想成数据库里的向量条目。
但 OpenClaw 这边最底层起点其实还是文件。
2.1 根记忆文件是 MEMORY.md
在 root-memory-files.ts 里,能直接看到:
- 规范根记忆文件名:
MEMORY.md - 旧名字:
memory.md - 还有专门的 repair 目录逻辑
里面几个关键函数也很直白:
resolveCanonicalRootMemoryPath(...)resolveCanonicalRootMemoryFile(...)shouldSkipRootMemoryAuxiliaryPath(...)
这几个函数的意思其实就是:
- 当前工作区里,真正被当作根记忆入口的是谁
- 哪些旧文件或修复残留不要重复算进去
所以 OpenClaw 的记忆不是从数据库开始的, 而是先假设:
工作区里有一批可读、可维护、对人也友好的 Markdown 记忆文件。
这一点我挺喜欢。
因为它保留了“人能直接看和改”的那层可控性。
2.2 默认还会扫 memory/ 目录
在 backend-config.ts 里,resolveDefaultCollections(...) 这段默认集合也很关键。
默认会纳入两块:
- 工作区根的
MEMORY.md workspaceDir/memory/**/*.md
这意味着 OpenClaw 默认的记忆布局,大致就是:
workspace/
MEMORY.md
memory/
*.md
**/*.md所以它不是把“记忆”藏在很深的私有数据库里, 而是先把人能维护的 Markdown 文件当成主语料。
3. 但它又不只是文件系统,它后面有完整的 memory backend
只停在文件层,很多事情做不了:
- 快速检索
- 相似度排序
- FTS
- 向量检索
- embedding cache
- 跨会话 recall
所以 OpenClaw 后面又补了一层 memory backend。
3.1 backend 分两类:builtin 和 qmd
这个在 types.memory.ts 和 backend-config.ts 里都能看到。
顶层类型:
MemoryBackend = "builtin" | "qmd"
我的理解是:
builtin:OpenClaw 自己内置这一套 SQLite / FTS / vector 方案qmd:外接 QMD 这一类独立后端
这点很重要。
因为它说明 OpenClaw 没把“记忆”写死成某一种检索实现。
它更像是先定义一套 runtime 契约,再让 backend 去实现它。
3.2 MemorySearchManager 才是它真正的运行时接口
在 host/types.ts 里,MemorySearchManager 这个接口很关键。
它至少包括这些能力:
search(...)readFile(...)status()sync?(...)probeEmbeddingAvailability()probeVectorAvailability()close?()
这一下我就比较清楚了:
OpenClaw 里的记忆不只是 search API,而是一整个 manager。
它既负责:
- 查
- 读
- 同步
- 状态探测
- 后端可用性探测
又要把这些能力统一暴露给上层 runtime。
4. builtin 这条线,本质上是 Markdown → chunk → SQLite / FTS / vector
只看 builtin 方案,我现在会把它理解成一条很标准但做得比较完整的链。
4.1 先 chunk
在 engine-storage.ts 里导出了这些东西:
chunkMarkdownbuildFileEntrybuildMultimodalChunkForIndexingreadMemoryFilenormalizeExtraMemoryPaths
这说明 builtin 方案第一步还是老老实实把 Markdown 切 chunk。
同时在 memory-search.ts 里也能看到默认 chunk 参数:
DEFAULT_CHUNK_TOKENS = 400DEFAULT_CHUNK_OVERLAP = 80
OpenClaw 至少在默认配置上,已经把“记忆不是整文件吃,而是 chunk 化索引”这件事定下来了。
4.2 再落到 SQLite 里的几张表
这块在 memory-schema.ts 里最直观。
几个关键常量:
memory_index_sourcesmemory_index_chunksmemory_index_ftsmemory_index_paths_ftsmemory_index_statememory_embedding_cachememory_index_chunks_vec
我现在会把它粗分成三组:
第一组:来源和状态
sourcesstate
负责知道“哪些文件被纳进来了、现在索引到什么状态”。
第二组:文本检索
chunksftspaths_fts
负责 chunk 文本和全文索引。
第三组:语义检索
embedding_cachechunks_vec
负责 embedding 缓存和向量索引。
所以 builtin 方案不是只有向量,也不是只有 FTS。
它本身就是一个混合检索底盘。
5. 检索配置这块也比我一开始以为的细很多
我原来以为 memory_search 可能就是:
- 查一下 topK
- 回来几条结果
但 memory-search.ts 这块看下来,它把检索配置拆得挺细。
5.1 数据源可以不只一类
ResolvedMemorySearchConfig 里有:
sourcessearchSources
而且源类型至少包括:
memorysessions
这说明 OpenClaw 里“记忆”不是只有手写记忆文件。
当配置打开以后,session transcript 也能变成一个被检索的记忆源。
5.2 它默认就带 hybrid search 思路
在同一个文件里还能看到:
hybrid.enabledvectorWeighttextWeightcandidateMultipliermmr.enabledmmr.lambdatemporalDecay.enabledtemporalDecay.halfLifeDays
OpenClaw 的 builtin memory 搜索至少在配置层已经考虑了:
- 向量分和文本分怎么混
- 结果去重 / 多样化怎么做(MMR)
- 时间衰减怎么做
这让我更愿意把它理解成:
它不是“有 embedding 就行”,而是在认真做检索排序。
5.3 向量并不是强依赖,fallback 可以退回去
同一个文件还能看到:
providerfallbackvector.enabledcache.enabled
再结合 types.memory.ts 看,会更清楚一点:
- embedding provider 可以换
- vector store 可以关
- cache 可以关
- fallback provider 也能设
所以它对“语义记忆”这件事不是死硬绑定的。
这也合理。
因为现实里最常出问题的,往往就是 embedding provider、成本和可用性。
6. rememberAcrossConversations 是我觉得最关键的一个开关
只看“记忆”这个词,很容易混淆两个东西:
- 当前会话里的上下文记住了什么
- 当前 agent 的其他会话能不能被拿来检索
OpenClaw 这里把这件事单独拎出来了。
6.1 类型上就明确了
在 types.memory.ts 里:
rememberAcrossConversations?: boolean;注释写得也很准:
Use relevant context from this agent’s other private conversations.
它不只是“记忆功能开不开”, 而是:
允不允许当前 agent 把别的私有会话当成检索语料。
6.2 配置解析时,这个开关会直接影响 session source
在 memory-search.ts 里, 能看到它会把:
rememberAcrossConversationsexperimental.sessionMemory
组合起来决定 session memory 是否真的纳管。
我的理解是:
experimental.sessionMemory更像能力开关rememberAcrossConversations更像使用策略开关
这两个不是完全一回事。
6.3 active-memory 插件也会专门看它
在 extensions/active-memory/session-policy.ts 里, 也能看到:
hasRememberAcrossConversationsAgent(...)shouldRememberAcrossConversations(...)
这说明跨会话 recall 不是只在底层 backend 起作用, 上层主动召回插件也会按这个策略收边界。
7. session transcript 为什么也会被纳进记忆,我这次也想通了
如果想做跨会话 recall,只搜 MEMORY.md 和 memory/*.md 其实不够。
很多信息根本还没被人手工整理成记忆文件。
所以 OpenClaw 才会把 transcript 也拉进来。
7.1 session-transcript-corpus.ts 就是在干这个事
这个文件本质上是在回答:
- 哪些 transcript 可以被当成 memory corpus
- active session 和 archive artifact 怎么区分
- SQLite transcript 和文件 transcript 怎么统一
- dreaming narrative / cron run 这种内部 session 怎么识别
这里我觉得最关键的是:
OpenClaw 没有把 transcript 当成“顺手附带的数据”,而是明确把它建模成一种 corpus source。
7.2 这也解释了为什么 memory source 会有 sessions
所以现在回头看:
memorysource 更偏人维护的长期记忆文件sessionssource 更偏运行期自然沉淀出来的对话材料
这两个源混在一起以后,OpenClaw 才可能同时做到:
- 能搜你手工整理过的偏稳定事实
- 也能搜最近还没来得及整理进记忆文件的会话内容
8. prompt 注入这条线,不是直接在检索工具里做,而是通过 memory plugin state 接上去
这一点我一开始没看明白,后面才顺过来。
8.1 memory-state.ts 是中间桥
这个文件很像一块注册中心。
里面维护了几类东西:
promptBuilderpromptPreparationscorpusSupplementsruntimeflushPlanResolver
我现在会把它理解成:
memory 插件能力先注册到这里,再由 agent runtime 在适当时机取出来。
8.2 prompt section 不是现拼字符串,而是有 prepared snapshot
在 memory-state.ts 里,有两块我觉得挺关键:
prepareMemoryPromptSection(...)buildMemoryPromptSection(...)
这里的意思大概是:
- 先异步准备一份本轮 run 专属的 memory prompt snapshot
- 再在真正组 prompt 时同步拿出来拼进去
这点其实挺工程化。
因为 context engine 很多时候希望 prompt assembly 是同步的, 但 memory retrieval 前面又可能带异步动作。
所以它中间多做了一层 prepared section 缓冲。
8.3 prepareAgentMemoryPrompt(...) 会把当前工具上下文也带进去
在 memory-prompt-prepare.ts 里, 可以看到它准备 memory prompt 时,不只是传 agentId,还会传:
toolNamescapabilityToolNamescitationsModeagentSessionKeysandboxed
这说明 memory prompt 不是纯静态文案。
它也要知道:
- 当前这轮可用哪些工具
- 当前是不是 sandbox
- 当前是哪个 agent / session
所以记忆注入本身也是 runtime-sensitive 的。
9. 主动召回这条线,其实主要是 active-memory 插件在做
如果只盯 core,很容易少看一半。
因为 OpenClaw 的“主动记忆”不是全在 core 里硬编码, 而是明显通过插件化方式挂出来的。
9.1 active-memory 插件挂在 before_prompt_build
在 extensions/active-memory/index.ts 里, 它会监听:
before_prompt_build
在最终 prompt 真正定稿之前, 它有机会先跑一轮 recall。
这一下就很合理。
因为主动记忆的本质就是:
在主模型回答前,先帮它查一轮“有没有值得插进来的记忆”。
9.2 它不是直接让主模型搜,而是起一个 recall subagent
在 extensions/active-memory/recall.ts 里, 能看到 runRecallSubagent(...) 这条线。
active recall 不是“主模型顺手搜一下”。
它是专门拉出一轮独立的 recall 执行。
我很认同这个设计。
因为主动记忆如果和主回答完全揉在一起,会很容易互相污染:
- 检索 query 不稳定
- recall 判断不稳定
- 主回答受 recall 过程噪声影响太大
拆成 recall subagent 后,边界就清楚很多。
9.3 它还会专门构造 recall prompt 和 search query
在 extensions/active-memory/prompt.ts 里, 可以看到它会单独做:
buildQuery(...)buildSearchQuery(...)buildRecallPrompt(...)
这里的意图很明确:
- 给 recall 子流程一份更适合检索的 query
- 把 JSON fence、旧 recall block、外部不可信块先剥掉
- 再让 recall 子流程只输出很短的 summary 或 NONE
这一下我就更确定了:
OpenClaw 的主动记忆不是简单把 search 结果塞给主模型,而是先做一轮专门的 recall 整理。
10. memory flush 这块,是我这次觉得最有意思的一层
只看 recall,很多系统都会做。
但 OpenClaw 这边还有一层:
- 对话太长以后,怎么把当前上下文刷回记忆文件
这块主要在:
- memory-flush.ts
- agent-runner-memory.ts
- memory-state.ts 的
resolveMemoryFlushPlan(...)
10.1 flush 不是随时做,而是有 gate
在 memory-flush.ts 里,能看到几层判断:
shouldRunMemoryFlush(...)shouldRunPreflightCompaction(...)hasAlreadyFlushedForCurrentCompaction(...)
flush 不是“上下文长了就立刻写文件”。
它会先判断:
- 当前 token 压力是不是到阈值了
- 当前 compaction cycle 里是不是已经 flush 过了
- 这次是 preflight compaction 还是正式 flush
这层很重要。
因为如果没有 gate,memory flush 很容易变成高频噪声写入。
10.2 flush plan 是插件给的,不是 core 写死的
在 memory-state.ts 里:
resolveMemoryFlushPlan(...)
会从 memory capability 里拿 flushPlanResolver。
core 只管:
- 什么时候该 flush
- flush run 怎么调度
但“写到哪个相对路径、用什么 prompt、保留多少 token buffer”这种事, 可以由 memory plugin 决定。
这也是一个很标准的 runtime / policy 分层。
10.3 agent-runner-memory.ts 负责把 flush 真跑起来
这个文件比较长,但我现在只记住它做的几件事:
- 判断当前 run 是否需要 preflight compaction / memory flush
- 确保 flush 目标文件路径在 workspace 里
- 调 embedded agent 跑一轮专门的 memory flush run
- 更新 session entry 上的 flush / compaction 计数
这说明 memory flush 在 OpenClaw 里不是一个同步小函数, 而是一轮受 runtime 管控的 maintenance run。
这点很关键。
因为它再次说明:
OpenClaw 的记忆不是静态知识库,而是会随着对话运行被持续整理和回写的。
11. 所以现在回头看,OpenClaw 的“记忆”其实包含了两种完全不同的东西
看到这里,我自己会把它明确拆成两类:
第一类:可检索记忆层
包括:
MEMORY.mdmemory/*.mdextraPathssessionstranscript corpus- SQLite / FTS / vector / embedding cache
这层的重点是:
- 存什么
- 怎么索引
- 怎么搜
- 怎么排序
第二类:运行时记忆层
包括:
- active-memory recall
- prepared memory prompt section
- cross-conversation gating
- memory flush
- compaction cycle 协同
这层的重点是:
- 什么时候该查
- 查到以后怎么注入
- 太长时怎么沉淀回文件
- 哪些 session / agent / channel 允许这样做
这两层不分开看,源码会很乱。
一分开以后就很清楚了:
前者更像 memory database,后者更像 memory runtime。
12. 如果只用一句话总结我这次看源码后的理解
我现在不会再把 OpenClaw 的记忆实现理解成“向量库 + recall”。
更准确一点说,它像是:
以 Markdown 记忆文件为起点,用 SQLite / FTS / 向量检索做底盘,再通过 active-memory 和 memory flush 把检索、提示词注入、跨会话召回、长上下文沉淀回写串成一条运行链。
所以我觉得这套实现挺有意思。
因为它没有把“记忆”只做成一个工具, 而是把它做成了 agent runtime 的一个正式子系统。
13. 总结
- OpenClaw 的记忆实现起点是工作区 Markdown 文件,而不是纯数据库黑盒
- builtin backend 本身就是 chunk + FTS + vector + embedding cache 的混合检索底盘
rememberAcrossConversations真正控制的是“其他会话能不能进入当前 agent 的检索视野”- 主动记忆主要由
active-memory插件挂在before_prompt_build上完成,而且它会单独起 recall subagent - memory flush 不是附加功能,它其实是长上下文治理的一部分:对话太长时,把当前运行态重新沉到记忆文件里
这篇先记到这里。
