乐于分享
好东西不私藏

OpenClaw源码解析(十七):记忆存储与索引:有哪些文件,怎么落库,为什么这样选

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/&lt;agentId&gt;.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. 这一篇最重要的结论

  1. builtin backend 的真实技术栈是 sqlite + FTS5 + 可选 sqlite-vec + embedding cache
  2. 长期记忆文件默认是 MEMORY.md 和 memory/**/*.md,可扩展到 extraPaths 和 sessions。
  3. session transcript 进入长期记忆时会被转译成更适合 recall 的纯文本格式。
  4. sync 不是手工 rebuild,而是 watcher、search、session update、interval 共同驱动。
  5. QMD backend 是一套外部集合索引体系,不是 builtin sqlite 的简单替皮。