Implementation Deep Dive

遗忘机制与倒排索引实现详解

深入 OpenClaw 记忆系统的两大核心:指数时间衰减遗忘机制 + BM25 倒排索引实现,包含完整算法、代码和可视化。

Part 1

遗忘机制实现 (Forgetting Mechanism)

OpenClaw 采用指数时间衰减模型,模拟人类记忆的艾宾浩斯遗忘曲线,让重要的记忆更持久,噪声自然消退。

核心衰减公式
decayedScore = originalScore × e(-λ × ageInDays)
其中 λ = ln(2) / halfLifeDays,默认半衰期为 30 天
即:每过 30 天,记忆的权重减半
⚡ 交互式衰减计算器
原始分数 (0-1):
记忆年龄 (天):
半衰期 (天):
📉 指数衰减曲线可视化
💻 TypeScript 实现代码
// 来源: OpenClaw 源码 - memory index manager function applyTemporalDecay( score: number, createdAt: number, now: number, halfLifeDays: number = 30 ): number { // 计算记忆年龄(天数) const ageInDays = (now - createdAt) / (1000 * 60 * 60 * 24); // 计算衰减常数 λ = ln(2) / halfLifeDays const lambda = Math.log(2) / halfLifeDays; // 应用指数衰减: e^(-λ × age) const decayFactor = Math.exp(-lambda * ageInDays); return score * decayFactor; } // 完整搜索评分(混合 + 衰减) function computeFinalScore(entry: MemoryEntry): number { // 1. 向量相似度分数 (0-1) let vectorScore = entry.vectorSimilarity; // 2. BM25 关键词分数 (转换为 0-1) let bm25Score = 1 / (1 + entry.bm25Rank); // 3. 混合加权 (默认 0.7 向量 + 0.3 BM25) let hybridScore = 0.7 * vectorScore + 0.3 * bm25Score; // 4. 应用时间衰减 let finalScore = applyTemporalDecay( hybridScore, entry.createdAt, Date.now(), 30 // 半衰期 30 天 ); return finalScore; }
记忆年龄
衰减系数
衰减后分数 (原始0.95)
场景示例
0 天(今天)
100%
0.950
当前会话信息
7 天(一周)
~84%
0.798
本周工作进度
30 天(一月)
50%
0.475
一个月前的决定
60 天(两月)
25%
0.238
两个月前的讨论
90 天(一季)
12.5%
0.119
季度旧信息
180 天(半年)
~1.6%
0.015
几乎被遗忘
🔄 遗忘机制在记忆中的完整应用流程
1

记忆写入时记录时间戳

每条记忆写入时,自动附加 createdAt (Unix 毫秒时间戳) 和 access_count (初始为 0)

2

检索时计算实时衰减

每次 memory_search 时,对候选记忆计算: decayedScore = score × e^(-λ × age)

3

按衰减后分数重排序

使用衰减后的分数重新排序候选记忆,较老的记忆自然排名下降

4

访问时更新访问权重

被访问的记忆 access_count +1,可配置 boost 因子提升重复访问的记忆

5

上下文压缩前自动刷新

会话接近 token 上限时,触发"预压缩 ping",提醒 Agent 将重要信息写入持久记忆

📡 上下文压缩前自动刷新机制
// 配置示例: 当会话接近 token 上限时自动触发记忆刷新 compaction: { "reserveTokensFloor": 20000, // 保留 token 下限 "memoryFlush": { "enabled": true, "softThresholdTokens": 4000, // 距压缩还剩 4000 token 时触发 "systemPrompt": "Session nearing compaction. Store durable memories now.", "prompt": "Write any lasting notes to memory/YYYY-MM-DD.md; reply with NO_REPLY if nothing to store." } } // 触发时机计算 (200K 上下文窗口): // triggerPoint = contextWindow - reserveTokensFloor - softThresholdTokens // = 200,000 - 20,000 - 4,000 = 176,000 tokens // 当会话使用 176,000+ tokens 时,自动触发记忆刷新
Part 2

倒排索引实现 (Inverted Index / BM25)

