Skip to content

OpenClaw 源码解读(八)记忆系统

本文基于 OpenClaw 2026.3.2 源码,深入解读记忆系统(Memory System)的架构设计与实现细节。


一、模块概览

记忆系统是 OpenClaw 的"长期记忆大脑",赋予 AI 助手跨会话持久记忆能力。它能自动索引用户的笔记文件和历史会话,通过向量检索 + 全文检索的混合方式进行语义搜索,让 AI 在对话中能"回忆起"之前讨论过的内容。

1.1 核心源码分布

目录/文件行数(约)职责
src/memory/manager.ts~786核心管理器(单例缓存、搜索入口、生命周期管理)
src/memory/manager-sync-ops.ts~1240同步操作基类(文件索引、Session 增量同步、Watcher)
src/memory/manager-embedding-ops.ts~807Embedding 操作层(批量嵌入、缓存、多 Provider 批处理)
src/memory/manager-search.ts~191搜索引擎(向量搜索 + 关键词搜索)
src/memory/hybrid.ts~149混合检索融合(向量 + BM25 分数加权融合)
src/memory/mmr.ts~214MMR 多样性重排序(Maximal Marginal Relevance)
src/memory/temporal-decay.ts~167时间衰减(指数衰减,半衰期驱动)
src/memory/query-expansion.ts~806查询扩展(多语言停用词 + 关键词提取)
src/memory/embeddings.ts~306Embedding Provider 工厂(6 种 Provider 创建与回退)
src/memory/embeddings-openai.ts~100+OpenAI Embedding 实现
src/memory/embeddings-gemini.ts~100+Gemini Embedding 实现
src/memory/embeddings-voyage.ts~100+Voyage AI Embedding 实现
src/memory/embeddings-mistral.ts~100+Mistral Embedding 实现
src/memory/embeddings-ollama.ts~100+Ollama 本地 Embedding 实现
src/memory/batch-openai.ts~200+OpenAI 批量 Embedding API
src/memory/batch-gemini.ts~200+Gemini 批量 Embedding API
src/memory/batch-voyage.ts~200+Voyage 批量 Embedding API
src/memory/memory-schema.ts~96SQLite Schema 定义(4 张表 + FTS5 虚拟表)
src/memory/internal.ts~330核心工具函数(分块、哈希、余弦相似度)
src/memory/session-files.ts~131会话文件处理(JSONL 解析 + 脱敏)
src/memory/sqlite.ts~19Node.js SQLite 模块加载
src/memory/sqlite-vec.ts~24sqlite-vec 向量扩展加载
src/memory/types.ts~80类型定义(接口契约)

1.2 系统全景

