140行代码实现 Open Claw 的 Memory 架构
本文介绍一种极简的 Memory 架构:基于 Markdown 文件的两层记忆模型。代码约 140 行,无外部依赖。
引子
Context 解决"Agent 知道什么",Memory 解决"Agent 如何学习"。
LLM 是无状态的。每次 API 调用都是独立的,模型不记得上一轮对话说了什么。要让 Agent 表现出记忆能力,需要一套持久化机制。
本文介绍一种极简的 Memory 架构:基于 Markdown 文件的两层记忆模型。代码约 140 行,无外部依赖。
Memory 的独特挑战
Memory 和静态配置有本质区别:配置是人类写好让 Agent 读取,Memory 是 Agent 在对话中自己写入。
这带来三个设计目标:
| 目标 | 含义 |
|---|---|
| 自主性 | Agent 判断什么值得记住,不依赖用户显式指令 |
| 时效性 | 区分持久信息和临时笔记,避免混淆 |
| 一致性 | 多轮对话中的数据同步,避免读写冲突 |
同时有两个约束:
- 无外部依赖:不引入数据库、向量存储、Redis 等基础设施
- 人机可编辑:用户能直接打开文件修改记忆内容
两层记忆模型
长期记忆
文件路径:memory/MEMORY.md
存储内容:用户画像、持久偏好、项目上下文。
# User Preferences
## 关于用户
- 名称:苏雄
- 角色:资深前端工程师
- 技术栈:TypeScript、React、Node.js、Bun
- 偏好:简洁方案,关注架构与可维护性写入时机:
- 用户明确表达偏好("我喜欢用 Zod 做校验")
- 用户反复纠正同一类行为(多次要求简洁 → 记录"偏好简洁")
- 用户主动要求记住某件事
生命周期:跨会话持久,只追加不删除(除非用户明确要求)。
日记忆
文件路径:memory/YYYY-MM-DD.md(如 memory/2026-02-15.md)
存储内容:当日任务、临时上下文、会话笔记。
# 2026-02-15 日记
## 今日任务
- [ ] 完成登录功能
- [x] 重构 memory 模块写入时机:任务记录、临时决策、待办事项。
生命周期:按日滚动。今天的笔记不会污染昨天的文件。支持回溯查看历史。
为什么是两层
单层记忆的问题:长期记忆被临时信息污染。
如果把"今天要完成登录功能"和"用户偏好 TypeScript"放在同一个文件里,几天后"今天"就失去意义了。时效性不同的信息需要物理隔离。
两层模型的类比:
- 长期记忆 = 档案室,存放持久有效的信息
- 日记忆 = 草稿本,记录当天的临时笔记
Agent 自主写入机制
工具复用
Memory 系统不新增专门的 API。Agent 通过已有的 write_file 工具写入记忆。
// Agent 调用示例
write_file((path = "memory/MEMORY.md"), (content = "..."));
write_file((path = "memory/2026-02-15.md"), (content = "..."));优势:
- 零额外实现。Agent 本身就有文件操作能力。
- 统一心智模型。对 Agent 来说,写记忆就是写文件,无需学习新概念。
写入指南注入
Agent 怎么知道自己有记忆能力?通过 system prompt 注入写入指南。
getMemoryGuide(): string {
return `## Memory System
You have a two-layer memory system:
- **Long-term memory**: \`${this.memoryFile}\`
- Store: user profile, preferences, important context
- Use \`write_file\` to append/update
- **Daily notes**: \`${this.todayFile()}\`
- Store: today's tasks, temporary context, session notes
- Use \`write_file\` to record
**When to remember:**
- User shares personal info (name, preferences, habits)
- Important decisions or context worth persisting
- Tasks or notes for today`;
}指南包含三部分信息:
- 文件路径(往哪里写)
- 写入时机(什么时候写)
- 格式约定(怎么写)
这是"Prompt 驱动行为"的典型应用:不通过代码硬编码逻辑,而是通过自然语言指令让 Agent 自主执行。
无状态读取
每轮对话从磁盘重新加载记忆内容:
async getMemoryContext(): Promise<string | null> {
const [longTerm, today] = await Promise.all([
this.readLongTerm(),
this.readToday(),
]);
const parts: string[] = [];
if (longTerm) parts.push(`## Long-term Memory\n\n${longTerm}`);
if (today) parts.push(`## Today's Notes (${this.todayStr()})\n\n${today}`);
return parts.length ? parts.join("\n\n---\n\n") : null;
}为什么不在内存中缓存?
Memory 文件可能被两方修改:Agent 通过 write_file 写入,用户直接编辑文件。如果在内存中缓存,会出现数据不一致。
无状态读取的代价是每轮多一次磁盘 IO。对于本地文件系统,这个开销可以忽略。
学习规则设计
Agent 需要规则来判断什么值得记住。没有规则会导致两个极端:
- 过度记忆:把所有对话内容都存下来,噪音淹没关键信息
- 遗漏记忆:用户反复重复同一偏好,Agent 始终不记
何时写入
三种触发条件:
-
显式表达:用户直接说出偏好
- "我喜欢用 Zod 做校验"
- "别用 class 组件"
-
重复纠正:用户多次纠正同一类行为
- 连续三次要求"简洁点" → 记录"偏好简洁回复"
-
主动要求:用户明确要求记住
- "记住这个项目用的是 pnpm"
何时不写入
三种应跳过的情况:
-
一次性指令:"这次用 JavaScript 写"(下次可能又要 TypeScript)
-
已存在信息:重复写入只会增加噪音
-
不确定时:先询问用户,不要擅自判断
格式约定
长期记忆采用结构化 Markdown:
## 关于用户
- 名称:...
- 角色:...
- 技术栈:...
## 偏好
- ...
## 项目上下文
- ...约定原则:
- 一行一条:便于追加和去重,避免复杂解析
- 只追加不删除:除非用户明确要求移除
- 去重合并:语义重复的信息更新已有条目而非新增
实现细节
MemoryStore 核心接口
完整实现约 140 行,核心接口:
| 方法 | 作用 |
|---|---|
ensureDir() | 确保 memory 目录存在 |
getMemoryContext() | 合并长期记忆 + 今日记忆 |
readLongTerm() | 读取长期记忆 |
readToday() | 读取今日记忆 |
getRecentMemories(n) | 回溯最近 N 天的记忆 |
getMemoryGuide() | 生成写入指南 |
日期滚动
日记忆文件按日期命名:
private todayFile(): string {
return resolve(this.memoryDir, `${this.todayStr()}.md`);
}
private todayStr(): string {
return new Date().toISOString().slice(0, 10); // YYYY-MM-DD
}效果:
- 2026-02-15 的对话写入
memory/2026-02-15.md - 2026-02-16 的对话写入
memory/2026-02-16.md - 自动隔离,无需手动清理
回溯能力
支持查看最近 N 天的记忆:
async getRecentMemories(days: number = 7): Promise<string | null> {
const memories: string[] = [];
const today = new Date();
for (let i = 0; i < days; i++) {
const date = new Date(today);
date.setDate(date.getDate() - i);
const dateStr = this.formatDate(date);
const content = await this.safeRead(
resolve(this.memoryDir, `${dateStr}.md`)
);
if (content) {
memories.push(`### ${dateStr}\n\n${content}`);
}
}
return memories.length ? memories.join("\n\n---\n\n") : null;
}用途:需要回顾近期上下文时,可以加载最近一周的日记忆。
容错处理
文件不存在时静默返回 null:
private async safeRead(path: string): Promise<string | null> {
try {
const content = await readFile(path, "utf-8");
return content.trim() || null;
} catch {
return null;
}
}这意味着:
- 新用户第一次使用时,记忆文件不存在,系统正常运行
- 用户删除某个记忆文件,不会导致崩溃
局限与演进
当前局限
无语义检索:记忆内容全文注入 system prompt。当记忆量增长后,会占用大量 token。无法按相关性检索。
无压缩:长期记忆只追加不删除,文件会无限增长。缺少自动摘要或清理机制。
无冲突处理:Agent 和用户可能同时编辑同一文件。当前实现没有锁机制。
演进方向
记忆摘要:定期对旧记忆做摘要压缩,保留关键信息,释放 token 空间。
向量化:将记忆内容向量化存储,支持语义相似度检索。按相关性加载,而非全量注入。
遗忘机制:自动识别过时信息(如"今天要做 X"在一周后已无意义),主动清理。
小结
本文介绍的 Memory 架构做了三件事:
- 两层分离:长期记忆和日记忆物理隔离,解决时效性混淆问题
- 工具复用:通过
write_file实现写入,零额外 API - Prompt 驱动:通过写入指南让 Agent 自主学习,而非硬编码逻辑
核心理念:让 Agent 从"被动配置"走向"主动学习"。