SQLite 存储结构与混合检索深度解析
本文档基于源码分析,详细记录记忆系统的 SQLite 表结构和混合检索实现。
与 08. 记忆系统 互补,本文聚焦底层存储和检索算法细节。
一、数据库位置与驱动
数据库路径:~/.openclaw/state/memory/{agentId}.sqlite
技术栈:
| 组件 | 技术 |
|---|---|
| SQLite 驱动 | Node.js 内置node:sqlite(同步 API) |
| 向量检索扩展 | sqlite-vec(原生编译扩展,通过src/memory/sqlite-vec.ts加载) |
| 全文检索 | SQLite FTS5(内置虚拟表) |
| 向量距离函数 | vec_distance_cosine() |
| 关键词排名 | BM25 算法(FTS5 原生) |
配置路径:agents.defaults.memorySearch.store
store: { driver: "sqlite" // 默认值 path: "~/.openclaw/state/memory/{agentId}.sqlite" vector: { enabled: true // 是否启用 sqlite-vec 向量索引 extensionPath?: string // 自定义 sqlite-vec 扩展路径 } }二、表结构详解(共 6 张表)
2.1meta– 元数据表
源码:src/memory/memory-schema.ts:10-13
CREATETABLEIFNOTEXISTSmeta(keyTEXTPRIMARYKEY,valueTEXTNOTNULL);| 列 | 类型 | 说明 |
|---|---|---|
key | TEXT PK | 元数据键名 |
value | TEXT | 值(如 embedding 模型名、provider、chunk 配置等) |
用途:存储记忆索引的全局配置信息,如当前使用的 embedding 模型、provider 等。
2.2files– 已索引文件追踪表
源码:src/memory/memory-schema.ts:16-22
CREATETABLEIFNOTEXISTSfiles(pathTEXTPRIMARYKEY,sourceTEXTNOTNULLDEFAULT'memory',hashTEXTNOTNULL,mtimeINTEGERNOTNULL,sizeINTEGERNOTNULL);| 列 | 类型 | 说明 |
|---|---|---|
path | TEXT PK | 文件路径 |
source | TEXT | 来源标识(默认'memory') |
hash | TEXT | 文件内容哈希 |
mtime | INTEGER | 文件修改时间(Unix 时间戳) |
size | INTEGER | 文件大小(字节) |
用途:增量同步的基础。通过hash和mtime判断文件是否变更,避免重复索引。
2.3chunks– 核心文本块表
源码:src/memory/memory-schema.ts:25-36
CREATETABLEIFNOTEXISTSchunks(idTEXTPRIMARYKEY,pathTEXTNOTNULL,sourceTEXTNOTNULLDEFAULT'memory',start_lineINTEGERNOTNULL,end_lineINTEGERNOTNULL,hashTEXTNOTNULL,modelTEXTNOTNULL,textTEXTNOTNULL,embeddingTEXTNOTNULL,updated_atINTEGERNOTNULL);| 列 | 类型 | 说明 |
|---|---|---|
id | TEXT PK | 块唯一标识 |
path | TEXT | 所属文件路径 |
source | TEXT | 来源标识 |
start_line | INTEGER | 块在文件中的起始行号 |
end_line | INTEGER | 块在文件中的结束行号 |
hash | TEXT | 块内容哈希 |
model | TEXT | 生成 embedding 所用的模型名 |
text | TEXT | 块的原文内容 |
embedding | TEXT | embedding 向量(JSON 序列化的浮点数组) |
updated_at | INTEGER | 更新时间戳 |
索引:
idx_chunks_path ON chunks(path)idx_chunks_source ON chunks(source)
分块配置(默认值):
chunking: { tokens: 400 // 每块 400 tokens overlap: 80 // 块间重叠 80 tokens }2.4embedding_cache– embedding 缓存表
源码:src/memory/memory-schema.ts:39-48
CREATETABLEIFNOTEXISTSembedding_cache(providerTEXTNOTNULL,modelTEXTNOTNULL,provider_keyTEXTNOTNULL,hashTEXTNOTNULL,embeddingTEXTNOTNULL,dimsINTEGER,updated_atINTEGERNOTNULL,PRIMARYKEY(provider,model,provider_key,hash));| 列 | 类型 | 说明 |
|---|---|---|
provider | TEXT | embedding 提供商(openai/gemini/voyage/local) |
model | TEXT | 模型名称 |
provider_key | TEXT | 提供商的 API key 标识 |
hash | TEXT | 文本内容哈希 |
embedding | TEXT | 缓存的 embedding 向量(JSON) |
dims | INTEGER | 向量维度 |
updated_at | INTEGER | 更新时间戳 |
索引:idx_embedding_cache_updated_at ON embedding_cache(updated_at)
用途:避免对相同文本重复调用 embedding API,以(provider, model, provider_key, hash)四元组作为复合主键去重。
2.5chunks_vec– 向量虚拟表(sqlite-vec)
源码:src/memory/manager-sync-ops.ts:234-237
CREATEVIRTUALTABLEIFNOTEXISTSchunks_vecUSINGvec0(idTEXTPRIMARYKEY,embeddingFLOAT[dimensions]-- dimensions 随 embedding 模型变化);| 列 | 类型 | 说明 |
|---|---|---|
id | TEXT PK | 关联到chunks.id |
embedding | FLOAT[N] | 浮点向量,维度由模型决定 |
启用条件:agents.*.memorySearch.store.vector.enabled = true
用途:利用sqlite-vec扩展提供高效的余弦相似度检索。查询时使用vec_distance_cosine()函数计算距离。
降级策略:当sqlite-vec扩展不可用时,回退到内存中逐一计算余弦相似度(src/memory/manager-search.ts)。
2.6chunks_fts– 全文检索虚拟表(FTS5)
源码:src/memory/memory-schema.ts:59-67
CREATEVIRTUALTABLEIFNOTEXISTSchunks_ftsUSINGfts5(text,id UNINDEXED,path UNINDEXED,source UNINDEXED,model UNINDEXED,start_line UNINDEXED,end_line UNINDEXED);| 列 | 是否索引 | 说明 |
|---|---|---|
text | 是 | 块的原文内容(参与全文索引) |
id | 否 | 块 ID(仅存储,不参与检索) |
path | 否 | 文件路径 |
source | 否 | 来源标识 |
model | 否 | embedding 模型 |
start_line | 否 | 起始行 |
end_line | 否 | 结束行 |
启用条件:agents.*.memorySearch.query.hybrid.enabled = true
用途:使用 SQLite FTS5 的 BM25 排名算法进行关键词全文检索。只有text列参与倒排索引,其余列通过UNINDEXED标记为仅存储。
表关系图
┌──────────┐ ┌──────────────┐ ┌─────────────┐ │ meta │ │ files │ │embedding_cache│ │ (配置) │ │ (文件追踪) │ │ (缓存) │ └──────────┘ └──────┬───────┘ └─────────────┘ │ path ▼ ┌──────────────┐ │ chunks │ │ (核心文本块) │ └──┬───────┬───┘ │ id │ id ▼ ▼ ┌────────────┐ ┌────────────┐ │ chunks_vec │ │ chunks_fts │ │ (向量索引) │ │ (全文索引) │ └────────────┘ └────────────┘三、混合检索机制
3.1 三种检索模式
| 模式 | 触发条件 | 算法 | 源码位置 |
|---|---|---|---|
| 纯向量检索 | FTS 未启用 | vec_distance_cosine余弦相似度 | src/memory/manager-search.ts:20-94 |
| 纯关键词检索 | 无 embedding provider | FTS5 BM25 排名 | src/memory/manager-search.ts:136-191 |
| 混合检索 | 两者都启用(默认) | 加权融合 | src/memory/manager.ts:259-367 |
3.2 混合检索流程
源码:src/memory/manager.ts:259-367、src/memory/hybrid.ts
用户查询 │ ├──────────────────────┐ │ │ ▼ ▼ 向量检索 关键词检索 (余弦相似度) (FTS5 BM25) │ │ │ top K×4 候选 │ top K×4 候选 │ │ └──────────┬───────────┘ │ ▼ 结果融合 (加权合并) │ ▼ 可选:时间衰减 │ ▼ 可选:MMR 多样性重排 │ ▼ 阈值过滤 + Top-K 截断 │ ▼ 返回最终结果3.3 向量检索
源码:src/memory/manager-search.ts:20-94
- 将用户查询通过 embedding 模型转为向量
- 使用
sqlite-vec的vec_distance_cosine()计算余弦距离 - 按距离升序排列,取 top-K 候选
-- sqlite-vec 向量检索 SQL(简化)SELECTid,vec_distance_cosine(embedding,?)ASdistanceFROMchunks_vecORDERBYdistanceASCLIMIT?降级:无sqlite-vec时,从chunks表加载所有 embedding 到内存,逐一计算余弦相似度。
3.4 关键词检索
源码:src/memory/manager-search.ts:136-191
- 对用户查询进行分词(Unicode 感知的正则分词)
- 构建 FTS5 查询表达式
- 使用 BM25 排名
查询构建(src/memory/hybrid.ts:33-55):
原始查询: "如何配置 SQLite 向量检索" ↓ Unicode 分词: /[\p{L}\p{N}_]+/gu 分词结果: ["如何", "配置", "SQLite", "向量", "检索"] ↓ 引号包裹 + AND 连接 FTS5 查询: "如何" AND "配置" AND "SQLite" AND "向量" AND "检索"3.5 BM25 分数转换
源码:src/memory/hybrid.ts:33-55
FTS5 的 BM25 返回负数(越小越相关),需要转换到[0, 1]区间:
relevance = -rank // 取反,变为正数 score = relevance / (1 + relevance) // 映射到 [0, 1)示例:
rank = -5.0→relevance = 5.0→score = 5/6 = 0.833rank = -0.5→relevance = 0.5→score = 0.5/1.5 = 0.333rank = -0.1→relevance = 0.1→score = 0.1/1.1 = 0.091
3.6 结果融合算法
源码:src/memory/hybrid.ts:57-155
加权融合公式:
finalScore = vectorWeight × vectorScore + textWeight × textScore默认权重(src/agents/memory-search.ts):
vectorWeight = 0.7(70% 语义相关性)textWeight = 0.3(30% 词汇匹配度)- 自动归一化:
vectorWeight / (vectorWeight + textWeight)
融合过程:
- 构建联合集:按 chunk ID 合并两路结果
- 计算混合分数:
- 仅在向量结果中出现:
score = vectorWeight × vectorScore - 仅在关键词结果中出现:
score = textWeight × textScore - 两路都命中:
score = vectorWeight × vectorScore + textWeight × textScore
- 仅在向量结果中出现:
- 按混合分数降序排列
3.7 可选后处理
时间衰减(Temporal Decay)
源码:src/memory/temporal-decay.ts
对时效性敏感的记忆应用指数衰减:
decayedScore = score × exp(-λ × ageInDays) 其中 λ = ln(2) / halfLifeDays- 默认半衰期 30 天:30 天前的记忆分数衰减为原来的 50%
- 日期来源:从记忆文件路径中解析(
memory/YYYY-MM-DD.md) - “常青” 文件(如
MEMORY.md或无日期文件)跳过衰减
配置:
temporalDecay: { enabled: false // 默认关闭 halfLifeDays: 30 // 半衰期天数 }MMR 多样性重排(Maximal Marginal Relevance)
源码:src/memory/mmr.ts
在保持相关性的同时增加结果多样性:
MMR(d) = λ × relevance(d) - (1-λ) × max_sim(d, selected) 其中 sim() 使用基于 token 的 Jaccard 相似度λ = 0.7:偏向相关性(λ → 1退化为纯相关性排序)- 贪心选择:每轮选 MMR 得分最高的文档加入结果集
配置:
mmr: { enabled: false // 默认关闭 lambda: 0.7 // 相关性 vs 多样性权衡 }四、完整配置参考
源码:src/agents/memory-search.ts:15-89
agents.defaults.memorySearch: { enabled: true // 是否启用记忆检索 provider: "auto" // embedding 提供商 // "openai"|"gemini"|"voyage"|"local"|"auto" model: "text-embedding-3-small" // embedding 模型(示例) store: { driver: "sqlite" path: "~/.openclaw/state/memory/{agentId}.sqlite" vector: { enabled: true // 启用 sqlite-vec extensionPath?: string // 自定义扩展路径 } } chunking: { tokens: 400 // 每块 token 数 overlap: 80 // 块间重叠 token 数 } query: { maxResults: 6 // 最终返回结果数 minScore: 0.35 // 最低分数阈值 [0, 1] hybrid: { enabled: true // 启用混合检索 vectorWeight: 0.7 // 向量检索权重 textWeight: 0.3 // 关键词检索权重 candidateMultiplier: 4 // 候选倍数(取 maxResults × 4 个候选) mmr: { enabled: false // MMR 多样性重排 lambda: 0.7 // 相关性 vs 多样性 [0, 1] } temporalDecay: { enabled: false // 时间衰减 halfLifeDays: 30 // 半衰期天数 } } } }五、核心源码文件索引
| 文件 | 功能 |
|---|---|
src/memory/memory-schema.ts | 表创建和 schema 初始化 |
src/memory/sqlite.ts | Node.js 内置 SQLite 模块加载 |
src/memory/sqlite-vec.ts | sqlite-vec 扩展加载 |
src/memory/manager.ts | 检索编排(第 259-367 行) |
src/memory/manager-search.ts | 向量和关键词检索函数 |
src/memory/manager-sync-ops.ts | 数据库初始化和向量表管理 |
src/memory/hybrid.ts | 混合结果融合和评分 |
src/memory/hybrid.test.ts | 混合检索算法测试 |
src/memory/mmr.ts | MMR 多样性重排 |
src/memory/temporal-decay.ts | 时间衰减评分 |
src/memory/query-expansion.ts | 会话式查询的关键词提取 |
src/agents/memory-search.ts | 配置 schema 和默认值 |