┌─────────────────────────────────────────────────────────────────────────┐
│                       记忆系统全景架构                                    │
│                                                                          │
│  ┌─── 数据源 ─────────────────────────────────────────────────────────┐ │
│  │                                                                     │ │
│  │  📄 MEMORY.md / memory/*.md    📝 sessions/*.jsonl                 │ │
│  │  (用户显式记忆笔记)              (会话历史转录)                      │ │
│  │                                                                     │ │
│  └───────────┬────────────────────────────┬────────────────────────────┘ │
│              │                            │                              │
│              ▼                            ▼                              │
│  ┌─── 索引管线 ──────────────────────────────────────────────────────┐  │
│  │                                                                    │  │
│  │  listMemoryFiles() ──→ buildFileEntry() ──→ chunkMarkdown()       │  │
│  │  listSessionFiles() ──→ buildSessionEntry() ──→ chunkMarkdown()   │  │
│  │         │                                                          │  │
│  │         ▼                                                          │  │
│  │  enforceEmbeddingMaxInputTokens()                                  │  │
│  │         │                                                          │  │
│  │         ▼                                                          │  │
│  │  ┌─── Embedding Provider ─────────────────────────────────────┐   │  │
│  │  │  OpenAI │ Gemini │ Voyage │ Mistral │ Ollama │ Local(GGUF)│   │  │
│  │  │         ├── Batch API(大批量异步)                          │   │  │
│  │  │         └── 在线 API(小批量同步 + 重试)                   │   │  │
│  │  └────────────────────────────────────────────────────────────┘   │  │
│  │         │                                                          │  │
│  │         ▼                                                          │  │
│  │  SQLite 持久化存储                                                 │  │
│  │  ┌──────────────────────────────────────────────────────────┐     │  │
│  │  │  meta 表       │ 索引元数据(model、provider、版本)      │     │  │
│  │  │  files 表      │ 已索引文件记录(path、hash、mtime)      │     │  │
│  │  │  chunks 表     │ 文本块(id、text、embedding、行号)      │     │  │
│  │  │  chunks_vec    │ sqlite-vec 向量索引(ANN 近似搜索)      │     │  │
│  │  │  chunks_fts    │ FTS5 全文索引(BM25 关键词搜索)         │     │  │
│  │  │  embedding_cache│ Embedding 缓存(避免重复计算)           │     │  │
│  │  └──────────────────────────────────────────────────────────┘     │  │
│  └────────────────────────────────────────────────────────────────────┘  │
│                                                                          │
│  ┌─── 检索管线 ──────────────────────────────────────────────────────┐  │
│  │                                                                    │  │
│  │  search(query)                                                     │  │
│  │     │                                                              │  │
│  │     ├── embedQueryWithTimeout() ──→ searchVector()                │  │
│  │     │   (语义向量检索)                                             │  │
│  │     │                                                              │  │
│  │     ├── searchKeyword() via FTS5 ──→ BM25 评分                    │  │
│  │     │   (关键词全文检索)                                           │  │
│  │     │                                                              │  │
│  │     └── mergeHybridResults()                                       │  │
│  │          │                                                         │  │
│  │          ├── 加权融合 (vectorWeight * vec + textWeight * text)     │  │
│  │          ├── applyTemporalDecayToHybridResults() (时间衰减)        │  │
│  │          └── applyMMRToHybridResults() (多样性重排序)              │  │
│  │                                                                    │  │
│  │     ──→ 返回 MemorySearchResult[]                                  │  │
│  └────────────────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────────────┘

二、类继承体系

记忆系统采用三层抽象类继承设计,逐层分离关注点:

MemoryManagerSyncOps (抽象基类)
    │  - 文件系统操作、文件监听、Session 增量同步
    │  - 数据库打开/Schema 初始化
    │  - SQLite-vec 向量扩展加载
    │  - 安全重建索引(temp-db swap)
    │  - Meta 元数据管理

    └──→ MemoryManagerEmbeddingOps (抽象中间类)
            │  - Embedding 批处理与重试
            │  - Embedding 缓存(load/upsert/prune)
            │  - 多 Provider 批量 API(OpenAI/Gemini/Voyage)
            │  - 文件索引(chunk → embed → 写入 DB)
            │  - Batch 失败计数与自动降级

            └──→ MemoryIndexManager (具体实现类)
                    - 搜索入口 search()
                    - 混合检索编排
                    - 单例缓存 + 工厂方法 get()
                    - 生命周期管理 close()
                    - 状态查询 status()
                    - 文件读取 readFile()

2.1 为什么这样分层?

![三层类继承体系与各层职责](https://qn.huat.xyz/blog/article-Illustration/OpenClaw 源码解读(八)记忆系统/01-infographic-class-hierarchy-1775150705744.png)

  1. SyncOps 层:纯文件 I/O + SQLite 操作,不涉及 Embedding。即使 Embedding Provider 不可用(FTS-only 模式),文件监听和同步逻辑仍然工作。
  2. EmbeddingOps 层:所有与 Embedding 相关的复杂逻辑(批处理、缓存、重试、Provider 切换)集中在此,与 IO 层解耦。
  3. Manager 层:面向消费者的公共 API,编排搜索流程,管理实例生命周期。

三、数据库 Schema

文件 src/memory/memory-schema.ts 定义了 6 张表/索引:

3.1 meta 表 — 索引元数据

sql
CREATE TABLE IF NOT EXISTS meta (
    key TEXT PRIMARY KEY,
    value TEXT NOT NULL
);

存储 memory_index_meta_v1 键,JSON 值包含:

  • model — Embedding 模型名(如 text-embedding-3-small
  • provider — Provider 标识(如 openai
  • providerKey — Provider 配置的哈希(用于检测配置变更)
  • sources — 数据源列表(["memory", "sessions"]
  • chunkTokens / chunkOverlap — 分块参数
  • vectorDims — 向量维度

当以上任何参数变更时,系统会触发完整重建索引

3.2 files 表 — 文件追踪

sql
CREATE TABLE IF NOT EXISTS files (
    path TEXT PRIMARY KEY,
    source TEXT NOT NULL DEFAULT 'memory',
    hash TEXT NOT NULL,
    mtime INTEGER NOT NULL,
    size INTEGER NOT NULL
);

记录每个已索引文件的 SHA-256 哈希。增量同步时比较 hash 决定是否需要重新索引。

3.3 chunks 表 — 文本块

sql
CREATE TABLE IF NOT EXISTS chunks (
    id TEXT PRIMARY KEY,        -- SHA-256(source:path:startLine:endLine:hash:model)
    path TEXT NOT NULL,
    source TEXT NOT NULL DEFAULT 'memory',
    start_line INTEGER NOT NULL,
    end_line INTEGER NOT NULL,
    hash TEXT NOT NULL,
    model TEXT NOT NULL,
    text TEXT NOT NULL,
    embedding TEXT NOT NULL,     -- JSON 序列化的 float[] 
    updated_at INTEGER NOT NULL
);

3.4 chunks_vec — 向量索引(sqlite-vec)

sql
CREATE VIRTUAL TABLE IF NOT EXISTS chunks_vec USING vec0(
    id TEXT PRIMARY KEY,
    embedding FLOAT[{dimensions}]
);

基于 sqlite-vec 扩展,提供 ANN(近似最近邻)向量搜索,使用 vec_distance_cosine() 计算余弦距离。

3.5 chunks_fts — 全文索引(FTS5)

sql
CREATE VIRTUAL TABLE IF NOT EXISTS chunks_fts USING fts5(
    text,
    id UNINDEXED,
    path UNINDEXED,
    source UNINDEXED,
    model UNINDEXED,
    start_line UNINDEXED,
    end_line UNINDEXED
);

基于 SQLite FTS5 扩展,使用 BM25 算法进行关键词评分。只有 text 列被索引,其余列用 UNINDEXED 标记以节省空间。

3.6 embedding_cache — Embedding 缓存

sql
CREATE TABLE IF NOT EXISTS embedding_cache (
    provider TEXT NOT NULL,
    model TEXT NOT NULL,
    provider_key TEXT NOT NULL,
    hash TEXT NOT NULL,           -- 文本内容的 SHA-256
    embedding TEXT NOT NULL,
    dims INTEGER,
    updated_at INTEGER NOT NULL,
    PRIMARY KEY (provider, model, provider_key, hash)
);

避免对相同文本重复调用 Embedding API。缓存键是 (provider, model, provider_key, text_hash) 四元组。

![数据库 Schema 全景:6 张表的关系与角色](https://qn.huat.xyz/blog/article-Illustration/OpenClaw 源码解读(八)记忆系统/02-infographic-db-schema-1775150706529.png)


四、Embedding Provider 体系

4.1 Provider 接口

typescript
// src/memory/embeddings.ts
export type EmbeddingProvider = {
    id: string;               // "openai" | "local" | "gemini" | "voyage" | "mistral" | "ollama"
    model: string;            // 如 "text-embedding-3-small"
    maxInputTokens?: number;  // 输入 token 上限
    embedQuery: (text: string) => Promise<number[]>;       // 单条查询嵌入
    embedBatch: (texts: string[]) => Promise<number[][]>;  // 批量嵌入
};

4.2 六种 Provider

Provider默认模型特点
openaitext-embedding-3-small支持 Batch API,兼容 OpenAI 协议的任意端点
geminiGemini embedding 模型支持 Batch API,Google 云端
voyageVoyage AI 模型支持 Batch API,专注 Embedding
mistralMistral embedding 模型HTTP API
ollamaOllama 本地模型本地运行,无需 API Key
localembeddinggemma-300m-qat-Q8_0.gguf基于 node-llama-cpp,纯本地 GGUF 模型

4.3 Auto 模式选择策略

provider === "auto" 时:

1. 检查是否有本地 GGUF 模型文件 → 优先使用 local
2. 依次尝试 openai → gemini → voyage → mistral
3. 如果所有远程 Provider 都缺少 API Key → 降级到 FTS-only 模式(provider = null)

4.4 Fallback 降级

如果主 Provider 在索引过程中抛出 Embedding 相关错误:

typescript
// manager-sync-ops.ts
private shouldFallbackOnError(message: string): boolean {
    return /embedding|embeddings|batch/i.test(message);
}

系统会自动切换到配置的 fallback Provider,并触发完整重建索引。

4.5 Embedding 归一化

所有 Embedding 向量在返回前都经过 L2 归一化:

typescript
function sanitizeAndNormalizeEmbedding(vec: number[]): number[] {
    const sanitized = vec.map((value) => (Number.isFinite(value) ? value : 0));
    const magnitude = Math.sqrt(sanitized.reduce((sum, value) => sum + value * value, 0));
    if (magnitude < 1e-10) return sanitized;
    return sanitized.map((value) => value / magnitude);
}

归一化后余弦相似度等价于内积,简化了后续计算。

![Embedding Provider 体系与选择策略](https://qn.huat.xyz/blog/article-Illustration/OpenClaw 源码解读(八)记忆系统/03-infographic-embedding-providers-1775150707456.png)


五、索引管线详解

5.1 同步触发时机

系统通过多种触发器保持索引最新:

触发器触发条件代码位置
文件监听memory/.md 文件的 add/change/unlinkensureWatcher() — chokidar
会话增量Session JSONL 文件更新超过阈值ensureSessionListener() — 事件订阅
定时轮询每 N 分钟自动同步ensureIntervalSync()
搜索触发搜索时如果 dirty 则触发同步search() 中的 onSearch 检查
会话启动新会话开始时warmSession()
强制同步CLI openclaw memory sync --force手动

5.2 文件发现

![索引管线:从数据源到 SQLite 存储](https://qn.huat.xyz/blog/article-Illustration/OpenClaw 源码解读(八)记忆系统/04-infographic-indexing-pipeline-1775150708282.png)

typescript
// internal.ts
export async function listMemoryFiles(workspaceDir: string, extraPaths?: string[]) {
    // 扫描以下位置:
    // 1. ${workspaceDir}/MEMORY.md
    // 2. ${workspaceDir}/memory.md
    // 3. ${workspaceDir}/memory/**/*.md(递归)
    // 4. extraPaths 中配置的额外目录/文件
    //
    // 安全措施:跳过符号链接、去重(realpath 去重)
}