BM25 (Best Matching 25) 是信息检索领域的经典算法,OpenClaw 通过 SQLite FTS5 实现倒排索引,用于精确关键词匹配。

BM25 评分公式
score(D, Q) = Σi=1n IDF(qi) × [f(qi, D) × (k1 + 1)] / [f(qi, D) + k1 × (1 - b + b × |D|/avgdl)]
IDF: 逆文档频率(词越罕见,权重越高)
f(qi, D): 词 qi 在文档 D 中的出现次数
|D|: 文档 D 的长度(词数)
avgdl: 所有文档的平均长度
k₁: 词频饱和参数(默认 1.2)
b: 长度归一化参数(默认 0.75)
💻 SQLite FTS5 倒排索引实现
-- 1. 创建 FTS5 全文搜索虚拟表(倒排索引核心) CREATE VIRTUAL TABLE memory_fts USING fts5( content, -- 记忆文本内容(被分词并建立倒排索引) file_path UNINDEXED, -- 文件路径(不被索引) line_start UNINDEXED, -- 起始行号(不被索引) line_end UNINDEXED -- 结束行号(不被索引) ); -- 2. 插入记忆时自动建立倒排索引 INSERT INTO memory_fts(content, file_path, line_start, line_end) VALUES('用户偏好使用 TypeScript 和 Prettier', 'memory/user.md', 1, 5); -- 3. BM25 搜索(FTS5 内置 bm25() 函数) SELECT file_path, line_start, line_end, snippet(memory_fts, 0, '<b>', '</b>', '...', 20) as preview, bm25(memory_fts) as score -- 越低越好(负分数) FROM memory_fts WHERE memory_fts MATCH 'TypeScript OR Prettier' ORDER BY score ASC -- bm25 分数越低表示匹配度越高 LIMIT 10; -- 4. 精确短语搜索 SELECT * FROM memory_fts WHERE memory_fts MATCH '"TypeScript 偏好"'; -- 精确短语匹配 -- 5. 前缀搜索 SELECT * FROM memory_fts WHERE memory_fts MATCH 'Type*'; -- 匹配所有以 Type 开头的词
🔍 BM25 倒排索引检索流程
1

分词与标准化

将查询 "TypeScript 配置" 分词为 ["typescript", "配置"],统一小写,CJK 字符按字分词

2

查倒排索引

FTS5 内部查找: "typescript" → [doc1, doc3, doc7...], "配置" → [doc2, doc3, doc5...]

3

计算 IDF (逆文档频率)

IDF(qi) = ln((N - n(qi) + 0.5) / (n(qi) + 0.5) + 1),N=总文档数,n(qi)=包含该词的文档数

4

计算词频饱和度

f(qi, D) × (k₁ + 1) / (f(qi, D) + k₁ × (1 - b + b × |D|/avgdl)),避免高频词主导

5

合并所有查询词分数

对查询中所有词的分数求和,得到每个文档的最终 BM25 分数,按分数升序排列(FTS5 的 bm25() 返回负值,越小越好)

📚 倒排索引结构示例
// 假设有以下 3 条记忆: // Doc1: "用户偏好使用 TypeScript 和 Prettier" // Doc2: "项目使用 PostgreSQL 数据库" // Doc3: "TypeScript 配置参考 Prettier 规范" // FTS5 自动构建的倒排索引(简化表示): { "typescript": [1, 3], // 出现在 Doc1 和 Doc3 "配置": [3], // 只出现在 Doc3 "prettier": [1, 3], // 出现在 Doc1 和 Doc3 "用户": [1], // 只出现在 Doc1 "项目": [2], // 只出现在 Doc2 "postgresql": [2], // 只出现在 Doc2 "数据库": [2] // 只出现在 Doc2 } // 查询 "TypeScript Prettier" 时: // 1. 查倒排索引: "typescript" → [1, 3], "prettier" → [1, 3] // 2. 合并文档: {1, 3} // 3. 计算 BM25 分数: // Doc1: IDF(typescript) × TF_score + IDF(prettier) × TF_score // Doc3: IDF(typescript) × TF_score + IDF(prettier) × TF_score // 4. 按分数排序返回
🔀 混合搜索:BM25 + 向量融合
1

