OpenClaw 内置记忆系统源码分析

> 来源: OpenClaw 源码 (main branch, 2026-05-08)

> 分析范围: memory-core 扩展 + memory-host-sdk 包 + 插件框架

> 核心文件: ~80 个 TypeScript 文件,集中在 extensions/memory-core/src/memory/src/memory-host-sdk/

一句话版本

OpenClaw 的记忆系统是一个基于 SQLite 的语义搜索引擎——它会自动读取你写的 MEMORY.md 和对话记录,切成小块算成"向量指纹",然后在你提问时用关键词+语义双重匹配找到最相关的内容塞进上下文里。

一、架构总览

OpenClaw 的记忆系统不是简单的"读个文件放进去",而是一套插件化、可扩展的语义索引管道,分为三层:


┌─────────────────────────────────────────────────────────┐
│                  第 1 层:插件入口                        │
│  extensions/memory-core/                                │
│  ├── 注册 memory_search / memory_get 工具               │
│  ├── 注册 MemoryPluginRuntime (生命周期管理)             │
│  ├── 注册 promptBuilder (动态构建记忆提示词)             │
│  └── 注册 flushPlanResolver (上下文刷新策略)             │
├─────────────────────────────────────────────────────────┤
│                  第 2 层:索引管理器                      │
│  extensions/memory-core/src/memory/manager.ts           │
│  ├── MemoryIndexManager (核心类)                        │
│  ├── SQLite 数据库 (chunks / files / meta / fts / vec)  │
│  ├── 嵌入提供者 (embedding provider)                    │
│  ├── FTS5 全文搜索 + sqlite-vec 向量搜索                │
│  └── 混合搜索权重合并                                   │
├─────────────────────────────────────────────────────────┤
│                  第 3 层:主机 SDK                       │
│  src/memory-host-sdk/                                   │
│  ├── 文件发现 (listMemoryFiles)                         │
│  ├── 文本分块 (chunkMarkdown)                           │
│  ├── 内容哈希 (SHA-256)                                 │
│  ├── 数据库 Schema (memory-schema.ts)                   │
│  ├── 本地嵌入提供者 (host/embeddings.ts)                │
│  └── QMD 外部集成 (qmd-manager.ts)                      │
└─────────────────────────────────────────────────────────┘

双后端模式

OpenClaw 支持两种后端,通过配置 memory.backend 切换:

后端原理适用场景
**builtin** (默认)SQLite + FTS5 + sqlite-vec,纯内建标准使用,开箱即用
**qmd**调用外部 `qmd` 二进制大规模索引、更高效的查询

两者通过 FallbackMemoryManager 包装:如果 QMD 挂掉,自动降级到 builtin。

二、核心数据结构:SQLite 数据库

当 OpenClaw 启动时,会在状态目录创建 SQLite 数据库,包含以下表:

chunks 表(核心分块存储)


CREATE TABLE IF NOT EXISTS chunks (
  id TEXT PRIMARY KEY,
  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 格式的浮点数数组
  updated_at INTEGER NOT NULL
);

每条记录 = 一个文本块 + 它的向量嵌入

files 表(文件追踪)


CREATE TABLE IF NOT EXISTS files (
  path TEXT PRIMARY KEY,
  source TEXT NOT NULL DEFAULT 'memory',
  hash TEXT NOT NULL,          -- SHA-256 内容哈希
  mtime INTEGER NOT NULL,
  size INTEGER NOT NULL
);

作用:避免重复索引——只有哈希变了才会重新处理

meta 表(索引元数据)


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

存储 memory_index_meta_v1 的 JSON,包含:

chunks_fts(FTS5 全文搜索表)


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
);

支持分词的全文搜索,默认使用 unicode61 分词器

chunks_vec(sqlite-vec 向量表)


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

支持余弦相似度的向量索引,需要 sqlite-vec 扩展

embedding_cache(嵌入缓存)


CREATE TABLE IF NOT EXISTS embedding_cache (
  provider TEXT NOT NULL,
  model TEXT NOT NULL,
  provider_key TEXT NOT NULL,
  hash TEXT NOT NULL,
  embedding TEXT NOT NULL,
  dims INTEGER,
  updated_at INTEGER NOT NULL,
  PRIMARY KEY (provider, model, provider_key, hash)
);

去重相同文本的嵌入请求,节省 API 费用

三、数据流水线:从文件到索引

整个过程可以概括为:


