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,包含:
- 模型名称、提供者
- 数据源列表(memory/sessions)
- 分块参数(tokens/overlap)
- 作用域哈希(用于检测是否需要全量重建)
- 向量维度
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 中间切断
}
关键参数(默认值):
chunking.tokens = 512:每块约 512 tokenschunking.overlap = 64:相邻块重叠 64 tokens
为什么要有 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 批处理支持
对于大量文本块,提供者可以走批处理模式:
- 提交一批文本 → 异步等待完成 → 拉取结果
- 支持轮询间隔、超时、并发限制
- 失败次数达到阈值(默认 5 次)后停用批处理
六、同步机制:保持索引新鲜
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 优势:
- 更大的索引容量
- 更快的搜索速度
- 自定义搜索模式(query/search/vsearch)
九、实际案例演示
案例 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 # 消息数阈值
十一、优劣分析
优势
- 双后端灵活:builtin 零依赖,QMD 高性能
- 增量索引:哈希检测 + 文件监听 + 会话增量,避免重复计算
- 混合搜索:FTS + Vector 互补,关键词明确时精确匹配,模糊时语义匹配
- 原子重构:重建索引不影响服务
- 插件化架构:可以替换嵌入提供者、添加 corpus supplement
- 短期记忆提升:模拟人类记忆的"近期优先"效应
局限
- 必须重启才能感知新文件? 不,chokidar 实时监听
- 全量重建时性能:原子替换涉及大量 SQLite I/O
- 嵌入缓存有限:有 maxEntries 限制,超量后需要修剪
- QMD 需要外部二进制:不是所有环境都有
- 会话索引默认开启:可能消耗额外磁盘空间
评分
| 维度 | 分数 | 说明 |
|---|---|---|
| 架构设计 | ⭐⭐⭐⭐⭐ | 分层清晰,插件化优雅 |
| 代码质量 | ⭐⭐⭐⭐⭐ | TypeScript 类型严谨,测试覆盖率高 |
| 扩展性 | ⭐⭐⭐⭐⭐ | provider / corpus / qmd 都可扩展 |
| 性能 | ⭐⭐⭐⭐ | 增量索引 + 原子重建,但全量有开销 |
| 文档化 | ⭐⭐⭐⭐ | 源码注释足但外部文档较少 |
报告生成时间: 2026-05-08 06:35 UTC
分析基于 OpenClaw main branch 源码