BM25 关键词检索

精确匹配: "Omada" "router" 等关键词,通过 FTS5 倒排索引快速定位

2

向量语义检索

语义匹配: "router" ≈ "路由器" ≈ "gateway",通过 sqlite-vec 余弦相似度计算

3

分数归一化

BM25 分数转换为 0-1 范围: normalizedScore = 1 / (1 + bm25Rank)

4

加权融合

finalScore = 0.7 × vectorScore + 0.3 × bm25Score(权重可配置)

5

MMR 去重(可选)

MMR Score = λ × Relevance - (1-λ) × MaxSimilarity,提升结果多样性

💻 完整的混合搜索实现代码
// OpenClaw 混合搜索完整实现(简化版) async function hybridSearch( query: string, options: { vectorWeight?: number, // 默认 0.7 textWeight?: number, // 默认 0.3 candidateMultiplier?: number, // 默认 4 enableMMR?: boolean, mmrLambda?: number, enableTemporalDecay?: boolean, halfLifeDays?: number } ): Promise<SearchResult[]> { // 1. 向量搜索(获取 Top-K × candidateMultiplier 候选) const vectorResults = await vectorSearch(query, { topK: 10 * (options.candidateMultiplier || 4) }); // 2. BM25 搜索(同样获取较多候选) const bm25Results = await bm25Search(query, { topK: 10 * (options.candidateMultiplier || 4) }); // 3. 合并候选池(取并集) const candidatePool = mergeCandidates(vectorResults, bm25Results); // 4. 计算混合分数 for (const candidate of candidatePool) { const vectorScore = candidate.vectorSimilarity || 0; const bm25Score = candidate.bm25Rank ? 1 / (1 + candidate.bm25Rank) // 转换为 0-1 : 0; candidate.hybridScore = (options.vectorWeight || 0.7) * vectorScore + (options.textWeight || 0.3) * bm25Score; } // 5. MMR 去重(提升多样性) let finalResults; if (options.enableMMR) { finalResults = applyMMR(candidatePool, options.mmrLambda || 0.7); } else { finalResults = candidatePool .sort((a, b) => b.hybridScore - a.hybridScore); } // 6. 应用时间衰减 if (options.enableTemporalDecay) { for (const result of finalResults) { result.finalScore = applyTemporalDecay( result.hybridScore, result.createdAt, Date.now(), options.halfLifeDays || 30 ); } finalResults.sort((a, b) => b.finalScore - a.finalScore); } // 7. 返回 Top-K return finalResults.slice(0, 10); }
Part 3

完整对比:OpenClaw vs 传统 RAG

OpenClaw 的记忆系统与传统 RAG 在索引结构、检索策略和遗忘机制上有本质区别。

🏗️ 索引结构

  • OpenClaw: SQLite + FTS5 + sqlite-vec
  • 传统 RAG: Pinecone / Weaviate / Chroma
  • 区别: OpenClaw 用文件即真相,人类可读
  • 更新: OpenClaw 增量同步,RAG 全量重建

🔍 检索策略

  • OpenClaw: BM25 + 向量 + MMR + 衰减
  • 传统 RAG: 仅向量相似度
  • 优势: 混合搜索平衡精确度和召回率
  • 多样性: MMR 避免结果过于相似

⏰ 遗忘机制

  • OpenClaw: 指数时间衰减 + 访问权重
  • 传统 RAG: 无内置遗忘机制
  • 自动刷新: 压缩前自动写入持久记忆
  • 人类可读: Markdown 文件可直接编辑
💡 核心要点总结
遗忘机制
  • 指数衰减: e^(-λt),半衰期默认 30 天
  • 访问权重: 重复访问的记忆获得 boost
  • 自动刷新: 压缩前触发记忆持久化
  • 实现位置: 检索时实时计算,不删除旧记忆
倒排索引
  • FTS5 虚拟表: SQLite 内置全文搜索
  • BM25 算法: 词频饱和度 + 长度归一化
  • 混合搜索: 0.7×向量 + 0.3×BM25
  • MMR 去重: 提升结果多样性
OpenClaw Memory System · Forgetting & Inverted Index Implementation · 2026