光宇
24.33M · 2026-03-31
OpenClaw 是最近 AI 圈最火的一个开源项目,没有之一。
从去年的 Agent 年,到今年的 AI 个人助理,OpenClaw 和去年 Manus 一样的,爆到不行,而且还是开源的版本。
由于最近自己也在做 Agent,于是也看了 OpenClaw 的代码来了解其记忆系统的实现。有一些觉得可以借鉴学习的地方。
OpenClaw 的记忆系统其实比较简单:它把「记忆」拆成了文件、索引、召回注入。
在 Agent 工程里,记忆是一套能力组合:
OpenClaw 的实现路径非常「工程」:Markdown 作为事实源,SQLite 作为检索索引,toolResult 作为注入通道。
OpenClaw 的记忆从存储逻辑上来看可以分为三层:
会话记忆更像「trace」。我们不能指望它解决跨会话连续性,只用它做排障、复盘、抽取素材(写入短期/长期)。
短期记忆适合承接「会话压缩之前的落盘」和「跨几天的上下文连续性」。它不是最终事实源,别把它当永久协议文档。
MEMORY.md ==「可执行的组织记忆」。它的价值不在于「写得多」,在于「冲突少、可被召回、能约束后续行为」。这层要治理,要像维护配置一样维护。
快照文件主要是解决 /new、/reset 指令的断片
session-memory 的 Hook 会在你执行 /new 或 /reset 前,把上一会话最近 N 条对话(默认 15 条)抽出来,写成一个 Markdown 文件放到 workspace/memory/ 下。
这个机制对「人类工作流」很友好。很多团队的真实使用是:今天临时开了个话题,明天又忘了开在另一个会话里。会话快照能把碎片变成可检索素材,后面再沉淀进 MEMORY.md。
事实源是文本文件,索引是可重建的派生物。索引坏了你删掉重建就行;事实源坏了才是真的坏。
建索引我们关心的三件事:增量、去重、成本
OpenClaw 的索引构建不是「每次全量重算」,也不是「精细 diff」:
OpenClaw 会递归扫描 workspace/memory/ 下所有 .md,并包含 MEMORY.md/memory.md。对应定位是 src/memory/internal.ts:115-145。
文件 hash 的策略:
hash 是增量的核心。它的意义不止省时间,还省钱:embedding provider 往往是计费点。这种主要是对于使用第三方 embedding 的。
chunkMarkdown() 的策略:
tokens*4 这种近似在工程里挺常见,优点是简单、稳定、跨模型大差不差。缺点也明显:
可以盯两个指标来调 chunking:
去重靠 id,chunk 写入策略如下:
这里有个很实际的 trade-off:
另外,id 里带了 provider.model,这会带来一个工程后果:embedding 模型换了,chunk id 会变,索引层面等价于全量重建。 provider/model/providerKey 变化会触发 full reindex。
embedding 缓存的主键设计:
这是「上线能用」的关键。否则就会遇到一个很尴尬的情况: 索引更新频繁触发 embedding 重算 → 延迟抖动 → API 费暴涨 → 还可能被 provider 限流。 system prompt 的 skills 段落甚至提醒「假设有 rate limits,避免 tight loop」,这就有点被打过之后写进规范的味道。
watch、interval、onSearch、session-delta
索引更新如果做得「过勤」,会把 CPU 和 IO 吃满;做得「过懒」,召回就是过期的。OpenClaw 在触发上给了多条路径:
还有两点:
主要看 buildMemorySection() 的代码,因为它把策略写死了:
提示词:
function buildSkillsSection(params: { skillsPrompt?: string; readToolName: string }) {
const trimmed = params.skillsPrompt?.trim();
if (!trimmed) {
return [];
}
return [
"## Skills (mandatory)",
"Before replying: scan <available_skills> <description> entries.",
`- If exactly one skill clearly applies: read its SKILL.md at <location> with `${params.readToolName}`, then follow it.`,
"- If multiple could apply: choose the most specific one, then read/follow it.",
"- If none clearly apply: do not read any SKILL.md.",
"Constraints: never read more than one skill up front; only read after selecting.",
"- When a skill drives external API writes, assume rate limits: prefer fewer larger writes, avoid tight one-item loops, serialize bursts when possible, and respect 429/Retry-After.",
trimmed,
"",
];
}
function buildMemorySection(params: {
isMinimal: boolean;
availableTools: Set<string>;
citationsMode?: MemoryCitationsMode;
}) {
if (params.isMinimal) {
return [];
}
if (!params.availableTools.has("memory_search") && !params.availableTools.has("memory_get")) {
return [];
}
const lines = [
"## Memory Recall",
"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. If low confidence after search, say you checked.",
];
if (params.citationsMode === "off") {
lines.push(
"Citations are disabled: do not mention file paths or line numbers in replies unless the user explicitly asks.",
);
} else {
lines.push(
"Citations: include Source: <path#line> when it helps the user verify memory snippets.",
);
}
lines.push("");
return lines;
}
工具描述:
label: "Memory Search",
name: "memory_search",
description:
"Mandatory recall step: semantically search MEMORY.md + memory/*.md (and optional session transcripts) before answering questions about prior work, decisions, dates, people, preferences, or todos; returns top snippets with path + lines. If response has disabled=true, memory retrieval is unavailable and should be surfaced to the user.",
parameters: MemorySearchSchema,
有三点:
两阶段召回比「一次性把相关文件 wholefile 塞进去」靠谱太多。
toolResult 消息是关键通道
很多人以为「记忆」是把内容写进 system prompt。OpenClaw 不是这么干的。
召回结果会以工具执行结果的形式进入会话消息列表,后续模型调用自然「看得到」。
这条通道对排障非常友好:我们可以在 transcript 里看到「这次回答之前它到底召回了什么」。而且 toolResult 天然可控预算、可控格式,比让模型把记忆揉进自由文本稳得多。
人为触发的「切会话」
当执行 /new、/reset 时,上一段会话尾部会被抽取成 YYYY-MM-DD-.md。这是「防断片」写入,价值是保住最近上下文。
坑:
我们的做法:
「需要我们调教,告诉她哪些重要」。
短期记忆要走「稀疏高密度」路线:条目少,但每条都能在未来的某个问题上直接复用。写入策略要围绕「将来会搜什么」来定,不要围绕「当下发生了什么」来记流水账。
心跳 / AGENTS.md / cron
三种更新机制:心跳、核心流程、cron。
读最近几天短期记忆 → 选值得长期记住的 → 提炼写入 MEMORY.md。
观点:
一般做法:
把记忆当成协议,不当成日记
「告诉她哪些重要」。把「重要」拆成几类,每类有明确写入规则,避免模型自由发挥。
一般的规则:
短期记忆(memory/YYYY-MM-DD.md)会允许更多过程性信息,但要满足一个条件:未来能被搜索问题命中。比如你写「今天讨论了 A」,基本没用;你写「决定 A 的原因是 B,后续若出现 C 用 D 回滚」,会有用一些。
如果希望 memory_search 在关键时刻召回到正确内容,就得用「未来的查询语句」来写记忆。
以上。