openclawmemorysource-codeimplementationsqlitevector-search
深度解析
openclaw/src/memory/目录下的核心实现
关键设计决策(宏观视角)
在深入源码之前,先理解 Memory 系统的三个核心设计问题。
1. Memory 如何与 Agent 集成?
答案:作为 Tool 提供给 LLM,而非自动注入。
flowchart TB subgraph "集成架构" A[Agent] -->|System Prompt 强制要求| B["先调用 memory_search"] B -->|Tool Call| C[memory_search] B -->|Tool Call| D[memory_get] C -->|查询| E[Memory System] D -->|读取| E end
关键决策对比:
| 方案 | 机制 | 优点 | 缺点 |
|---|---|---|---|
| Tool-based (OpenClaw) | Agent 显式调用 | ✅ 可控、可观测、精确 | 需要 AI 配合 |
| Auto-injection | 系统自动注入 | 无需 AI 参与 | ❌ 可能注入无关内容、Token 浪费 |
System Prompt 强制要求 (src/agents/system-prompt.ts):
"Before answering anything about prior work, decisions, dates, people,
preferences, or todos: run memory_search on MEMORY.md + memory/*.md;
then use memory_get to pull only the needed lines."2. 什么东西写入了记忆?
答案:三层分层架构,价值递减。
flowchart TB subgraph "三层记忆模型" S[Session Memory] -->|临时| D[Daily Memory] D -->|短期| P[Permanent Memory] end S -.->|自动记录| S2[JSON Lines<br/>可能丢失] D -.->|Agentic Flush| D2[memory/YYYY-MM-DD.md<br/>AI 判断写入] P -.->|人工维护| P2[MEMORY.md<br/>长期保留]
| 层级 | 存储 | 写入方式 | 生命周期 |
|---|---|---|---|
| Session | sessions/*.jsonl | 自动记录 | 当前会话,可能丢失 |
| Daily | memory/YYYY-MM-DD.md | Agentic Flush | AI 判断写入,按日期组织 |
| Permanent | MEMORY.md | 人工/AI | 长期保留,精选知识 |
Agentic Flush 机制:上下文快满时,给 AI 一个特殊 Turn,让 AI 自主判断什么值得写入 memory/YYYY-MM-DD.md。如果无需写入,回复 NO_REPLY 静默跳过。
3. 聊天历史如何保留?
答案:两种方式互补。
flowchart LR A[聊天历史] --> B[方式1: Context Window] A --> C[方式2: Memory Search] B -->|Pi-mono 自动维护| D["messages 数组"] D -->|传入 LLM| E[自然理解] D -->|限制| F[受限于 context window] C -->|配置 sources| G[索引 Session JSONL] G -->|memory_search| H[检索增强] H -->|显式提供| E
- Context Window:Pi SDK 自动维护,适合当前会话
- Memory Search:配置
sources: ["sessions"],通过 Tool 检索历史
目录结构
src/memory/
├── manager.ts # MemoryIndexManager 主类
├── manager-sync-ops.ts # 同步操作基类
├── manager-embedding-ops.ts # 嵌入操作基类
├── manager-search.ts # 搜索实现
├── search-manager.ts # 后端路由
├── hybrid.ts # 混合搜索算法
├── mmr.ts # MMR 多样性重排
├── temporal-decay.ts # 时间衰减
├── embeddings*.ts # 各 Provider 实现
├── memory-schema.ts # 数据库 Schema
├── qmd-manager.ts # QMD 后端
└── types.ts # 类型定义
核心类分析
1. MemoryIndexManager 主类
文件: manager.ts
职责: 索引管理器的主入口,协调同步、嵌入、搜索三大功能。
类继承设计:
flowchart BT M[MemoryIndexManager] S[SearchOperations] E[EmbeddingOperations] Y[SyncOperations] M -->|extends| S S -->|extends| E E -->|extends| Y
设计意图: 通过继承链分离职责,避免”上帝类”。
// manager.ts
class MemoryIndexManager extends SearchOperations {
// 主入口方法
async search(query: string, options?: SearchOptions): Promise<Result[]> {
// 1. 检查脏数据
if (this.dirty) await this.sync();
// 2. 执行搜索(在父类 SearchOperations 中实现)
return super.search(query, options);
}
}
// manager-search.ts
class SearchOperations extends EmbeddingOperations {
async search(query: string, options?: SearchOptions) {
// 向量搜索 + 关键词搜索 + 融合
}
}
// manager-embedding-ops.ts
class EmbeddingOperations extends SyncOperations {
async embedChunks(chunks: Chunk[]) {
// 批量嵌入 + 缓存
}
}
// manager-sync-ops.ts
class SyncOperations {
async sync() {
// 文件同步 + 索引更新
}
}为什么这样设计?
- 单一职责: 每个基类只负责一个方面
- 可测试性: 可以单独测试每个基类
- 可扩展性: 新增功能只需添加基类
2. 同步机制
文件: manager-sync-ops.ts
核心方法: sync()
async sync(force: boolean = false): Promise<void> {
// 防止并发同步
if (this.syncing) return;
this.syncing = true;
try {
// 判断是否需要全量重建
if (await this.needsFullReindex() || force) {
await this.runSafeReindex();
} else {
// 增量同步
await this.syncMemoryFiles();
await this.syncSessionFiles();
}
} finally {
this.syncing = false;
this.dirty = false;
}
}精妙之处:
syncing标志防止并发冲突- 自动判断全量重建 vs 增量同步
finally确保状态清理
安全重建: runSafeReindex()
private async runSafeReindex(): Promise<void> {
const tmpPath = `${this.dbPath}.tmp-${uuid()}`;
const backupPath = `${this.dbPath}.backup-${uuid()}`;
try {
// 1. 在临时数据库重建
await this.rebuildInDatabase(tmpPath);
// 2. 关闭原数据库连接
await this.closeDatabase();
// 3. 原子替换
await rename(this.dbPath, backupPath); // 旧 -> 备份
await rename(tmpPath, this.dbPath); // 新 -> 正式
// 4. 删除备份
await unlink(backupPath);
// 5. 重新连接
await this.openDatabase();
} catch (err) {
// 回滚:从备份恢复
if (await fileExists(backupPath)) {
await rename(backupPath, this.dbPath);
}
throw err;
}
}为什么安全?
- 原子性: 通过文件重命名实现原子操作
- 零停机: 重建时读旧库,完成后瞬间切换
- 可回滚: 任何步骤失败都能恢复原状
文件监控
private setupFileWatcher(): void {
this.watcher = chokidar.watch([
"MEMORY.md",
"memory.md",
"memory/**/*.md"
], {
ignoreInitial: true,
awaitWriteFinish: {
stabilityThreshold: 1500, // 1.5秒防抖
pollInterval: 100
}
});
this.watcher.on("change", () => this.markDirty());
}
private markDirty(): void {
this.dirty = true;
// 防抖:1.5秒后触发同步
setTimeout(() => {
if (this.dirty && !this.syncing) {
this.sync();
}
}, 1500);
}设计亮点:
chokidar跨平台文件监控awaitWriteFinish防抖,避免文件写入过程中触发setTimeout二次防抖,批量处理文件变更
4. Memory Flush 机制(核心!)
文件: src/auto-reply/reply/memory-flush.ts
这是 OpenClaw Memory 系统最关键的设计——Agentic Flush 的完整实现。
4.1 Prompt 构造流程
基础 User Prompt(第 13-18 行):
export const DEFAULT_MEMORY_FLUSH_PROMPT = [
"Pre-compaction memory flush.",
"Store durable memories now (use memory/YYYY-MM-DD.md; create memory/ if needed).",
"IMPORTANT: If the file already exists, APPEND new content only and do not overwrite existing entries.",
`If nothing to store, reply with ${SILENT_REPLY_TOKEN}.`,
].join(" ");基础 System Prompt(第 20-24 行):
export const DEFAULT_MEMORY_FLUSH_SYSTEM_PROMPT = [
"Pre-compaction memory flush turn.",
"The session is near auto-compaction; capture durable memories to disk.",
`You may reply, but usually ${SILENT_REPLY_TOKEN} is correct.`,
].join(" ");4.2 Prompt 处理三步曲
Step 1: 时间戳替换(resolveMemoryFlushPromptForRun)
const dateStamp = formatDateStampInTimezone(nowMs, userTimezone);
const withDate = params.prompt.replaceAll("YYYY-MM-DD", dateStamp);
// 添加时间线
return `${withDate}\n${timeLine}`;
// 结果示例: "Current time: 2026-01-15 14:30:00 PST"关键细节:使用用户配置的时区,不是 UTC!从 cfg.agents.defaults.userTimezone 获取。
Step 2: NO_REPLY 强制机制(ensureNoReplyHint)
function ensureNoReplyHint(text: string): string {
if (text.includes(SILENT_REPLY_TOKEN)) {
return text;
}
// 如果用户自定义 prompt 忘了加 NO_REPLY,自动追加
return `${text}\n\nIf no user-visible reply is needed, start with NO_REPLY.`;
}Step 3: 合并 System Prompt
const flushSystemPrompt = [
params.followupRun.run.extraSystemPrompt,
memoryFlushSettings.systemPrompt,
].filter(Boolean).join("\n\n");4.3 最终构造的完整 Prompt
【System Prompt】
Pre-compaction memory flush turn.
The session is near auto-compaction; capture durable memories to disk.
You may reply, but usually NO_REPLY is correct.
【User Prompt】
Pre-compaction memory flush.
Store durable memories now (use memory/2026-01-15.md; create memory/ if needed).
IMPORTANT: If the file already exists, APPEND new content only and do not overwrite existing entries.
If nothing to store, reply with NO_REPLY.
Current time: 2026-01-15 14:30:00 PST
If no user-visible reply is needed, start with NO_REPLY.
4.4 可配置项
通过 agents.defaults.compaction.memoryFlush:
| 配置项 | 默认值 | 说明 |
|---|---|---|
enabled | true | 是否启用 Flush |
softThresholdTokens | 4000 | 触发阈值(距离上限的 token 数) |
prompt | 见上文 | 覆盖 User Prompt |
systemPrompt | 见上文 | 覆盖 System Prompt |
forceFlushTranscriptBytes | 2MB | 强制触发阈值(文件大小) |
4.5 触发条件
// shouldRunMemoryFlush() 逻辑
if (totalTokens >= contextWindow - reserveTokens - softThreshold) {
// 且本 compaction cycle 未执行过
if (!hasAlreadyFlushedForCurrentCompaction(entry)) {
return true; // 触发 Flush!
}
}关键洞察:
- 软阈值:token 接近上限时触发(默认提前 4000 tokens)
- 硬阈值:transcript 文件超过 2MB 强制触发
- 防重:每个 compaction cycle 只触发一次
5. 长期记忆 MEMORY.md 管理
核心区别:与 Daily 不同,MEMORY.md 没有自动 Flush 机制。
5.1 为什么没有 Flush 机制?
这是 OpenClaw 的刻意设计选择:
Daily Memory: 自动 Flush → 防止信息丢失(兜底机制)
MEMORY.md: 无自动机制 → 保证记忆质量(人工精选)
设计哲学:
- Daily 是”原始记录”——防止上下文压缩导致的信息丢失
- MEMORY.md 是”精选知识”——只有经过人工判断值得长期保留的才写入
5.2 AGENTS.md 如何指导 AI 写入
虽然没有 Flush Prompt,但 AI 通过 AGENTS.md 持续获得写入指导。
AGENTS.md 是 Bootstrap 文件(src/agents/workspace.ts 第 25 行):
export const DEFAULT_AGENTS_FILENAME = "AGENTS.md";注入 System Prompt 流程:
- 加载 Bootstrap 文件(
src/agents/pi-embedded-helpers/bootstrap.ts):
export function buildBootstrapContextFiles(files: WorkspaceBootstrapFile[], ...)
→ 处理 AGENTS.md 等文件- 注入 Project Context(
src/agents/system-prompt.ts第 612-644 行):
const contextFiles = params.contextFiles ?? [];
if (validContextFiles.length > 0) {
lines.push("# Project Context", "");
for (const file of validContextFiles) {
lines.push(`## ${file.path}`, "", file.content, ""); // AGENTS.md 内容注入
}
}AGENTS.md 中的记忆指导(docs/reference/templates/AGENTS.md):
### 🧠 MEMORY.md - Your Long-Term Memory
- **ONLY load in main session** (direct chats with your human)
- **DO NOT load in shared contexts** (security consideration)
- You can **read, edit, and update** MEMORY.md freely in main sessions
- Write significant events, thoughts, decisions, opinions, lessons learned
- This is your curated memory — the distilled essence, not raw logs
### 📝 Write It Down - No "Mental Notes"!
- **Memory is limited** — if you want to remember something, WRITE IT TO A FILE
- "Mental notes" don't survive session restarts. Files do.
- When someone says "remember this" → update `memory/YYYY-MM-DD.md` or relevant file
- When you learn a lesson → document it so future-you doesn't repeat it5.3 写入触发方式
方式 1: 用户主动要求(主要方式)
用户: "记住我喜欢 VS Code"
用户: "把这个决策写入长期记忆"
用户: "remember this"
AI 判断:长期偏好/重要决策 → 写入 MEMORY.md
方式 2: AI 自主判断
AI: "我注意到您经常提到这个偏好,Should I save this to MEMORY.md?"
或
AI 直接执行:完成重要配置后自动写入
方式 3: 人工直接编辑
- 用户直接编辑
MEMORY.md文件 - 文件监控自动索引(与 Daily 相同机制)
5.4 写入实现机制
与 Daily 相同,没有专门的 append API:
// AI 的执行逻辑:
1. read file("MEMORY.md")
→ 得到现有内容
2. 在内存中拼接:
existingContent + "\n" + newContent
3. write file("MEMORY.md", combinedContent)
→ 原子替换整个文件关键区别:
- Daily:被 Prompt 强制要求写入(被动)
- MEMORY.md:AI 根据 AGENTS.md 指导自主决定(主动)
5.5 记忆读取:同样通过 AGENTS.md 指导
重要澄清:OpenClaw 没有代码层面的自动读取机制!
文档中的误解:
❌ 错误理解:"启动时自动读取 today + yesterday"
✅ 正确理解:"AGENTS.md 指导 AI 去读取"
AGENTS.md 中的读取指导(docs/reference/templates/AGENTS.md 第 20-22 行):
## Every Session
Before doing anything else:
1. Read `SOUL.md` — this is who you are
2. Read `USER.md` — this is who you're helping
3. Read `memory/YYYY-MM-DD.md` (today + yesterday) for recent context
4. **If in MAIN SESSION**: Also read `MEMORY.md`实现机制:
AGENTS.md (包含读取指导)
↓
buildBootstrapContextFiles() 处理
↓
注入 System Prompt 的 "# Project Context"
↓
AI 看到:"Read memory/YYYY-MM-DD.md (today + yesterday)"
↓
AI 自主执行:调用 read 工具读取文件
关键洞察:
- ❌ 没有代码自动读取 daily 文件
- ❌ 没有代码自动读取 MEMORY.md
- ✅ 完全依赖 AGENTS.md 的 Prompt 指导
- ✅ AI 自主决定是否执行(虽然 AGENTS.md 强烈建议执行)
对比总结:
| 操作 | Daily (memory/*.md) | Permanent (MEMORY.md) |
|---|---|---|
| 写入 | Flush Prompt 强制触发 | AGENTS.md 指导 + AI 自主 |
| 读取 | AGENTS.md 指导读取 | AGENTS.md 指导读取 |
| 自动性 | 写入自动,读取非自动 | 读写都非自动 |
6. 嵌入管理
文件: manager-embedding-ops.ts
批量嵌入策略
private async embedChunksWithBatch(
chunks: Chunk[],
provider: EmbeddingProvider
): Promise<Embedding[]> {
// 1. 检查缓存(关键性能优化)
const uncachedChunks: Chunk[] = [];
for (const chunk of chunks) {
const cached = await this.getCachedEmbedding(chunk.hash);
if (cached) {
embeddings.push(cached);
} else {
uncachedChunks.push(chunk);
}
}
// 2. 批量嵌入
const batchSize = this.getBatchSize(provider);
for (let i = 0; i < uncachedChunks.length; i += batchSize) {
const batch = uncachedChunks.slice(i, i + batchSize);
const batchEmbeddings = await this.embedWithRetry(batch, provider);
// 3. 更新缓存
for (let j = 0; j < batch.length; j++) {
await this.cacheEmbedding(batch[j].hash, batchEmbeddings[j]);
}
embeddings.push(...batchEmbeddings);
}
return embeddings;
}缓存策略:
- L1: 内存 LRU Cache(进程内)
- L2: SQLite
embedding_cache表(持久化) - 基于内容 hash,内容不变永不过期
为什么重要?
- 嵌入 API 调用昂贵($0.02/1K tokens)
- 缓存命中率通常 > 80%
- 成本降低 90%+
7. 搜索实现
文件: manager-search.ts, hybrid.ts
混合搜索流程
async search(query: string, options: SearchOptions): Promise<SearchResult[]> {
// 1. 查询向量化
const queryVector = await this.embedQueryWithTimeout(query);
// 2. 向量搜索(Top K * 4 候选)
const vectorResults = await this.searchVector(
queryVector,
options.maxResults * options.candidateMultiplier // 4x
);
// 3. 关键词搜索(BM25)
const keywordResults = await this.searchKeyword(query);
// 4. 融合(hybrid.ts)
const merged = mergeHybridResults(
vectorResults,
keywordResults,
options.vectorWeight, // 默认 0.7
options.textWeight // 默认 0.3
);
// 5. 可选:时间衰减
if (options.temporalDecay?.enabled) {
applyTemporalDecay(merged, options.temporalDecay.halfLifeDays);
}
// 6. 可选:MMR 多样性重排
if (options.mmr?.enabled) {
return applyMMRToHybridResults(merged, queryVector, options.mmr.lambda);
}
// 7. 截断返回
return merged.slice(0, options.maxResults);
}候选池扩展策略:
- 先召回 4 倍结果(默认 6 → 24)
- 给融合算法更多选择空间
- 最后截断到用户请求的 maxResults
数据库 Schema 设计
文件: memory-schema.ts
-- 元数据表
CREATE TABLE meta (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
);
-- 存储: 模型名称、分块参数、schema 版本
-- 文件表
CREATE TABLE files (
path TEXT PRIMARY KEY,
source TEXT NOT NULL DEFAULT 'memory',
hash TEXT NOT NULL, -- SHA256 内容哈希
mtime INTEGER NOT NULL, -- 修改时间
size INTEGER NOT NULL
);
-- 用于增量同步:对比 hash 判断文件是否变更
-- 块表(核心)
CREATE TABLE 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
);
-- 向量存储为 JSON 文本,应用层解析计算
-- 嵌入缓存表
CREATE TABLE 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)
);
-- 复合主键避免重复计算
-- FTS5 全文搜索虚拟表
CREATE VIRTUAL TABLE chunks_fts USING fts5(
text, id UNINDEXED, path UNINDEXED, source UNINDEXED
);
-- 自动分词索引设计决策:
- JSON 存储向量: 简单通用,不依赖 sqlite-vec 扩展
- hash 索引: 快速判断内容是否变更
- FTS5 虚拟表: SQLite 内置,无需额外依赖
关键数据流
索引创建流程
sequenceDiagram participant F as Markdown File participant C as chunkMarkdown participant E as embedChunks participant DB as SQLite F->>C: 读取文件内容 C->>C: 按 tokens 分块(默认 400) C->>C: 保留段落边界,80 tokens 重叠 C->>E: chunks[] loop 批量嵌入 E->>E: 检查缓存 E->>E: 调用 Provider API E->>E: 更新缓存 end E->>DB: INSERT INTO chunks DB->>DB: FTS5 自动索引
搜索流程
sequenceDiagram participant U as User participant S as Search Manager participant B as Builtin Backend participant DB as SQLite U->>S: search(query) alt dirty S->>B: sync() B->>DB: 更新索引 end B->>B: embedQuery(query) par 并行查询 B->>DB: SELECT vector (topK*4) B->>DB: SELECT FTS5 (BM25) end B->>B: mergeHybridResults() B->>B: applyMMR() B->>S: results S->>U: results
设计亮点总结
- 分层继承: 避免上帝类,职责分离清晰
- 安全重建: 原子替换,失败可回滚
- 双重防抖: 文件监控 + setTimeout
- 缓存优先: 嵌入缓存是关键性能优化
- 候选池扩展: 4x 召回给融合算法更多选择
- JSON 向量: 简单通用,不依赖扩展