OpenClaw源码解析(十七):记忆存储与索引:有哪些文件,怎么落库,为什么这样选
1. 先回答:有哪些“记忆文件”
当前实现里,长期记忆相关的数据源至少分三类。
1.1 工作区显式记忆文件
默认路径是:
-
<workspace>/MEMORY.md -
<workspace>/memory/**/*.md
这里的语义最直接:
-
用户或系统明确写下来的长期记忆
1.2 额外自定义 markdown 路径
通过 extraPaths 配置,可以把额外文件或目录纳入记忆源。
这些也必须是 markdown。
1.3 session transcripts
这不是默认长期记忆,而是可选来源。
两种路径会让 session 内容进入记忆相关系统:
-
builtin memory 开启 experimental sources: [“sessions”] -
QMD backend 配置 sessions export/index
所以 session transcript 在 OpenClaw 里很特殊:
-
它天然是短期工作记忆的原材料 -
但在特定配置或导出链下,也可以成为长期记忆数据源
2. 短期记忆 vs 长期记忆:当前实现里的真正边界
这里要非常明确。
2.1 短期记忆
机制是:
-
session transcript -
context engine -
system prompt / prompt assembly
特点:
-
面向当前或相邻几轮任务 -
直接参与 prompt -
更强调当前状态连续性
2.2 长期记忆
机制是:
-
markdown 文件 -
可选 session transcript 索引/导出 -
sqlite / qmd 索引 -
memorysearch / memoryget 按需召回
特点:
-
不直接自动进入 prompt -
先索引、后检索 -
更强调跨会话持久性
所以不要把 “session transcript 被索引” 理解成“短期记忆和长期记忆没区别”。
区别仍然在于:
-
一个是当前运行时工作集 -
一个是可召回历史库
3. builtin backend 用什么数据库
当前 builtin backend 使用:
-
node:sqlite
这是 Node 内建 sqlite 接口,通过 src/memory/sqlite.ts 做 require 包装。
3.1 为什么不是外部数据库
从当前代码结构看,选择 sqlite 的工程动机很明显:
-
本地单机使用为主 -
零服务依赖 -
同时承载 metadata、FTS、embedding cache 很方便 -
配合扩展可以做向量检索
这非常符合 OpenClaw 的整体风格:
默认优先本地、可携带、低运维成本的内建能力。
3.2 为什么单独包一层 requireNodeSqlite()
因为不同 Node 发行版不一定带 node:sqlite。
所以这层包装的作用是:
-
把缺失 sqlite runtime 的情况变成可理解的错误 -
而不是让系统抛出模糊的 builtin module 错误
4. builtin memory 的数据库 schema 长什么样
ensureMemoryIndexSchema(…) 定义了几张核心表。
4.1 meta
存:
-
索引元信息
比如:
-
provider/model -
chunk 配置 -
sources -
vector dims
4.2 files
存:
-
path -
source -
hash -
mtime -
size
它的意义是:
记录当前有哪些源文件,以及这些文件内容是否变化。
4.3 chunks
存:
-
chunk id -
path -
source -
起止行 -
hash -
model -
text -
embedding -
updated_at
这张表是 builtin recall 的核心文本块表。
4.4 embedding_cache
存:
-
provider -
model -
provider_key -
hash -
embedding -
dims -
updated_at
这是非常值得学习的一张表。
它说明系统不是每次索引都重新算 embedding,而是:
对同样文本 hash 的 embedding 进行跨次缓存。
5. builtin backend 不只存普通表,还依赖两类检索结构
5.1 FTS5 虚表
chunks_fts
用途:
-
关键词检索 -
支撑 hybrid recall -
也支撑 FTS-only 降级模式
5.2 sqlite-vec 虚表
chunks_vec
用途:
-
向量检索
但它不是必然可用的,是:
-
可选 -
动态探测 -
带超时加载
这说明 builtin backend 本质上是:
sqlite 元数据表+ FTS5+ 可选 sqlite-vec+ embedding cache
而不是只有一张“向量表”。
6. 为什么 builtin 要同时保留 FTS 和 vector
因为这两者解决的问题不一样。
6.1 FTS 的优势
-
不依赖 embedding provider -
对精确关键词/文件名/术语更敏感 -
可作为降级路径
6.2 vector 的优势
-
对语义相近表达有更强召回 -
对口语化 query 更友好
6.3 两者合用的意义
这解释了为什么 current design 不是“纯 vector DB”:
-
纯 vector 容易丢掉精确 lexical hit -
纯 FTS 又难处理语义近邻
所以 builtin backend 的技术选型体现的是:
不是追求“最潮数据库”,而是追求单机可用前提下的召回韧性。
7. builtin memory 的默认存储路径为什么按 agent 隔离
resolveStorePath(…) 默认给的是:
-
stateDir/memory/<agentId>.sqlite
这意味着:
-
每个 agent 默认有自己独立的 memory DB -
不同 agent 可以有不同记忆视图
这和 OpenClaw 其他能力按 agent 隔离的设计是一致的。
所以记忆不是全局一锅端,而是默认 agent-scoped。
8. markdown 文件是怎么变成 chunk 的
builtin manager 索引 markdown 文件时会经过:
-
listMemoryFiles(…) -
buildFileEntry(…) -
chunkMarkdown(…)
8.1 listMemoryFiles(…)
负责:
-
找到 MEMORY.md -
找到 memory/**/*.md -
找到 extraPaths 下的 markdown -
过滤掉 symlink -
去重
8.2 buildFileEntry(…)
负责:
-
读文件 -
算 hash -
记录相对路径、mtime、size
8.3 chunkMarkdown(…)
负责:
-
按大致 token 预算切块 -
保留 overlap -
记录每个块的 startLine / endLine
这里默认用的是一个粗略换算:
-
tokens * 4 ~= chars
这表明当前 chunking 是偏工程化近似,而不是 tokenizer 精细切块。
9. session transcript 是怎么变成可索引文本的
如果启用了 sessions source,或 QMD 需要导出 sessions,会走:
-
listSessionFilesForAgent(…) -
buildSessionEntry(…)
buildSessionEntry(…) 很值得注意:
-
只提取 user / assistant -
跳过 tool messages -
只保留 text 内容 -
做 redactSensitiveText(…) -
最终拼成: -
User: … -
Assistant: …
也就是说,session 进入长期记忆索引前不是原样 JSONL,而是:
提炼成对 recall 更友好的纯文本会话摘要流。
这个转换很重要,因为它明确告诉你:
-
长期记忆检索不想索引完整工具协议噪声 -
更重视人类可理解的 user/assistant 语义
10. builtin sync 什么时候发生
MemoryManagerSyncOps 里有几种触发源:
10.1 on session start
-
warmSession(…)
10.2 on search
-
dirty 时由 search(…) 触发
10.3 file watcher
-
chokidar 监听 markdown 变化
10.4 session transcript update listener
-
onSessionTranscriptUpdate(…) -
增量计算 bytes/messages 变化
10.5 interval sync
-
定时刷新
所以 builtin backend 并不是“手动 rebuild 一次”的模式,而是:
watcher + on-demand sync + interval sync 的混合模型。
11. 为什么 session source 还有 deltaBytes / deltaMessages 阈值
因为 session transcript 变化比 markdown 文件更频繁。
如果每写一行 JSONL 就立刻重建索引,代价太高。
所以系统会累计:
-
新增字节数 -
新增消息数
只有达到阈值才把该 session 标成 dirty 并触发同步。
这是一种很典型的“高频事件 -> 低频索引刷新”的节流设计。
12. builtin 索引时为什么会跳过 embedding sync
当前代码里,如果没有 embedding provider,sync 会:
-
直接跳过 embedding sync
日志里明确叫:
-
FTS-only mode
这再次说明 builtin memory 的核心目标不是“非得有向量库”,而是:
先尽量让记忆系统有可用搜索,再争取更好的语义召回。
13. QMD backend 的存储与索引思路完全不同
QMD backend 不是复用 builtin sqlite schema,而是使用:
-
QMD 命令行工具 -
QMD 自己维护的 index.sqlite -
XDG config/cache 目录
QMD manager 会为每个 agent 准备:
-
agentStateDir/qmd/xdg-config -
agentStateDir/qmd/xdg-cache/qmd/index.sqlite
所以它本质上是:
外部索引系统,以 OpenClaw agent 为作用域进行托管。
14. QMD backend 的“记忆文件”组织方式
QMD 不直接认 OpenClaw 的 files/chunks 表,而是认 collections。
它会解析并维护:
-
default memory collections -
custom path collections -
optional sessions collection
每个 collection 有:
-
name -
path -
pattern -
kind
这说明 QMD 路径更接近:
多集合文档索引系统
而 builtin 更接近:
单 sqlite 库里的多 source 混合索引
15. QMD 的 session memory 是怎么做的
QMD 不是直接搜 OpenClaw session JSONL。
它会:
-
先导出 session 为 markdown 到 exportDir -
再把这些导出文件纳入 collection
这样做的好处是:
-
QMD 处理的是统一文档格式 -
与 builtin 的 session text 抽取思路类似,都是先把 transcript 变成 recall 友好的文档
所以在 QMD 路径里,session memory 更像:
会话导出文档化,再交给外部索引器。
16. 为什么 search-manager 对 QMD 还要套 builtin fallback
因为 QMD 有自己的额外风险:
-
CLI 不可用 -
collection 元数据损坏 -
update/search 超时 -
MCP/mcporter 链路问题
而 builtin 至少能在本地 sqlite 上生存。
这就是为什么 search-manager 的架构不是:
-
“qmd 开了就彻底替代 builtin”
而是:
-
“qmd 优先,builtin 保底”
17. 这一篇最重要的结论
-
builtin backend 的真实技术栈是 sqlite + FTS5 + 可选 sqlite-vec + embedding cache。 -
长期记忆文件默认是 MEMORY.md 和 memory/**/*.md,可扩展到 extraPaths 和 sessions。 -
session transcript 进入长期记忆时会被转译成更适合 recall 的纯文本格式。 -
sync 不是手工 rebuild,而是 watcher、search、session update、interval 共同驱动。 -
QMD backend 是一套外部集合索引体系,不是 builtin sqlite 的简单替皮。
夜雨聆风