OpenClaw 最近很火。火的原因有很多,但我觉得其中一个蛮重要的因素是它的记忆系统。大多数 coding agent 跑完一次就忘了,下次你得重新解释一遍项目背景、架构偏好、之前踩过的坑。OpenClaw 不一样,它能跨 session 记住你是谁、你的项目长什么样、你之前做过什么决定,而且这些记忆是透明的,你随时能打开文件看到它记了什么。
我觉得可以从三个维度来理解这套系统:写入(Storage)、检索(Retrieval)、治理(Governance)。
写入:Markdown-first
OpenClaw 的记忆设计有一个非常明确的核心原则:Markdown 文件是 source of truth。
具体来说:
- agent 的长期记忆存在
MEMORY.md里, - 短期的、按天组织的记忆存在
memory/YYYY-MM-DD.md里, - 完整的会话历史以 JSONL 格式存在
~/.openclaw/agents/<agentId>/sessions/下,每个 session 一个.jsonl文件。这些原始对话记录不在项目目录里,但会被 dreaming 的 Light phase 读取,从中提取值得保留的信号。
打开这些文件,你就能看到 agent 记住了什么。MEMORY.md 是经过整理的长期记忆,存的是持久性的事实和决策:
# Long-term Memory
## About the human
- Lives in Mountain View, prefers Chinese for daily conversation
- Working on XXXX
## Project decisions
- Coding style: strict TypeScript, no any, prefer explicit types
## Lessons learned
- Don't auto-commit without asking. User wants to review diffs first.
- Morning briefing should include calendar + email, skip weather unless asked.memory/YYYY-MM-DD.md 则是按时间顺序的当日原始记录,更碎片化:
# 2026-04-07
## 10:00 AM
User asked to research OpenClaw's memory system. Read through source code.
Key files: memory-core plugin, short-term-promotion.ts, dreaming-phases.ts.
## 1:00 PM
Started drafting a blog post about OpenClaw memory.
Framework: storage, retrieval, governance.
## 6:00 PM
User pointed out the memory flush section had a false causal claim.
Fixed: Markdown-first and memory flush are parallel design choices,
not cause-and-effect.两者的关系是:daily notes 是原始素材,MEMORY.md 是提炼后的结论。AGENTS.md 模板里对此有明确的指引:"Daily files are raw notes; MEMORY.md is curated wisdom."
你可以直接编辑这些文件来纠正或补充信息,改完后索引会自动重建。
这和大多数 RAG 系统的思路不同。典型的 RAG 系统里,truth 在向量库里,用户看不到也碰不到。OpenClaw 反过来:向量库、embedding、SQLite 索引,这些都是从文件派生出来的二级结构,随时可以重建,文件才是不可替代的。
Agent 怎么和记忆交互
Agent 通过两个工具访问记忆:memory_search 做语义搜索,返回匹配的 snippet 和 score;memory_get 读取特定文件的特定行。写入没有专门的工具,agent 直接用通用的文件编辑工具往 MEMORY.md 或 memory/YYYY-MM-DD.md 里写。写什么、写到哪,由 AGENTS.md 里的 prompt 引导:日常笔记写 daily note,持久性的事实和决策写 MEMORY.md。
Memory Flush:compaction 前的保底
长对话会撞上 context window 的上限。OpenClaw 用 compaction 来解决这个问题:把旧对话压缩成摘要,释放 token 空间。但 compaction 是有损的,压缩过程中可能会丢失细节。
为了防止重要信息在 compaction 中被丢掉,OpenClaw 在 compaction 之前会额外跑一轮 agent turn,prompt 里明确要求 agent 把当前对话中值得保留的信息写到 memory 文件里:
export const DEFAULT_MEMORY_FLUSH_PROMPT = [
"Pre-compaction memory flush.",
MEMORY_FLUSH_TARGET_HINT,
MEMORY_FLUSH_READ_ONLY_HINT,
MEMORY_FLUSH_APPEND_ONLY_HINT,
"Do NOT create timestamped variant files...",
`If nothing to store, reply with ${SILENT_REPLY_TOKEN}.`,
].join(" ");这就是 memory flush:在有损操作发生之前,先把该存的存到磁盘上。存到磁盘上的内容不受 compaction 影响,之后可以通过 memory_search 被检索到。
检索:Hybrid Search Pipeline
文件是 truth,但搜索不能每次都全文扫描。OpenClaw 在文件之上建了一层索引,核心流程是:切 chunk → 做 embedding → 建关键词索引 → 搜索时并行查两条路径 → 加权合并。
Chunking
chunkMarkdown() 负责把 markdown 文件按 token 数切成重叠的 chunk:
export function chunkMarkdown(
content: string,
chunking: { tokens: number; overlap: number },
): MemoryChunk[] {
const lines = content.split("\n");
const maxChars = Math.max(32, chunking.tokens * CHARS_PER_TOKEN_ESTIMATE);
const overlapChars = Math.max(0, chunking.overlap * CHARS_PER_TOKEN_ESTIMATE);
const chunks: MemoryChunk[] = [];
// ... 按行累积,达到 maxChars 时 flush,
// 保留 overlapChars 的尾部作为下一个 chunk 的开头
}每个 chunk 记录了 startLine、endLine、text、hash 和用于 embedding 的 embeddingInput。这些元数据后面在 dreaming 的 rehydration 环节会再次用到。
Indexing
indexFile() 把每个 chunk 写入三个地方:
// 1. chunks 表:存文本、embedding、元数据
db.prepare(
`INSERT INTO chunks (id, path, source, start_line, end_line,
hash, model, text, embedding, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET ...`
).run(id, path, source, startLine, endLine, hash, model, text,
JSON.stringify(embedding), now);
// 2. vector 表:存向量,用于相似度搜索
replaceMemoryVectorRow({ db, tableName: VECTOR_TABLE, id, embedding });
// 3. FTS5 表:存文本,用于关键词搜索
db.prepare(
`INSERT INTO ${FTS_TABLE} (text, id, path, source, model,
start_line, end_line) VALUES (?, ?, ?, ?, ?, ?, ?)`
).run(text, id, path, source, model, startLine, endLine);三张表各司其职:chunks 是主表,VECTOR_TABLE 支撑向量检索,FTS_TABLE(SQLite FTS5)支撑 BM25 关键词检索。
Search
搜索时,两条路径并行执行,然后做加权合并:
// 关键词搜索(BM25)
const keywordResults =
hybrid.enabled && this.fts.enabled && this.fts.available
? await this.searchKeyword(cleaned, candidates).catch(() => [])
: [];
// 向量搜索(cosine similarity)
const queryVec = await this.embedQueryWithTimeout(cleaned);
const hasVector = queryVec.some((v) => v !== 0);
const vectorResults = hasVector
? await this.searchVector(queryVec, candidates).catch(() => [])
: [];
// 加权合并
const merged = await this.mergeHybridResults({
vector: vectorResults,
keyword: keywordResults,
vectorWeight: hybrid.vectorWeight,
textWeight: hybrid.textWeight,
mmr: hybrid.mmr,
temporalDecay: hybrid.temporalDecay,
});mergeHybridResults 不是简单的分数相加。它支持 MMR(Maximal Marginal Relevance,用来降低结果之间的冗余)和 temporal decay(时间衰减,让更新的记忆得到更高权重)。这些都是可配置的。
整个 search pipeline 可以用一张图概括:
这就是 OpenClaw built-in memory 的完整 recall pipeline。整个实现都在 SQLite 里完成,不依赖外部服务。
其他 Backend 方案:改的是 recall engine,不是 memory ontology
上面讲的是默认的 SQLite built-in 方案。OpenClaw 还支持其他 backend,但理解它们之间的关系比理解每个方案的细节更重要。
QMD(Query-Memory-Document) 是一个实验性的 sidecar 后端。它不替换 Markdown-first 的设计,而是把搜索这一层抽成一个独立进程,提供更强的 retrieval pipeline:reranking、query expansion、多 collection 管理。适合对召回质量有更高要求的场景,但 memory ontology 完全没变,长期记忆仍然是 MEMORY.md。
LanceDB 方向有两条路线。官方的 memory-lancedb 插件定位很轻,核心就是用 LanceDB 做纯 vector search,配一层简单的 auto-recall / auto-capture。社区版的 memory-lancedb-pro 则更像一个独立的 memory subsystem,加了 hybrid + cross-encoder rerank、结构化 schema(scope / category / importance)、更重的自动化逻辑。
| 方案 | 搜索能力 | 复杂度 | Memory ontology |
|---|---|---|---|
| SQLite built-in | Hybrid(vector + BM25) | 低 | Markdown 文件 |
| QMD sidecar | Hybrid + rerank + query expansion | 中 | Markdown 文件 |
| memory-lancedb | 纯 vector | 低 | Markdown 文件 |
| memory-lancedb-pro | Hybrid + cross-encoder rerank | 高 | 自有结构化 schema |
关键判断:除了 memory-lancedb-pro 走了自己的路,其他所有方案改的都是 recall engine,不是 memory ontology。 不管你用 SQLite 还是 QMD 还是 LanceDB,MEMORY.md 和 memory files 仍然是那个你能直接打开编辑的记忆文件。
Apr 7, 2026 Update: OpenClaw 2026.4.5 版本加入了 dreaming 机制。
治理:Dreaming
我二月刚装 OpenClaw 的时候,还没有 dreaming 这套机制。那时候记忆治理基本靠用户自己来:在 heartbeat 里设定定期整理的规则,或者手动清理 MEMORY.md。最近的版本加入了 dreaming,把这件事自动化了。
dreaming 是一套自动化的短期记忆整理与长期提升系统。 名字借用了人类睡眠的阶段,分成三个 phase:
- Light:扫最近几天的 daily notes 和 session transcripts,把里面的内容登记到一个短期 recall store 里。类似人睡着后大脑开始回放白天的经历,先把素材摊开。
- REM:从这些素材里提炼模式。根据 concept tags 找出反复出现的主题,生成 reflections 和 candidate truths,给后续的决策提供判断依据。类似做梦时大脑在整理碎片、建立关联。
- Deep:做最终的 promotion 决策。对每条 candidate 做多维打分,过阈值的才会被写入
MEMORY.md。这是唯一会修改长期记忆的阶段。类似深度睡眠时大脑把真正重要的经历固化成长期记忆。
整个 pipeline 的效果是:agent 日常使用中产生的大量短期信号,经过层层筛选,只有反复出现、跨上下文验证过的内容才会进入长期记忆。
数据流全貌
第一步:记账
当 agent 调用 memory_search 时,系统会异步把被返回的搜索结果记录到短期 recall store:
function queueShortTermRecallTracking(params: {
workspaceDir?: string;
query: string;
rawResults: MemorySearchResult[];
surfacedResults: MemorySearchResult[];
timezone?: string;
}): void {
const trackingResults = resolveRecallTrackingResults(
params.rawResults, params.surfacedResults);
void recordShortTermRecalls({
workspaceDir: params.workspaceDir,
query: params.query,
results: trackingResults,
timezone: params.timezone,
}).catch(() => {
// best-effort, must never block memory recall
});
}这里有两个细节值得注意。第一,只记录真正 surfaced 给模型的结果,不是所有原始搜索结果。第二,写入是 best-effort 的,用 void + .catch() 确保不阻塞正常的 memory recall。
这些信号最终落在 memory/.dreams/short-term-recall.json。每个 entry 以 source:path:startLine:endLine 为 key,累计 recallCount、totalScore、queryHashes、recallDays、conceptTags 等统计量。形成一本 evidence ledger。
第二步:Light 和 REM 补充 reinforcement
Light phase 做两件事:扫最近的 memory/YYYY-MM-DD.md daily notes,以及整理 session transcript。它把这些内容也记成 short-term signals,补充进 recall store。
REM phase 在 light 的基础上更进一步:根据 recent entries 的 concept tags 提炼主题、生成 reflections 和 candidate truths,并把命中的 key 写入 memory/.dreams/phase-signals.json。
这两个 phase 都不会写 MEMORY.md。它们的角色是 reinforcement:给 deep phase 提供额外的信号和权重加成。
第三步:Deep Phase 打分与提升
Deep phase 是唯一会写 MEMORY.md 的阶段。它对每个 candidate 做六维加权打分:
export const DEFAULT_PROMOTION_WEIGHTS: PromotionWeights = {
frequency: 0.24, // 被召回的总次数
relevance: 0.3, // 平均召回质量
diversity: 0.15, // query/day context 的多样性
recency: 0.15, // 时间衰减(half-life decay)
consolidation: 0.1, // 是否跨多天反复出现
conceptual: 0.06, // concept tag 的密度
};
const score =
weights.frequency * frequency +
weights.relevance * avgScore +
weights.diversity * diversity +
weights.recency * recency +
weights.consolidation * consolidation +
weights.conceptual * conceptual +
phaseBoost; // light/REM 阶段的额外加成这个设计的意图很清楚:不是"偶然命中一次"就提升到长期记忆,而是要求一条记忆反复出现(frequency)、在不同上下文里出现(diversity)、跨多天出现(consolidation)、而且最近仍然活跃(recency)。
Rehydration:不信旧快照
这是整套设计里我最喜欢的细节。在真正写入 MEMORY.md 之前,系统不会直接用 recall store 里存的旧 snippet,而是回到源文件里重新读取:
async function rehydratePromotionCandidate(
workspaceDir: string,
candidate: PromotionCandidate,
): Promise<PromotionCandidate | null> {
const sourcePaths = resolveShortTermSourcePathCandidates(
workspaceDir, candidate.path);
for (const sourcePath of sourcePaths) {
let rawSource: string;
try {
rawSource = await fs.readFile(sourcePath, "utf-8");
} catch (err) {
if ((err as NodeJS.ErrnoException)?.code === "ENOENT") {
continue; // 文件已删除,跳过
}
throw err;
}
const lines = rawSource.split(/\r?\n/);
const relocated = relocateCandidateRange(lines, candidate);
if (!relocated) continue;
return { ...candidate, ...relocated };
}
return null; // 无法 rehydrate,放弃这条 candidate
}如果 daily note 已经改写过,promotion 会基于新文本。如果原始内容已经被删掉,这条 candidate 就不会被写进 MEMORY.md。short-term store 只是 evidence,不是最终 truth。
写入 MEMORY.md
通过所有检查的 candidate 最终被追加到 MEMORY.md,格式是这样的:
function buildPromotionSection(candidates, nowMs, timezone) {
const sectionDate = formatMemoryDreamingDay(nowMs, timezone);
const lines = [
"", `## Promoted From Short-Term Memory (${sectionDate})`, ""];
for (const candidate of candidates) {
const source =
`${candidate.path}:${candidate.startLine}-${candidate.endLine}`;
lines.push(
`<!-- openclaw-memory-promotion:${candidate.key} -->`);
lines.push(
`- ${snippet} [score=${candidate.score.toFixed(3)} recalls=...`);
}
return lines.join("\n");
}每条 promoted 记忆前面有一个 HTML comment 形式的 marker,用来防止重复写入。整个操作是 append-only 的,不会覆盖已有内容。
Dreaming 的价值
读完这套实现,我觉得有三个设计选择特别好。
第一,把"被搜到"和"值得长期保留"明确分开了。中间用 short-term recall store 作为缓冲层,一条记忆不会因为一次偶然命中就被写进长期记忆。它需要反复出现、在不同上下文里出现、而且经得起 rehydration 的验证。
第二,light 和 REM 扮演的是 reinforcement 角色,不是直接写 durable memory。系统可以有反思能力(REM 生成 reflections 和 candidate truths),但不会把所有反思都硬塞进 MEMORY.md。这些 phase 只负责给 deep phase 提供额外信号。
第三,不管系统变得多复杂,文件始终是 truth。recall store、phase signals、QMD 索引,这些都是派生结构。用户随时可以打开 MEMORY.md 看到 agent 记住了什么,也可以直接删掉或修改某一行。这种透明性在 agent 越来越自主的时代里很有价值。
局限
读完源码,我觉得这套记忆系统有三个比较明显的结构性问题。
第一,所有记忆操作都依赖 tool call。 OpenClaw 的记忆是通过 memory_search、memory_get 和文件编辑工具来读写的,不是通过 hook 自动注入的。这意味着每一次记忆交互都要消耗 token 和延迟:你告诉 agent 你的名字,它得调一次写文件的工具才能存下来;你问一个之前聊过的问题,它得先调 memory_search 才能拿到上下文。tool call 本身的开销(函数定义、参数、返回值)加起来并不便宜,而且整个流程变慢了。更实际的问题是,模型并不总是会在该用工具的时候用工具。有时候它会偷懒,直接凭 context 里已有的信息回答,跳过 memory_search,结果就是明明记忆里有相关信息但没被召回。记忆系统的可靠性上限被模型的 tool use 一致性卡住了。
第二,写入时对已有记忆是盲的。 Agent 往 MEMORY.md 写新内容的时候,并不会自动检查里面已经有什么。除非你显式要求它先读一遍整个文件,否则它会直接 append。结果就是容易写入冗余信息,不会主动更新旧条目,也不会把新旧信息合并。你一个月前说"这个项目是 in progress",今天说"已经收尾了",MEMORY.md 里会同时存在两条矛盾的记录。Dreaming 的 promotion 也是 append-only 的,不解决这个问题。
第三,它不会遗忘。 记忆会无限增长,过时的信息不会被自动清理。随着时间推移,MEMORY.md 里会积累越来越多不再准确的内容,搜索时这些旧信息和新信息一起被召回,反而降低了上下文质量。适度的遗忘机制对保持记忆的信噪比很重要,但目前 OpenClaw 没有这个能力。
References: