OpenClaw Memory 模块完整分析

OpenClaw Memory 模块完整分析

一、项目背景

OpenClaw 是一个本地优先的个人 AI 助手,支持多种消息通道(WhatsApp、Telegram、Slack 等)。Memory 模块为 AI Agent 提供语义记忆搜索能力------Agent 可以在 Markdown 记忆文件和历史会话中进行向量 + 关键词的混合检索。

二、整体架构

scss 复制代码
┌─────────────────────────────────────────────────┐
│                  入口层 (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)

1:80:openclaw/src/memory/types.ts 复制代码
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)

19:73:openclaw/src/memory/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)

51:149:openclaw/src/memory/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)

17:34:openclaw/src/memory/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)

100:103:openclaw/src/memory/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)

184:265:openclaw/src/memory/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)

144:260:openclaw/src/memory/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)
}

三层降级策略:

  1. auto 模式:依次尝试 local → openai → gemini → voyage → mistral
  2. 指定 + fallback:用户指定的 provider 失败时切换到 fallback
  3. 全部失败 → FTS-only:纯关键词搜索,仍可用但质量降低

8. 数据库 Schema (memory-schema.ts)

9:83:openclaw/src/memory/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. 实例缓存 + 单例

41:42:openclaw/src/memory/manager.ts 复制代码
const INDEX_CACHE = new Map<string, MemoryIndexManager>();
103:139:openclaw/src/memory/manager.ts 复制代码
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 和缓存驱逐

这套架构特别适合以下场景复用:

  1. RAG 系统------需要在本地文档上做语义搜索
  2. 知识库检索------混合搜索 + 时间衰减适合持续更新的知识
  3. Agent 工具------作为 AI Agent 的长期记忆组件
  4. 离线优先应用------SQLite 本地存储 + 可选远程 Embedding 的架构
相关推荐
ID_180079054732 小时前
淘宝商品详情 API 接口 item_get: 高效获取商品数据的技术方案
java·前端·数据库
We་ct2 小时前
LeetCode 637. 二叉树的层平均值:BFS层序遍历实战解析
前端·数据结构·算法·leetcode·typescript·宽度优先
敲敲了个代码2 小时前
浏览器时间管理大师:深度拆解 5 大核心调度 API
前端·javascript·学习·web
树獭叔叔2 小时前
02-大模型位置编码详解:大模型如何理解顺序?
后端·aigc·openai
ssshooter2 小时前
看完就懂 useLayoutEffect
前端·react.js·面试
parade岁月2 小时前
DOM 里有 Tailwind class,为什么样式还是不生效?v4 闭环修复实战
前端·vue.js
ashuicoder2 小时前
vue文件自动生成路由会成为主流
前端·vue.js
一念杂记2 小时前
玩Huggingface免费服务器(2vCPU+16GRAM+100G空间)系列领取免费服务器保姆级教程
服务器·ai编程