Part 1
遗忘机制实现 (Forgetting Mechanism)
OpenClaw 采用指数时间衰减模型,模拟人类记忆的艾宾浩斯遗忘曲线,让重要的记忆更持久,噪声自然消退。
💻 TypeScript 实现代码
function applyTemporalDecay(
score: number,
createdAt: number,
now: number,
halfLifeDays: number = 30
): number {
const ageInDays = (now - createdAt) / (1000 * 60 * 60 * 24);
const lambda = Math.log(2) / halfLifeDays;
const decayFactor = Math.exp(-lambda * ageInDays);
return score * decayFactor;
}
function computeFinalScore(entry: MemoryEntry): number {
let vectorScore = entry.vectorSimilarity;
let bm25Score = 1 / (1 + entry.bm25Rank);
let hybridScore = 0.7 * vectorScore + 0.3 * bm25Score;
let finalScore = applyTemporalDecay(
hybridScore,
entry.createdAt,
Date.now(),
30
);
return finalScore;
}
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 将重要信息写入持久记忆
📡 上下文压缩前自动刷新机制
compaction: {
"reserveTokensFloor": 20000,
"memoryFlush": {
"enabled": true,
"softThresholdTokens": 4000,
"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."
}
}
Part 2
倒排索引实现 (Inverted Index / BM25)
BM25 (Best Matching 25) 是信息检索领域的经典算法,OpenClaw 通过 SQLite FTS5 实现倒排索引,用于精确关键词匹配。
💻 SQLite FTS5 倒排索引实现
CREATE VIRTUAL TABLE memory_fts USING fts5(
content,
file_path UNINDEXED,
line_start UNINDEXED,
line_end UNINDEXED
);
INSERT INTO memory_fts(content, file_path, line_start, line_end)
VALUES('用户偏好使用 TypeScript 和 Prettier', 'memory/user.md', 1, 5);
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
LIMIT 10;
SELECT * FROM memory_fts
WHERE memory_fts MATCH '"TypeScript 偏好"';
SELECT * FROM memory_fts
WHERE memory_fts MATCH '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() 返回负值,越小越好)
📚 倒排索引结构示例
{
"typescript": [1, 3],
"配置": [3],
"prettier": [1, 3],
"用户": [1],
"项目": [2],
"postgresql": [2],
"数据库": [2]
}
🔀 混合搜索: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,提升结果多样性
💻 完整的混合搜索实现代码
async function hybridSearch(
query: string,
options: {
vectorWeight?: number,
textWeight?: number,
candidateMultiplier?: number,
enableMMR?: boolean,
mmrLambda?: number,
enableTemporalDecay?: boolean,
halfLifeDays?: number
}
): Promise<SearchResult[]> {
const vectorResults = await vectorSearch(query, {
topK: 10 * (options.candidateMultiplier || 4)
});
const bm25Results = await bm25Search(query, {
topK: 10 * (options.candidateMultiplier || 4)
});
const candidatePool = mergeCandidates(vectorResults, bm25Results);
for (const candidate of candidatePool) {
const vectorScore = candidate.vectorSimilarity || 0;
const bm25Score = candidate.bm25Rank
? 1 / (1 + candidate.bm25Rank)
: 0;
candidate.hybridScore =
(options.vectorWeight || 0.7) * vectorScore +
(options.textWeight || 0.3) * bm25Score;
}
let finalResults;
if (options.enableMMR) {
finalResults = applyMMR(candidatePool, options.mmrLambda || 0.7);
} else {
finalResults = candidatePool
.sort((a, b) => b.hybridScore - a.hybridScore);
}
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);
}
return finalResults.slice(0, 10);
}