5.3 Markdown 分块算法

typescript
// internal.ts
export function chunkMarkdown(content: string, chunking: { tokens: number; overlap: number }) {
    // 参数:
    //   tokens = 目标 chunk 大小(token 数,1 token ≈ 4 chars)
    //   overlap = 重叠 token 数
    //
    // 算法:
    //   maxChars = Math.max(32, tokens * 4)
    //   overlapChars = Math.max(0, overlap * 4)
    //
    //   逐行累积到当前 chunk,当超过 maxChars 时:
    //     1. flush() — 保存当前 chunk(记录 startLine、endLine、text、hash)
    //     2. carryOverlap() — 从当前 chunk 尾部保留 overlapChars 的行
    //   
    //   超长行会被按 maxChars 分段,确保不会产生超大 chunk
}

重叠机制的意义:确保语义在 chunk 边界不会断裂。如果一段关键信息恰好在边界处,前后两个 chunk 都能包含它。

5.4 会话文件处理

typescript
// session-files.ts
export async function buildSessionEntry(absPath: string): Promise<SessionFileEntry | null> {
    // 1. 读取 JSONL 文件(每行一个 JSON 记录)
    // 2. 过滤出 type === "message" 的记录
    // 3. 只保留 role === "user" 或 "assistant" 的消息
    // 4. 提取文本内容(支持 string 和 content blocks 格式)
    // 5. 文本正规化(合并换行/空格)
    // 6. 敏感信息脱敏(redactSensitiveText)
    // 7. 格式化为 "User: xxx\nAssistant: yyy"
    // 8. 生成 lineMap(内容行 → 原始 JSONL 行号映射)
}

