OpenClaw 记忆系统原理讲解
让我用大学老师的方式,带你深入理解 OpenClaw 记忆系统的完整实现逻辑!🎓
📚 OpenClaw 记忆系统原理讲解
一、整体架构(宏观视角)
┌─────────────────────────────────────────────────────────────┐
│ 用户提问(关于历史/决策/上下文) │
└─────────────────────┬───────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ memory_search 工具(语义搜索入口) │
│ - 输入:query, maxResults, minScore │
│ - 输出:带评分的 snippets 列表 │
└─────────────────────┬───────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ MemoryManager(记忆管理器) │
│ ┌─────────────────────┐ ┌─────────────────────────────┐ │
│ │ 向量搜索 (Vector) │ │ 全文搜索 (FTS) │ │
│ │ LanceDB + Embedding│ │ SQLite FTS5 │ │
│ └──────────┬──────────┘ └──────────────┬──────────────┘ │
│ └──────────────┬───────────────┘ │
│ │ │
│ ┌───────▼────────┐ │
│ │ Hybrid 混合排序 │ │
│ └───────┬────────┘ │
└────────────────────────────┼─────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ memory_get 工具(精确读取) │
│ - 输入:path, from, lines │
│ - 输出:指定文件的指定行内容 │
└─────────────────────────────────────────────────────────────┘
二、核心算法详解
2.1 写入流程(Write Path)
当你调用 memory_search 或 memory_get 时,底层发生了什么?
// 伪代码:记忆搜索的核心逻辑
async search(query: string, opts: { maxResults, minScore }) {
// 步骤 1: 查询预处理
const cleaned = query.trim();
if (!cleaned) return [];
// 步骤 2: 混合搜索配置
const hybrid = this.settings.query.hybrid;
const candidates = Math.min(200, Math.max(1,
Math.floor(maxResults * hybrid.candidateMultiplier)
));
// 步骤 3: 向量搜索(语义相似度)
const queryVec = await this.embedQueryWithTimeout(cleaned);
const vectorResults = await this.searchVector(queryVec, candidates);
// 步骤 4: 全文搜索(关键词匹配)
const keywordResults = hybrid.enabled ?
await this.searchKeyword(cleaned, candidates) : [];
// 步骤 5: 混合排序(Hybrid Ranking)
const merged = this.hybridRank(vectorResults, keywordResults);
// 步骤 6: 过滤和截断
return merged
.filter(entry => entry.score >= minScore)
.slice(0, maxResults);
}
关键公式:
最终得分 = α × 向量相似度 + (1-α) × 文本匹配度
2.2 向量搜索(Vector Search)
async searchVector(queryVec: number[], limit: number) {
// 使用 LanceDB 进行近似最近邻搜索 (ANN)
return (await this.table
.vectorSearch(queryVec) // 向量相似度搜索
.limit(limit) // 限制返回数量
.toArray() // 转换为数组
).map(row => {
// 距离转相似度分数
const score = 1 / (1 + (row._distance ?? 0));
return {
entry: {
id: row.id,
text: row.text,
importance: row.importance,
category: row.category
},
score // 相似度分数 [0, 1]
};
});
}
数学原理:
余弦相似度:cos(θ) = (A·B) / (||A|| × ||B||)
欧氏距离转相似度:score = 1 / (1 + distance)
2.3 全文搜索(Full-Text Search)
async searchKeyword(term: string, candidates: number) {
// 使用 SQLite FTS5 进行全文检索
const keywords = extractKeywords(term);
const searchTerms = keywords.length > 0 ? keywords : [term];
// 多关键词搜索
const resultSets = await Promise.all(
searchTerms.map(t => this.fts.search(t, candidates))
);
// 合并结果(取最高分)
const seenIds = new Map();
for (const results of resultSets) {
for (const result of results) {
const existing = seenIds.get(result.id);
if (!existing || result.score > existing.score) {
seenIds.set(result.id, result);
}
}
}
return [...seenIds.values()]
.sort((a, b) => b.score - a.score);
}
2.4 混合排序(Hybrid Ranking)
这是最核心的算法!
hybridRank(vectorResults, keywordResults) {
const seen = new Map();
// 1. 合并两个结果集
for (const r of [...vectorResults, ...keywordResults]) {
const existing = seen.get(r.id);
if (!existing) {
seen.set(r.id, r);
} else {
// 2. 分数融合(Reciprocal Rank Fusion 变体)
const vectorWeight = 0.7; // 向量搜索权重
const keywordWeight = 0.3; // 关键词权重
r.score = existing.score * vectorWeight + r.score * keywordWeight;
}
}
// 3. 按综合分数排序
return [...seen.values()]
.sort((a, b) => b.score - a.score);
}
三、文件读取逻辑(memory_get)
async readFile(relPath: string, from?: number, lines?: number) {
// 1. 解析文件路径
const fullPath = path.join(this.workspacePath, relPath);
// 2. 安全检查(防止路径遍历攻击)
if (!fullPath.startsWith(this.workspacePath)) {
throw new Error("Invalid path");
}
// 3. 读取文件
const content = await fs.readFile(fullPath, 'utf-8');
const allLines = content.split('\n');
// 4. 按需截取(节省 Token)
const start = from ? from - 1 : 0; // 1-indexed 转 0-indexed
const end = lines ? start + lines : allLines.length;
const snippet = allLines.slice(start, end).join('\n');
return {
path: relPath,
text: snippet,
totalLines: allLines.length,
returnedLines: end - start
};
}
四、实际调用示例
场景:用户问"我记得上周讨论过 Token 认证的事情"
┌─────────────────────────────────────────────────────────┐
│ Step 1: 用户提问 │
│ "我记得上周讨论过 Token 认证的事情" │
└────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Step 2: 调用 memory_search │
│ query: "Token 认证 讨论" │
│ maxResults: 5 │
│ minScore: 0.5 │
└────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Step 3: 向量搜索(找到语义相似的内容) │
│ - "智图 Token 认证规范" (score: 0.89) │
│ - "SecurityUtils 获取 Token" (score: 0.82) │
└────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Step 4: 返回结果给 Agent │
│ [ │
│ {path: "memory/zhitu-token-auth.md", score: 0.89}, │
│ {path: "memory/security-utils-guide.md", score: 0.82}│
│ ] │
└────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Step 5: Agent 调用 memory_get 读取详细内容 │
│ path: "memory/zhitu-token-auth.md" │
│ from: 1, lines: 50 │
└────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Step 6: 生成最终回答 │
│ "根据记忆,Token 认证的获取接口是..." │
└─────────────────────────────────────────────────────────┘
五、性能优化技巧
5.1 候选集放大(Candidate Multiplier)
// 如果用户要 5 条结果,实际搜索 50 条候选
const candidates = maxResults * 10; // 放大 10 倍
// 然后在内存中做精细排序,取前 5 条
5.2 分数截断(Score Cutoff)
// 低于阈值的直接过滤,节省后续处理
results.filter(r => r.score >= minScore)
5.3 增量读取(Chunked Reading)
// 不是一次性读取整个 MEMORY.md
// 而是按需读取指定行
memory_get(path, from=100, lines=50)
六、关键设计决策
| 设计点 | 选择 | 原因 |
|---|---|---|
| 存储格式 | Markdown | 人类可读 + 机器可解析 |
| 搜索方式 | 向量 + 全文混合 | 兼顾语义和精确匹配 |
| 分数融合 | 加权平均 | 简单有效,可调节 |
| 读取策略 | 按需截取 | 节省 Token,提高效率 |
七、作业(思考题)📝
- 如果
MEMORY.md文件超过 10 万行,如何优化搜索性能? - 为什么向量搜索和全文搜索要做混合,而不是只用一种?
minScore参数设置过高或过低会有什么影响?