文件系统         →    分块     →    嵌入     →    存储
MEMORY.md              text1          [0.1,0.2...]    chunks + fts + vec
memory/*.md            text2          [0.1,0.2...]    chunks + fts + vec
session/*.jsonl                                    

3.1 文件发现 (`listMemoryFiles`)


// 来源: src/memory-host-sdk/host/internal.ts
async function listMemoryFiles(workspaceDir, extraPaths?, multimodal?) {
  // 1. 检查 MEMORY.md, memory.md
  // 2. 遍历 memory/ 目录
  // 3. 加上 extraPaths 配置中的额外路径
  // 4. 多模态:图片/音频等非 .md 文件
  // 5. 去重(处理符号链接)
}

发现优先级:

1. {workspaceDir}/MEMORY.md

2. {workspaceDir}/memory.md(小写备用)

3. {workspaceDir}/memory/*(递归,忽略 .git/node_modules)

4. extraPaths 配置中的路径

3.2 文本分块 (`chunkMarkdown`)

这是最巧妙的部分之一。OpenClaw 不是整个文件当一条记录,而是按token 预算切成小块:


// 来源: src/memory-host-sdk/host/internal.ts
function chunkMarkdown(content: string, chunking: { tokens: number; overlap: number }) {
  // 1. 按行分割
  // 2. 计算每行的字符权重(CJK 字符算 1 个 token,拉丁字符约 4 个 1 token)
  // 3. 按 maxChars 限制合并行
  // 4. 支持 overlap:前后块有重叠行
  // 5. CJK 特殊处理:过长行按 fine step 二次切割
  // 6. 避免在 UTF-16 surrogate pair 中间切断
}

关键参数(默认值):

为什么要有 overlap? 避免搜索时恰好切在关键内容中间,比如一个函数定义被切到两块里。

3.3 变化检测(避免重复索引)

每个文件内容经过 SHA-256 哈希,只有哈希变化才重新索引:


// 哈希变化判断
if (!needsFullReindex && existingHashes.get(entry.path) === entry.hash) {
  // 跳过,文件未变化
  return;
}

3.4 会话文件索引

会话不是直接按 .jsonl 文件的全部内容索引,而是经过 buildSessionEntry 处理:

1. 读取 JSONL 会话文件

2. 提取消息文本(去掉工具调用等噪声)

3. 拼接成纯文本字符串

4. 记录原始行号和映射表(lineMap

5. 按同样的分块逻辑处理

四、搜索流水线:从问题到答案


用户提问          →     混合搜索     →     结果合并     →     返回
"之前讨论过         FTS5 关键词        BM25 + Cosine    Top-K 结果
 什么数据库方案?"     Vector 语义       MMR 去重          附带引用
                    时间衰减           排序              

4.1 搜索入口 (`memory_search` 工具)


// 来源: extensions/memory-core/src/tools.ts
execute: async (_toolCallId, params) => {
  const query = params.query;
  const maxResults = params.maxResults;
  const minScore = params.minScore;
  const corpus = params.corpus; // "memory" | "wiki" | "all"

  // 如果是 wiki 搜索,跳过 builtin 记忆
  // 1. 获取 MemoryIndexManager
  // 2. 调用 manager.search(query, opts)
  // 3. 装饰引用(添加 citation 格式)
  // 4. 合并 corpus supplement 结果
  // 5. 记录短期回忆追踪
  // 6. 返回排序后的结果
}

4.2 核心搜索逻辑 (`MemoryIndexManager.search`)


// 来源: extensions/memory-core/src/memory/manager.ts
async search(query, opts) {
  // 1. 预检:检查是否有索引内容,没有则触发同步
  // 2. 异步触发后台同步(如果配置了 onSearch sync)
  // 3. 确保嵌入提供者已初始化
  
  // 分支 A:无嵌入提供者(纯 FTS 模式)
  if (!this.provider) {
    // FTS5 直接搜索 + 关键词宽泛化搜索
    // 如果 AND 搜索没结果,拆成单个关键词搜索
    return 关键词匹配结果;
  }
  
  // 分支 B:有嵌入提供者(混合搜索)
  const keywordResults = FTS5关键词搜索(query);
  const queryVec = 嵌入查询文本(query);  // 把问题转成向量
  const vectorResults = 向量相似度搜索(queryVec);
  
  // 混合搜索:合并关键词 + 向量结果
  // 应用时间衰减、MMR 多样性去重
  return 混合排序结果;
}

4.3 混合搜索策略


// 来源: extensions/memory-core/src/memory/hybrid.ts
mergeHybridResults({ vector, keyword, vectorWeight, textWeight, mmr, temporalDecay }) {
  // 1. 向量搜索按 cosine 相似度排序
  // 2. 关键词按 BM25 排名排序
  // 3. 权重合并:score = vector_weight * vec_score + text_weight * keyword_score
  // 4. MMR 去重:避免相似度过高的结果扎堆
  // 5. 时间衰减:旧结果降权(可选)
  // 6. 返回 Top-K
}

权重默认值: vectorWeight = 0.7, textWeight = 0.3(语义优先)

4.4 MMR 多样性算法


// 来源: extensions/memory-core/src/memory/mmr.ts
// MMR = Maximum Marginal Relevance
// 公式:λ * score(i) - (1-λ) * max(similarity(i, j)) for j in selected
// 简单说:不但要看相关性,还要看和已选结果的区别度
// 避免一堆相似的结果占满名额

4.5 时间衰减


// 来源: extensions/memory-core/src/memory/temporal-decay.ts
// 基于文件的修改时间计算衰减系数
// 半衰期可配置(默认 30 天)
// decay = 2^(-days_since_modification / half_life_days)
// 最近的记忆权重高,远古记忆逐渐淡化

五、嵌入提供者系统

嵌入提供者负责把文本变成向量(一个浮点数数组,如 1024 维)。

5.1 提供者类型

通过 embeddings.ts 的工厂函数创建:


// 来源: extensions/memory-core/src/memory/embeddings.ts
createEmbeddingProvider({ provider, fallback, model, apiKey, ... }) {
  // "auto" 模式:按优先级依次尝试所有提供者
  // 特定模式:尝试指定提供者,失败则 fallback
}

已注册的提供者:

ID说明
`local`内置本地嵌入(默认,零依赖)
`openai`OpenAI Embeddings API
`voyage`Voyage AI
`cohere`Cohere
`gemini`Google Gemini
`anthropic`Anthropic

5.2 自动选择逻辑

auto 模式按 autoSelectPriority 顺序尝试:

1. 本地嵌入(如果有模型文件)

2. 远程 API 提供者

5.3 回退链


如果主提供者失败 → 触发回退(fallback 提供者)
  → 修改索引标记
  → 全量重新索引(用新提供者重新生成所有嵌入)

5.4 批处理支持

对于大量文本块,提供者可以走批处理模式:

六、同步机制:保持索引新鲜

6.1 五种触发同步的方式

触发方式说明
**文件监听 (watch)**chokidar 监控 MEMORY.md 和 memory/ 目录变化
**会话监听 (session)**监听 `onSessionTranscriptUpdate` 事件
**定时同步 (interval)**按配置的间隔定期同步
**搜索触发 (onSearch)**搜索时如果检测到脏标记,异步同步
**会话启动 (onSessionStart)**新会话开始时预热同步

6.2 文件监听去抖


// 文件变化 → 标记 dirty → 等待 500ms → 触发同步
// 避免短时间内多次修改导致频繁重建

6.3 会话增量同步

会话文件不是每次全量重新索引,而是:

1. 记录文件大小

2. 增量检测新增字节数

3. 累计到一定阈值才触发同步

4. 避免每次对话更新都重建索引

6.4 原子重建

当需要全量重建时(比如模型变了、分块参数变了),采用原子替换策略:


// 1. 建一个临时数据库 (.tmp-uuid)
// 2. 把所有数据写到临时库
// 3. 写完后:关闭旧库 → 替换文件 → 打开新库
// 4. 失败时:恢复旧库(不丢数据)

七、短期记忆提升(Short-Term Promotion)

记忆系统还有一个"短期记忆"机制:


// 来源: extensions/memory-core/src/short-term-promotion.ts
// 每次搜索的结果会被记录到一个短期存储中
// 短期记忆中的内容优先返回
// 模拟人类"最近想起的事情更容易再想起"的效应
// 使用 Deep Dreaming 机制进行周期性加强

八、QMD 外部后端

除 builtin 外,OpenClaw 支持集成外部 QMD(Quantized Memory Database)二进制:


// 来源: extensions/memory-core/src/memory/qmd-manager.ts
// QMD 是一个独立的 Rust 二进制
// 提供更高效的索引和搜索能力
// 通过 FallbackMemoryManager 实现无缝降级

QMD 优势:

九、实际案例演示

案例 1:标准记忆检索

场景: Agent 被问到"我之前讨论过什么数据库方案?"


1. Agent 调用 memory_search(query: "数据库方案")
2. MemoryIndexManager.search("数据库方案") 被调用
3. 预检:索引是否存在?没有则触发同步
4. FTS5 搜索:"数据库" AND "方案" → 找到 chunks 123, 456
5. 向量搜索:编码 "数据库方案" 为 [0.31, -0.55, ...] → 找到 chunks 123, 789
6. 混合合并:
   - chunk 123: FTS=0.85, Vec=0.92 → score=0.7*0.92+0.3*0.85=0.899
   - chunk 456: FTS=0.72, Vec=0.45 → score=0.7*0.45+0.3*0.72=0.531
   - chunk 789: FTS=0.00, Vec=0.78 → score=0.7*0.78=0.546
7. MMR 去重后:chunk 123, 789, 456
8. 返回带引用的结果

案例 2:在 AGENTS.md 中定义后自动注入

当 Agent 加载 AGENTS.md 时,会触发 buildMemoryPromptSection()


// 来源: extensions/memory-core/src/prompt-section.ts
// 构建提示词片段,告诉 Agent 如何使用记忆工具
// 片段格式类似:
// "Mandatory recall step: semantically search MEMORY.md + memory/*.md
//  before answering questions about prior work, decisions, dates..."

这就是为什么每次 Agent 启动时,系统提示词里都包含记忆相关指令。

案例 3:文件监听触发自动索引


1. 用户编辑 MEMORY.md 并保存
2. chokidar 检测到 change 事件
3. 标记 dirty = true
4. 等待 500ms 去抖
5. 触发 sync({ reason: "watch" })
6. listMemoryFiles() 重新扫描
7. 比较哈希:MEMORY.md 哈希变了
8. 重新 chunkMarkdown + 嵌入 + 写库
9. 状态恢复 dirty = false

案例 4:会话历史索引


1. 用户和 Agent 对话
2. 每轮对话结束,触发 onSessionTranscriptUpdate
3. 会话文件 session-xxx.jsonl 有新增内容
4. 累计 deltaBytes 到达阈值
5. 触发 sync({ reason: "session-delta" })
6. buildSessionEntry() 解析 JSONL 并提取文本
7. 按行号映射分段
8. 索引写入 chunks + fts + vec
9. 下次讨论历史内容时,Agent 能通过 memory_search 查到

十、配置参考


# ~/.openclaw/config.yaml 中的记忆配置
memory:
  backend: builtin                    # builtin | qmd
  sources: [memory, sessions]         # 数据源
  extraPaths: []                      # 额外搜索路径
  chunking:
    tokens: 512                       # 每块 token 数
    overlap: 64                       # 重叠 token 数
  store:
    path: ~/.openclaw/memory.db       # 数据库路径
    vector:
      enabled: true
      extensionPath: ''               # sqlite-vec 扩展路径
    fts:
      tokenizer: unicode61            # unicode61 | trigram
  query:
    minScore: 0.0                     # 最低匹配分数
    maxResults: 10                    # 最多返回结果
    hybrid:
      enabled: true
      vectorWeight: 0.7               # 语义权重
      textWeight: 0.3                 # 关键词权重
      candidateMultiplier: 20         # 候选倍率
      mmr:
        enabled: false                # MMR 去重
        lambda: 0.5
      temporalDecay:
        enabled: false                # 时间衰减
        halfLifeDays: 30
  provider: auto                      # 嵌入提供者
  model: ''                           # 模型名
  fallback: none                      # 回退提供者
  sync:
    watch: true                       # 文件监听
    watchDebounceMs: 500
    intervalMinutes: 0                # 定时同步(0=禁用)
    onSearch: false                   # 搜索触发同步
    onSessionStart: true              # 会话启动预热
    sessions:
      deltaBytes: 4096               # 会话增量字节阈值
      deltaMessages: 0               # 消息数阈值

十一、优劣分析

优势

局限

评分

维度分数说明
架构设计⭐⭐⭐⭐⭐分层清晰,插件化优雅
代码质量⭐⭐⭐⭐⭐TypeScript 类型严谨,测试覆盖率高
扩展性⭐⭐⭐⭐⭐provider / corpus / qmd 都可扩展
性能⭐⭐⭐⭐增量索引 + 原子重建,但全量有开销
文档化⭐⭐⭐⭐源码注释足但外部文档较少

报告生成时间: 2026-05-08 06:35 UTC

分析基于 OpenClaw main branch 源码