lineMap 的设计精妙之处:session 文件是 JSONL 格式,但索引时把消息内容提取拼接成纯文本再分块。chunk 的 startLine/endLine 原本指向拼接后的文本行号,通过 remapChunkLines() 将其映射回原始 JSONL 文件行号,方便调试和定位。

5.5 增量同步 vs 全量重建

增量同步(默认路径):

对每个文件:
  比较 files 表中的 hash 与当前文件 hash
  如果相同 → 跳过
  如果不同 → 重新 indexFile()
  
清理过期记录:
  files 表中存在但文件系统中不存在的 → 删除对应的 files、chunks、chunks_vec、chunks_fts

全量重建索引(Safe Reindex)触发条件:

typescript
const needsFullReindex =
    params?.force ||              // 手动强制
    !meta ||                      // 首次索引
    meta.model !== provider.model ||     // 模型变更
    meta.provider !== provider.id ||     // Provider 变更
    meta.providerKey !== providerKey ||   // API 配置变更
    metaSourcesDiffer(meta, ...) ||      // 数据源变更
    meta.chunkTokens !== settings.chunking.tokens ||  // 分块参数变更
    meta.chunkOverlap !== settings.chunking.overlap || 
    (vectorReady && !meta?.vectorDims);  // 向量表维度缺失

5.6 安全重建索引(Temp-DB Swap)

全量重建采用原子交换策略,确保索引一致性:

1. 创建临时数据库 ${dbPath}.tmp-${uuid}
2. 初始化 Schema + 种子(从旧 DB 迁移 embedding_cache)
3. 在临时 DB 上执行所有索引操作
4. 关闭旧 DB 和临时 DB
5. 原子交换文件:
   a. 旧 DB → 备份路径
   b. 临时 DB → 目标路径
   c. 如果交换失败 → 从备份恢复
6. 删除备份
7. 重新打开目标 DB

如果中间任何步骤失败,restoreOriginalState() 会恢复到交换前状态。这是事务安全的索引重建方案。

5.7 会话增量同步机制

会话同步采用字节/消息双阈值 + 防抖策略:

Session JSONL 更新事件


scheduleSessionDirty()  ─── 5 秒防抖 ───→ processSessionDeltaBatch()


updateSessionDelta()
    │  统计文件增长的字节数和新行数(消息数)


是否达到阈值?(deltaBytes 或 deltaMessages)

    ├── 否 → 累积等待
    └── 是 → sync({ reason: "session-delta" })

countNewlines() 方法用分块读取(64KB 缓冲区)统计新增行数,避免大文件整体读入内存。

