OpenClaw Memory 模块完整分析
2026年2月25日 11:24
OpenClaw Memory 模块完整分析
一、项目背景
OpenClaw 是一个本地优先的个人 AI 助手,支持多种消息通道(WhatsApp、Telegram、Slack 等)。Memory 模块为 AI Agent 提供语义记忆搜索能力——Agent 可以在 Markdown 记忆文件和历史会话中进行向量 + 关键词的混合检索。
二、整体架构
┌─────────────────────────────────────────────────┐
│ 入口层 (index.ts) │
│ getMemorySearchManager() → 选择后端策略 │
├────────────┬────────────────────────┬───────────┤
│ QMD 后端 │ FallbackManager │ Builtin 后端│
│ (外部CLI) │ (主备自动切换) │ (核心实现) │
├─────────────┴───────────────────────┴───────────┤
│ MemoryIndexManager │
│ ┌──────────┬──────────┬──────────┐ │
│ │ SyncOps │Embedding │ Search │ │
│ │(文件监听) │ Ops(索引) │(混合搜索) │ │
│ └──────────┴──────────┴──────────┘ │
├─────────────────────────────────────────────────┤
│ 存储层: SQLite + FTS5 + sqlite-vec │
├─────────────────────────────────────────────────┤
│ Embedding Providers: OpenAI|Gemini|Voyage| │
│ Mistral|Local(node-llama-cpp) │
└──────────────────────────────────────────────────┘
三、核心设计详解
1. 统一接口 (MemorySearchManager)
export type MemorySource = "memory" | "sessions";
export type MemorySearchResult = {
path: string;
startLine: number;
endLine: number;
score: number;
snippet: string;
source: MemorySource;
citation?: string;
};
// ...
export interface MemorySearchManager {
search(query, opts?): Promise<MemorySearchResult[]>;
readFile(params): Promise<{ text: string; path: string }>;
status(): MemoryProviderStatus;
sync?(params?): Promise<void>;
probeEmbeddingAvailability(): Promise<MemoryEmbeddingProbeResult>;
probeVectorAvailability(): Promise<boolean>;
close?(): Promise<void>;
}
这是整个模块的核心抽象——无论底层用什么后端(builtin SQLite 还是外部 QMD CLI),对上层暴露统一接口。
2. 后端策略选择 (search-manager.ts)
export async function getMemorySearchManager(params: {
cfg: OpenClawConfig;
agentId: string;
purpose?: "default" | "status";
}): Promise<MemorySearchManagerResult> {
const resolved = resolveMemoryBackendConfig(params);
if (resolved.backend === "qmd" && resolved.qmd) {
// ... 尝试 QMD 后端,失败则 fallback 到 builtin
const wrapper = new FallbackMemoryManager({
primary,
fallbackFactory: async () => {
const { MemoryIndexManager } = await import("./manager.js");
return await MemoryIndexManager.get(params);
},
}, () => QMD_MANAGER_CACHE.delete(cacheKey));
// ...
}
// 默认使用 builtin
const manager = await MemoryIndexManager.get(params);
return { manager };
}
设计亮点:
-
策略模式 + 懒加载:通过
dynamic import延迟加载后端实现 - FallbackMemoryManager:代理模式,主后端失败自动切换到备用后端,对上层透明
- 缓存驱逐:失败时自动从缓存中移除,下次请求可以重试
3. 混合搜索引擎 (hybrid.ts)
export async function mergeHybridResults(params: {
vector: HybridVectorResult[];
keyword: HybridKeywordResult[];
vectorWeight: number;
textWeight: number;
mmr?: Partial<MMRConfig>;
temporalDecay?: Partial<TemporalDecayConfig>;
}): Promise<Array<{ path; startLine; endLine; score; snippet; source }>> {
// 1. 按 ID 合并向量和关键词结果
// 2. 加权融合分数: score = vectorWeight * vectorScore + textWeight * textScore
// 3. 时间衰减: 旧记忆分数降低
// 4. MMR 重排序: 增加结果多样性
}
这是搜索的核心——三层融合管线:
| 阶段 | 算法 | 作用 |
|---|---|---|
| 加权融合 | score = w_v × vectorScore + w_t × textScore |
平衡语义相似度和关键词匹配 |
| 时间衰减 | 指数衰减 e^(-λ × age)
|
让近期记忆权重更高 |
| MMR 重排序 | λ × relevance - (1-λ) × max_similarity |
增加结果多样性,避免重复 |
4. 时间衰减机制 (temporal-decay.ts)
export function toDecayLambda(halfLifeDays: number): number {
return Math.LN2 / halfLifeDays;
}
export function calculateTemporalDecayMultiplier(params: {
ageInDays: number;
halfLifeDays: number;
}): number {
const lambda = toDecayLambda(params.halfLifeDays);
return Math.exp(-lambda * clampedAge);
}
核心设计:
-
日期命名文件
memory/2024-01-15.md自动从文件名提取时间 -
常青文件
MEMORY.md和非日期命名的memory/*.md不衰减(核心知识) - fallback 到 mtime:无法从文件名解析日期时,使用文件修改时间
5. MMR 多样性重排序 (mmr.ts)
export function computeMMRScore(relevance: number, maxSimilarity: number, lambda: number): number {
return lambda * relevance - (1 - lambda) * maxSimilarity;
}
使用 Jaccard 相似度(基于 token 集合的交集/并集)来衡量结果间的相似程度,避免返回大量重复内容。比起用向量余弦相似度做 MMR,Jaccard 更轻量且无需额外嵌入计算。
6. Markdown 分块策略 (internal.ts)
export function chunkMarkdown(
content: string,
chunking: { tokens: number; overlap: number },
): MemoryChunk[] {
const maxChars = Math.max(32, chunking.tokens * 4);
const overlapChars = Math.max(0, chunking.overlap * 4);
// 按行扫描,达到 maxChars 时 flush
// flush 后保留尾部 overlapChars 作为重叠区
}
设计要点:
-
token 估算:
tokens × 4转为字符数(粗略但高效) - 滑动窗口重叠:chunk 之间有 overlap,避免语义在边界处被截断
- 超长行切割:单行超过 maxChars 时自动分段
- 每个 chunk 记录
startLine/endLine,支持精确引用
7. Embedding Provider 工厂 (embeddings.ts)
export async function createEmbeddingProvider(
options: EmbeddingProviderOptions,
): Promise<EmbeddingProviderResult> {
// auto 模式: local → openai → gemini → voyage → mistral
// 指定模式: primary → fallback
// 所有 API key 缺失: 返回 null provider (FTS-only mode)
}
三层降级策略:
- auto 模式:依次尝试 local → openai → gemini → voyage → mistral
- 指定 + fallback:用户指定的 provider 失败时切换到 fallback
- 全部失败 → FTS-only:纯关键词搜索,仍可用但质量降低
8. 数据库 Schema (memory-schema.ts)
// meta: 索引元信息 (model/provider/版本)
// files: 文件记录 (path, hash, mtime, source)
// chunks: 文本块 (id, path, text, embedding, model)
// embedding_cache: 嵌入缓存 (provider+model+hash → embedding)
// chunks_fts: FTS5 全文搜索虚拟表
// chunks_vec: sqlite-vec 向量搜索虚拟表
9. 同步机制 (manager-sync-ops.ts)
同步有多种触发方式:
| 触发方式 | 场景 |
|---|---|
watch |
chokidar 文件监听,debounce 后触发 |
session-start |
新会话开始时预热 |
session-delta |
会话文件增长超过阈值(字节/消息数) |
search |
搜索时如果 dirty 则先同步 |
interval |
定时同步(可配置分钟数) |
重建索引采用安全替换策略:先写入临时 DB,完成后原子交换,失败则回滚。
10. 实例缓存 + 单例
const INDEX_CACHE = new Map<string, MemoryIndexManager>();
static async get(params): Promise<MemoryIndexManager | null> {
const key = `${agentId}:${workspaceDir}:${JSON.stringify(settings)}`;
const existing = INDEX_CACHE.get(key);
if (existing) return existing;
// ... 创建新实例
INDEX_CACHE.set(key, manager);
return manager;
}
用 agentId + workspaceDir + settings 作为缓存 key,保证同一配置只有一个 Manager 实例,避免重复打开数据库和文件监听器。
四、参考价值总结
如果你想在自己的项目中实现类似的记忆/知识检索系统,这个模块有以下核心参考价值:
| 维度 | 设计模式 | 参考价值 |
|---|---|---|
| 接口抽象 |
MemorySearchManager 接口 |
将搜索、同步、状态查询统一抽象,后端可替换 |
| 混合搜索 | Vector + BM25 加权融合 | 兼顾语义理解和精确匹配,比单纯向量搜索更鲁棒 |
| 结果优化 | MMR + 时间衰减 | 解决结果重复和旧信息权重过高的问题 |
| 降级策略 | Provider 三层 fallback + FTS-only | 无 API key 也能用,极大提升了可用性 |
| 主备切换 | FallbackMemoryManager 代理模式 | QMD 后端失败自动切到 builtin,对上层透明 |
| 增量同步 | hash 对比 + 文件监听 + delta 阈值 | 只重新索引变化的文件,会话增量基于字节/消息数阈值 |
| 安全重建 | 临时 DB → 原子交换 → 失败回滚 | 全量重建索引时不影响在线查询 |
| 嵌入缓存 | SQLite embedding_cache 表 | 避免重复调用 API,重建索引时可复用历史嵌入 |
| 分块策略 | 行级滑动窗口 + overlap | 保留行号信息,支持精确引用,overlap 防止语义断裂 |
| 实例管理 | 缓存 Map + 复合 key | 避免重复实例,正确处理 close 和缓存驱逐 |
这套架构特别适合以下场景复用:
- RAG 系统——需要在本地文档上做语义搜索
- 知识库检索——混合搜索 + 时间衰减适合持续更新的知识
- Agent 工具——作为 AI Agent 的长期记忆组件
- 离线优先应用——SQLite 本地存储 + 可选远程 Embedding 的架构