Suxiong

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`;
}

指南包含三部分信息:

  1. 文件路径(往哪里写)
  2. 写入时机(什么时候写)
  3. 格式约定(怎么写)

这是"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 始终不记

何时写入

三种触发条件:

  1. 显式表达:用户直接说出偏好

    • "我喜欢用 Zod 做校验"
    • "别用 class 组件"
  2. 重复纠正:用户多次纠正同一类行为

    • 连续三次要求"简洁点" → 记录"偏好简洁回复"
  3. 主动要求:用户明确要求记住

    • "记住这个项目用的是 pnpm"

何时不写入

三种应跳过的情况:

  1. 一次性指令:"这次用 JavaScript 写"(下次可能又要 TypeScript)

  2. 已存在信息:重复写入只会增加噪音

  3. 不确定时:先询问用户,不要擅自判断

格式约定

长期记忆采用结构化 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 架构做了三件事:

  1. 两层分离:长期记忆和日记忆物理隔离,解决时效性混淆问题
  2. 工具复用:通过 write_file 实现写入,零额外 API
  3. Prompt 驱动:通过写入指南让 Agent 自主学习,而非硬编码逻辑

核心理念:让 Agent 从"被动配置"走向"主动学习"。

On this page