![同步策略对比:增量同步 vs 全量重建 vs 会话增量](https://qn.huat.xyz/blog/article-Illustration/OpenClaw 源码解读(八)记忆系统/05-infographic-sync-strategies-1775150709171.png)


六、搜索管线详解

6.1 搜索入口

typescript
// manager.ts
async search(query: string, opts?): Promise<MemorySearchResult[]> {
    // 1. 可选:warmSession() 触发会话同步
    // 2. 可选:dirty 时触发搜索同步
    // 3. 根据模式分流:
    //    a. FTS-only 模式(无 Embedding Provider)→ 纯关键词搜索
    //    b. Vector-only 模式(FTS 不可用)→ 纯向量搜索
    //    c. Hybrid 模式 → 向量 + 关键词混合搜索
}

6.2 向量搜索

typescript
// manager-search.ts
export async function searchVector(params) {
    // 优先路径:sqlite-vec 可用
    //   使用 vec_distance_cosine() 计算余弦距离
    //   score = 1 - distance(距离越小,分数越高)
    //
    // 降级路径:sqlite-vec 不可用
    //   从 chunks 表加载所有 embedding
    //   在内存中逐一计算 cosineSimilarity()
    //   排序取 Top-K
}

两条路径的性能差异

  • sqlite-vec:底层 C 实现的 ANN 索引,时间复杂度接近 O(log n)
  • 降级路径:O(n) 暴力扫描,但保证在 sqlite-vec 不可用时仍能工作

6.3 关键词搜索

typescript
// manager-search.ts
export async function searchKeyword(params) {
    // 1. buildFtsQuery() 将用户查询转为 FTS5 MATCH 表达式
    //    "hello world" → '"hello" AND "world"'
    //
    // 2. FTS5 MATCH 查询 + bm25() 排名
    //
    // 3. bm25RankToScore() 将 BM25 rank 转为 [0,1] 分数
    //    score = 1 / (1 + rank)
}

6.4 FTS-only 模式(无 Embedding Provider)

![搜索管线:混合检索融合流程](https://qn.huat.xyz/blog/article-Illustration/OpenClaw 源码解读(八)记忆系统/06-infographic-search-pipeline-1775150710021.png)

当所有 Embedding Provider 都不可用时(如缺少 API Key),系统降级到纯 FTS 搜索:

typescript
// manager.ts - search() 中的 FTS-only 分支
if (!this.provider) {
    // 1. extractKeywords() 提取关键词
    //    "that thing we discussed about the API" → ["discussed", "API"]
    // 2. 对每个关键词分别执行 searchKeyword()
    // 3. 合并去重(保留最高分)
    // 4. 排序 + minScore 过滤 + maxResults 截断
}

6.5 混合检索融合

typescript
// hybrid.ts
export async function mergeHybridResults(params) {
    // Step 1: 按 chunk ID 合并向量和关键词结果
    //   每个结果获得 (vectorScore, textScore) 二元组
    //   只出现在一种结果中的条目,另一分数为 0
    
    // Step 2: 加权融合
    //   score = vectorWeight * vectorScore + textWeight * textScore
    //   默认:vectorWeight = 0.7, textWeight = 0.3
    
    // Step 3: 时间衰减(可选)
    //   applyTemporalDecayToHybridResults()
    
    // Step 4: MMR 多样性重排序(可选)
    //   applyMMRToHybridResults()
}

6.6 时间衰减算法

typescript
// temporal-decay.ts
export function applyTemporalDecayToScore(params) {
    // 指数衰减公式:score' = score × e^(-λ × age)
    // 其中 λ = ln(2) / halfLifeDays
    //
    // 默认半衰期 30 天 → 30 天前的记忆分数衰减到原来的 50%
}

时间戳来源的优先级:

  1. 文件路径中的日期:memory/2025-01-15.md → 2025-01-15
  2. 常青文件不衰减MEMORY.mdmemory.mdmemory/topics.md 等被认为是长期知识
  3. 文件系统 mtime:最后修改时间
typescript
function isEvergreenMemoryPath(filePath: string): boolean {
    // MEMORY.md, memory.md → 常青
    // memory/任何非日期文件.md → 常青
    // memory/2025-01-15.md → 非常青(有日期)
}

6.7 MMR 多样性重排序

typescript
// mmr.ts — Carbonell & Goldstein (1998)
//
// MMR = λ × relevance - (1-λ) × max_similarity_to_selected
//
// 算法流程:
// 1. 将所有候选分数归一化到 [0, 1]
// 2. 预计算所有候选的 token 集合(用于 Jaccard 相似度)
// 3. 贪心迭代选择:
//    a. 对每个未选候选,计算 MMR 分数
//    b. 选择 MMR 最高的候选加入已选集合
//    c. 重复直到所有候选都被选择

为什么需要 MMR? 纯相似度排序可能返回大量内容高度重叠的结果。MMR 通过惩罚与已选结果相似的候选,确保返回结果的多样性

相似度计算使用 Jaccard 相似度(而非余弦相似度),因为此时操作的是文本片段而非向量:

typescript
export function jaccardSimilarity(setA: Set<string>, setB: Set<string>): number {
    // |A ∩ B| / |A ∪ B|
}

七、查询扩展(Query Expansion)

7.1 多语言停用词

query-expansion.ts 包含 8 种语言的停用词表:

语言变量特殊处理
英语STOP_WORDS_EN包含时间词 (yesterday, today)
西班牙语STOP_WORDS_ES
葡萄牙语STOP_WORDS_PT
阿拉伯语STOP_WORDS_AR
中文STOP_WORDS_ZH字符级分词 + bigram
韩语STOP_WORDS_KO尾缀助词剥离
日语STOP_WORDS_JA混合脚本(汉字/假名/ASCII)分割

7.2 CJK 分词策略

由于没有引入专门的分词器(如 jieba),系统对 CJK 文本采用字符 n-gram 策略:

typescript
// 中文:单字 + 双字
// "讨论方案" → ["讨", "论", "方", "案", "讨论", "论方", "方案"]

// 日文:分离不同脚本
// "APIのバグ修正" → ["api", "バグ", "修正", "修", "正"]

// 韩文:剥离助词
// "API를" → ["api를", "api"]  (strip 를)
// "논의" → 保留原词(2 音节以上有效)

7.3 关键词提取流程

typescript
export function extractKeywords(query: string): string[] {
    // 1. tokenize() — 分词
    // 2. 过滤停用词(8 种语言)
    // 3. 过滤无效关键词(太短、纯数字、纯标点)
    // 4. 去重
    //
    // 示例:
    //   "that thing we discussed about the API" → ["discussed", "api"]
    //   "之前讨论的那个方案" → ["讨论", "方案", "讨", "论", "方", "案"]
}

![多语言查询扩展策略](https://qn.huat.xyz/blog/article-Illustration/OpenClaw 源码解读(八)记忆系统/07-infographic-query-expansion-1775150710721.png)

八、Embedding 批处理与缓存

8.1 批处理策略

typescript
// manager-embedding-ops.ts
private buildEmbeddingBatches(chunks: MemoryChunk[]): MemoryChunk[][] {
    // 按 EMBEDDING_BATCH_MAX_TOKENS = 8000 分批
    // 估算每个 chunk 的 UTF-8 字节数作为 token 近似
    // 超大单个 chunk → 独占一个 batch
}

8.2 三级 Batch API

对于 OpenAI、Gemini、Voyage 三家 Provider,支持异步批量 API:

embedChunksWithBatch()

    ├── OpenAI → embedChunksWithOpenAiBatch() → runOpenAiEmbeddingBatches()
    ├── Gemini → embedChunksWithGeminiBatch() → runGeminiEmbeddingBatches()
    └── Voyage → embedChunksWithVoyageBatch() → runVoyageEmbeddingBatches()

Batch API 支持异步轮询,适合大量文件首次索引场景。

8.3 Batch 失败处理

batchFailureCount = 0

batch 调用失败


recordBatchFailure()
    │  batchFailureCount += 1


batchFailureCount >= BATCH_FAILURE_LIMIT (2) ?

    ├── 是 → 自动禁用 batch(batch.enabled = false)
    │         后续使用同步 embedBatchWithRetry()

    └── 否 → 本次降级到同步 API,batch 保持启用

8.4 Embedding 缓存机制

embedChunksInBatches(chunks)


collectCachedEmbeddings(chunks)
    │  对每个 chunk.hash 查询 embedding_cache 表

    ├── 缓存命中 → 直接使用
    └── 缓存未命中 → 调用 Provider API


    upsertEmbeddingCache()  ← 写入缓存

缓存键为 (provider, model, provider_key, text_hash) 四元组,确保切换 Provider 或 API 配置后缓存自动失效。

缓存还有 LRU 淘汰机制:

typescript
pruneEmbeddingCacheIfNeeded() {
    // 如果缓存条目数 > maxEntries
    // 按 updated_at 升序删除最旧的 excess 条
}

8.5 重试策略

typescript
// 指数退避 + 抖动
embedBatchWithRetry(texts) {
    // 最大重试 3 次
    // 基础延迟 500ms,最大延迟 8000ms
    // 每次 delay *= 2,加 20% 随机抖动
    // 只对限流错误重试(429、rate_limit、5xx、cloudflare)
}

![Embedding 批处理与缓存三级架构](https://qn.huat.xyz/blog/article-Illustration/OpenClaw 源码解读(八)记忆系统/08-infographic-batch-cache-1775150711642.png)

九、单例管理与生命周期

9.1 单例缓存

typescript
// manager.ts
const INDEX_CACHE = new Map<string, MemoryIndexManager>();
const INDEX_CACHE_PENDING = new Map<string, Promise<MemoryIndexManager>>();

static async get(params) {
    // 缓存键 = agentId:workspaceDir:JSON(settings)
    // 1. 检查 INDEX_CACHE → 命中返回
    // 2. 检查 INDEX_CACHE_PENDING → 避免并发创建
    // 3. 创建新实例 → 写入两个 Cache
}

每个 Agent 的记忆系统只有一个实例,避免多个 WebSocket 连接同时打开同一个 SQLite 数据库。

9.2 构造函数做了什么?

typescript
private constructor(params) {
    // 1. 打开 SQLite 数据库
    // 2. 计算 providerKey(配置指纹)
    // 3. 初始化 Schema(ensureSchema)
    // 4. 恢复向量维度(从 meta 表)
    // 5. 启动文件监听器(chokidar)
    // 6. 启动 Session 事件监听器
    // 7. 启动定时同步
    // 8. 标记 dirty(需要首次同步)
    // 9. 解析 batch 配置
}

9.3 关闭流程

typescript
async close() {
    // 1. 标记 closed = true
    // 2. 清除所有定时器(watch、session、interval)
    // 3. 关闭文件监听器
    // 4. 取消 Session 事件订阅
    // 5. 等待进行中的 sync 完成
    // 6. 关闭 SQLite 连接
    // 7. 从 INDEX_CACHE 中移除
}

9.4 只读恢复

在某些场景下(如 NFS 或 Docker 卷),SQLite 可能突然变为只读模式。系统有自动恢复机制:

typescript
private async runSyncWithReadonlyRecovery(params) {
    try {
        await this.runSync(params);
    } catch (err) {
        if (this.isReadonlyDbError(err)) {
            // 1. 关闭当前连接
            // 2. 重新打开数据库
            // 3. 重新初始化 Schema
            // 4. 重新尝试同步
            // 5. 记录恢复统计
        }
    }
}

![单例管理与生命周期](https://qn.huat.xyz/blog/article-Illustration/OpenClaw 源码解读(八)记忆系统/09-infographic-lifecycle-1775150712434.png)

十、数据流全景图

用户创建/修改 MEMORY.md


chokidar 检测变更 ──→ markDirty() ──→ scheduleWatchSync() (防抖)


sync({ reason: "watch" })

        ├── 检查是否需要全量重建(meta 参数比对)
        │      ├── 是 → runSafeReindex()(temp-db 原子交换)
        │      └── 否 → 增量同步


syncMemoryFiles() / syncSessionFiles()

        ├── listMemoryFiles() → 发现文件
        ├── buildFileEntry() → 计算 hash
        ├── hash 比对 → 跳过未变更文件


indexFile(entry, { source: "memory" })

        ├── readFile → chunkMarkdown() → chunks[]
        ├── enforceEmbeddingMaxInputTokens() → 截断过长 chunk


embedChunksInBatches(chunks) / embedChunksWithBatch(chunks)

        ├── loadEmbeddingCache() → 缓存命中的直接用
        ├── buildEmbeddingBatches() → 8000 token 分批
        ├── embedBatchWithRetry() → Provider API 调用(含重试)
        ├── upsertEmbeddingCache() → 写入缓存


写入 SQLite:
        ├── DELETE 旧的 chunks / chunks_vec / chunks_fts
        ├── INSERT INTO chunks (id, text, embedding, ...)
        ├── INSERT INTO chunks_vec (id, embedding_blob)
        └── INSERT INTO chunks_fts (text, id, path, ...)
        
──────────── 搜索时 ────────────

用户问 "上周讨论的 API 设计"


search(query)

        ├── warmSession() → 可能触发同步

        ├── FTS-only 模式?
        │     └── extractKeywords() → searchKeyword() × N → 合并去重

        ├── embedQueryWithTimeout() → queryVec

        ├── searchVector(queryVec) → 向量结果(余弦距离)

        ├── searchKeyword(query) → 关键词结果(BM25)


mergeHybridResults()

        ├── 按 ID 合并(向量分数 + 文本分数)
        ├── 加权融合:score = 0.7 × vec + 0.3 × text
        ├── 时间衰减:score × e^(-λ × age)
        ├── MMR 重排序:平衡相关性与多样性


过滤 minScore → 截断 maxResults → 返回 MemorySearchResult[]

![数据流全景:写入路径与查询路径](https://qn.huat.xyz/blog/article-Illustration/OpenClaw 源码解读(八)记忆系统/10-infographic-data-flow-1775150713410.png)

十一、关键设计决策与权衡

11.1 为什么选择 SQLite 而非专门的向量数据库?

优势

  • 零外部依赖:Node.js 22+ 内置 node:sqlite,不需要运行额外的数据库进程
  • 单文件部署:整个索引就是一个 .db 文件,方便备份和迁移
  • 事务原子性:利用 SQLite 的 ACID 保证索引一致性
  • FTS5 内置:全文搜索不需要额外的搜索引擎

权衡

  • 向量搜索性能不如 Pinecone/Qdrant 等专用方案
  • 通过 sqlite-vec 扩展弥补,对于个人助手的数据规模足够

11.2 为什么 Embedding 存两份?

chunks.embedding(JSON 文本)和 chunks_vec(二进制向量表)存了同一份 Embedding,原因:

  1. chunks_vec 依赖 sqlite-vec 扩展,可能加载失败
  2. 降级路径需要从 chunks.embedding 解析向量做内存计算
  3. embedding_cache 是第三份"缓存",用于跨索引重建复用

11.3 混合检索的权重设计

默认 vectorWeight=0.7, textWeight=0.3 的设计考量:

  • 语义搜索(向量)擅长理解意图("API 设计" ≈ "接口架构")
  • 关键词搜索擅长精确匹配(搜索 "React" 就要包含 "React")
  • 0.3 的文本权重确保精确匹配不会被语义淹没
  • 代码中还有一个"宽松降级":如果混合分数都低于 minScore,但关键词结果存在,放宽阈值到 Math.min(minScore, textWeight)

11.4 Session 增量同步的精细控制

不是每收到一条消息就索引,而是通过双阈值控制:

  • deltaBytes:累积字节增量达到阈值
  • deltaMessages:累积消息条数达到阈值

加上 5 秒防抖,避免高频对话时过于频繁地触发索引。这是一个吞吐量 vs 实时性的平衡。


![关键设计决策与权衡](https://qn.huat.xyz/blog/article-Illustration/OpenClaw 源码解读(八)记忆系统/11-infographic-design-tradeoffs-1775150714214.png)

十二、与其他模块的交互

┌─── Agent 系统 ───┐
│                   │
│  Agent 运行时     │──→ MemorySearchManager.search() ──→ 注入 context
│  (Pi Agent Core)  │
│                   │──→ MemorySearchManager.readFile() ──→ 工具调用
│                   │
│  memory-search.ts │──→ resolveMemorySearchConfig() ──→ 解析配置
└───────────────────┘


┌─── 配置系统 ───┐
│                 │
│  config.json    │──→ agents.defaults.memorySearch.* ──→ 提供配置
│  (JSON5)        │     provider, model, sources, chunking, hybrid, ...
└─────────────────┘


┌─── Gateway ───┐
│                │
│  状态查询      │──→ MemoryIndexManager.status() ──→ 显示在 status 面板
│  CLI 命令      │──→ sync({ force: true }) ──→ 手动触发同步
└────────────────┘


┌─── 会话系统 ───┐
│                 │
│  transcript-    │──→ onSessionTranscriptUpdate() ──→ 触发 session 索引
│  events.ts      │
└─────────────────┘

![模块交互:记忆系统与外部模块的接口关系](https://qn.huat.xyz/blog/article-Illustration/OpenClaw 源码解读(八)记忆系统/12-infographic-module-interactions-1775150714988.png)

十三、配置参考

记忆系统的配置位于 agents.defaults.memorySearch(或 per-agent override):

json5
{
  "agents": {
    "defaults": {
      "memorySearch": {
        "provider": "auto",           // "auto" | "openai" | "gemini" | "local" | ...
        "model": "",                   // 默认由 provider 决定
        "fallback": "none",            // 备选 provider
        "sources": ["memory", "sessions"],
        "extraPaths": [],              // 额外索引路径
        "chunking": {
          "tokens": 400,               // 每个 chunk 的目标 token 数
          "overlap": 50                // 重叠 token
        },
        "query": {
          "maxResults": 10,
          "minScore": 0.35,
          "hybrid": {
            "enabled": true,
            "vectorWeight": 0.7,
            "textWeight": 0.3,
            "candidateMultiplier": 3,  // 过采样倍率
            "mmr": {
              "enabled": false,
              "lambda": 0.7
            },
            "temporalDecay": {
              "enabled": false,
              "halfLifeDays": 30
            }
          }
        },
        "sync": {
          "watch": true,               // 文件监听
          "watchDebounceMs": 2000,
          "intervalMinutes": 0,        // 定时轮询(0=禁用)
          "onSearch": true,            // 搜索时同步
          "onSessionStart": true,
          "sessions": {
            "deltaBytes": 4096,        // Session 增量字节阈值
            "deltaMessages": 10        // Session 增量消息阈值
          }
        },
        "store": {
          "path": "~/.openclaw/agents/{agentId}/memory.db",
          "vector": {
            "enabled": true,
            "extensionPath": ""
          }
        },
        "cache": {
          "enabled": true,
          "maxEntries": 10000
        },
        "remote": {
          "batch": {
            "enabled": false,
            "wait": true,
            "concurrency": 2,
            "pollIntervalMs": 2000,
            "timeoutMinutes": 60
          }
        },
        "local": {
          "modelPath": "",
          "modelCacheDir": ""
        }
      }
    }
  }
}

十四、总结

OpenClaw 的记忆系统是一个工程完成度极高的检索增强生成(RAG)实现,有以下亮点:

  1. 混合检索:向量 + BM25 双引擎,比纯向量检索更稳健
  2. 优雅降级:Provider 不可用 → FTS-only;sqlite-vec 不可用 → 内存向量计算;Batch 失败 → 同步 API
  3. 增量索引:基于 hash 的增量同步 + Session 增量阈值,避免不必要的重建
  4. 安全重建:temp-db 原子交换,保证索引一致性
  5. 多语言支持:8 种语言的停用词 + CJK 分词策略
  6. 可观测性:完善的 status() 状态查询和日志子系统
  7. 6 种 Embedding Provider:从纯本地到云端全覆盖,自动选择 + 自动回退

整个设计遵循"个人助手优先"理念:单文件 SQLite 存储、零外部服务依赖、本地可运行,同时通过 Batch API 和缓存优化支持大规模数据。

读文档、看源码、写代码,理解 AI Agent 本质 🤖