Back to Writing
Feb 20, 2026·中文·AI & Agent

OpenClaw 的记忆系统:从 Markdown 文件到 Dreaming

OpenClaw 最近很火。火的原因有很多,但我觉得其中一个蛮重要的因素是它的记忆系统。大多数 coding agent 跑完一次就忘了,下次你得重新解释一遍项目背景、架构偏好、之前踩过的坑。OpenClaw 不一样,它能跨 session 记住你是谁、你的项目长什么样、你之前做过什么决定,而且这些记忆是透明的,你随时能打开文件看到它记了什么。

我觉得可以从三个维度来理解这套系统:写入(Storage)、检索(Retrieval)、治理(Governance)。

写入:Markdown-first

OpenClaw 的记忆设计有一个非常明确的核心原则:Markdown 文件是 source of truth

具体来说:

打开这些文件,你就能看到 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.mdmemory/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 记录了 startLineendLinetexthash 和用于 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 关键词检索。

搜索时,两条路径并行执行,然后做加权合并:

// 关键词搜索(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-inHybrid(vector + BM25)Markdown 文件
QMD sidecarHybrid + rerank + query expansionMarkdown 文件
memory-lancedb纯 vectorMarkdown 文件
memory-lancedb-proHybrid + 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:

整个 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,累计 recallCounttotalScorequeryHashesrecallDaysconceptTags 等统计量。形成一本 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_searchmemory_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: