OpenClaw 是怎么用 SQLite 的
从零理解嵌入式数据库:事务、触发器、全文索引、向量搜索
SQLite 是世界上部署最广的数据库。手机里的微信、飞机上的娱乐系统、你的浏览器——背后都可能跑着一个 SQLite。OpenClaw 的 AI 记忆系统,也选择了它。
🧠 先搞清楚:SQLite 和其他数据库有什么不同?
提到数据库,大多数人脑子里浮现的是 MySQL 或 PostgreSQL——需要单独安装、需要启动一个服务进程、需要配置端口和密码,应用程序通过网络连接过去。
SQLite 完全不一样。它是一个嵌入式数据库,没有服务进程,不占用端口,整个数据库就是本地磁盘上的一个文件(比如 memory.db)。应用程序直接加载 SQLite 库,像读写普通文件一样操作它。
MySQL / PostgreSQL 的架构:
应用 ──网络──> 数据库服务进程 ──> 数据文件(分散在磁盘多处)
SQLite 的架构:
应用 ──直接读写──> 一个 .db 文件
优点很直接:零配置、零依赖、可以随应用一起打包分发。代价是它不适合多台机器同时写入的场景——但对于 OpenClaw 这样的本地 AI 助手,这完全不是问题。
🏛️ SQLite 的核心概念
在看 OpenClaw 具体怎么用之前,先把几个基础概念讲清楚。
表和索引
表(Table)就是数据存储的地方,可以把它理解成一张电子表格,每一行是一条记录,每一列是一个字段。
索引(Index)是为了加速查询。假设你有一张存了 10 万条记录的表,每次查询都从第一行扫到最后一行,速度会很慢。建了索引之后,SQLite 会额外维护一个排好序的数据结构,查询时可以直接定位,速度大幅提升。代价是写入时要同时更新索引,占用更多磁盘空间。
事务
事务(Transaction)是数据库里「要么全做,要么全不做」的机制。
想象你在批量写入 500 条记录。写到第 300 条时,程序崩溃了——这 300 条写进去了,剩下 200 条没写,数据就乱了。
有了事务,你可以把这 500 条写入包在一个事务里:
BEGIN(开始)
→ 写第 1 条
→ 写第 2 条
→ ...
→ 写第 500 条
COMMIT(提交,全部生效)
如果中途出错:
ROLLBACK(回滚,全部撤销,数据库回到原始状态)
OpenClaw 在批量写入 embedding 缓存时就用了事务——把几百条缓存记录包在一个 BEGIN / COMMIT 里,要么全写成功,要么全不写。这样即使中途出错,数据库也不会处于半写入的混乱状态。
触发器
触发器(Trigger)是「当某件事发生时,自动做另一件事」的机制。
比如:每当有一条新记录插入 A 表,自动把这条记录的某些字段同步到 B 表。这个「自动同步」的动作就是触发器,不需要应用程序手动写代码去做。
OpenClaw 的全文搜索索引就是用触发器来维护的——每次往 chunks 表里写入新数据,触发器自动把内容同步到全文索引表里,保持两边一致。
虚拟表
虚拟表(Virtual Table)是 SQLite 的一个扩展机制。它看起来像一张普通的表,可以用 SQL 查询,但背后不是普通的行列存储,而是一个自定义的数据结构或算法。
OpenClaw 用了两种虚拟表:
- FTS5:SQLite 内置的全文搜索虚拟表,背后是倒排索引
- vec0:
sqlite-vec扩展提供的向量搜索虚拟表,背后是向量相似度计算
WAL 模式
WAL(Write-Ahead Logging,预写日志)是 SQLite 的一种写入模式。
默认模式下,SQLite 写数据时会锁住整个文件,其他读操作必须等待。WAL 模式下,写操作先写到一个单独的 .wal 日志文件,读操作可以继续读旧版本的数据,互不阻塞。
这对需要「读写同时进行」的场景很重要。OpenClaw 在打开数据库后会立刻开启 WAL 模式——一边在后台更新索引,一边响应用户的搜索请求,两者不会互相卡住。
⚙️ OpenClaw 的 SQLite 架构
OpenClaw 的记忆系统把上面这些概念全部用上了,组成了一套完整的本地搜索引擎。
数据库文件放在哪
每个 Agent 有自己独立的数据库文件,存在 ~/.openclaw/agents/<agentId>/ 目录下。Agent 之间的记忆完全隔离,互不干扰。
应用启动时,OpenClaw 会打开(或创建)这个文件,同时加载 sqlite-vec 这个 C 扩展——这是一个原生动态库,加载进来之后 SQLite 就获得了向量搜索的能力。如果加载失败(比如某些环境不支持),OpenClaw 会自动降级到纯全文搜索模式,不会崩溃。
四张核心表
┌──────────────────────────────────────────────────────────┐
│ memory.db │
│ │
│ meta 存数据库的版本信息和配置元数据 │
│ files 记录哪些文件被索引了(路径、hash、大小) │
│ chunks 文件切片后的内容 + embedding 向量 │
│ embedding_cache 向量的缓存,避免重复调用 embedding API │
│ │
│ chunks_fts FTS5 虚拟表(全文索引,触发器同步) │
│ chunks_vec vec0 虚拟表(向量索引,sqlite-vec 提供) │
└──────────────────────────────────────────────────────────┘
files 表记录的是「源头」:哪个文件被索引了,文件的 hash 是什么。每次文件发生变化,OpenClaw 对比 hash 就能知道哪些文件需要重新索引,不用每次全量扫描。
chunks 表是核心。OpenClaw 不会把整个文件存成一条记录,而是把文件按段落或 token 数量切成小块(chunk),每块单独存一行,同时存一个 embedding 向量。搜索时是在 chunk 级别做的,这样可以精确定位到文件的具体段落,而不是笼统地返回整个文件。
embedding_cache 表是性能优化。同样的文本调用 embedding API 会得到同样的向量,没必要重复调用。OpenClaw 把 (provider, model, 文本hash) → 向量 的映射缓存在这张表里,再次遇到相同内容直接取缓存,省掉网络请求和费用。
全文搜索:FTS5 + 触发器
chunks_fts 是一张 FTS5 虚拟表,背后是一个倒排索引——简单理解就是「每个词 → 出现在哪些 chunk 里」的映射表。搜索关键词时,直接查这个映射,比全表扫描快很多。
OpenClaw 用触发器把 FTS 索引和 chunks 表绑定在一起:
- 往
chunks插入新记录 → FTS 自动添加 - 从
chunks删除记录 → FTS 自动删除 - 更新
chunks的内容 → FTS 自动更新
这样应用层完全不用手动维护索引,写数据库的代码和搜索代码可以独立开发,不会出现「数据更新了但索引没更新」的问题。
向量搜索:vec0 虚拟表
chunks_vec 是 sqlite-vec 扩展提供的向量虚拟表,每条记录存一个高维向量(比如 1536 维的 OpenAI embedding)。
向量搜索的原理是:把用户的查询也转成向量,然后在所有存储的向量里找「最近的邻居」(余弦相似度最高的)。OpenClaw 把这个能力封装成标准 SQL 查询,从应用层看起来和查普通表没什么区别。
向量搜索的优势是能理解语义——「帮我写段代码」和「帮我编程」意思一样,全文搜索不一定能匹配上,但向量搜索可以。
两种搜索合并:混合搜索
OpenClaw 同时跑 FTS 全文搜索和向量搜索,然后把两个结果合并排序,取最终 Top N。
这是因为两种方法各有盲区:
- 全文搜索擅长精确词语匹配,但不理解语义
- 向量搜索理解语义,但对专有名词(比如函数名、错误码)可能不够精确
两者结合,覆盖面更广,准确率更高。
原子性重建索引
有时候需要重建整个索引(比如换了 embedding 模型、模型的维度变了)。OpenClaw 不会直接在原来的数据库文件上操作,而是:
- 新建一个临时数据库文件,在里面构建新索引
- 构建完成后,把临时文件「换」成正式文件(原子替换)
整个过程要么完全成功,要么完全失败,不会出现索引只建了一半的中间状态。搭配 WAL 模式,替换期间正在进行的搜索请求也不受影响。
🔍 文件监听触发同步
OpenClaw 还用了一个有意思的设计:文件系统监听。
用户的 workspace 目录(也就是 MEMORY.md、各种笔记文件所在的地方)由 chokidar 这个库实时监听。一旦有文件被修改、新增、删除,OpenClaw 会在几秒钟后自动触发重新索引,把最新的内容同步到数据库。
这意味着你用任何编辑器修改记忆文件,OpenClaw 都能自动感知,不需要手动刷新。
📝 总结
OpenClaw 的记忆系统,本质上是把 SQLite 当成一个本地搜索引擎来用,而不只是一个存数据的地方。
| 能力 | SQLite 的哪个特性 |
|---|---|
| 全文关键词搜索 | FTS5 虚拟表 + 倒排索引 |
| 语义向量搜索 | vec0 虚拟表(sqlite-vec 扩展) |
| 索引自动同步 | 触发器 |
| 批量写入安全 | 事务(BEGIN / COMMIT / ROLLBACK) |
| 读写不互相阻塞 | WAL 模式 |
| 原子重建索引 | 临时文件 + 文件替换 |
一个 .db 文件,零服务依赖,把这些能力全部打包进去——这大概就是 SQLite 在 2026 年还这么流行的原因。