Skip to content

第12章 长期记忆:持久化与检索

"Memory is the treasury and guardian of all things." — Cicero

本章要点

  • 长期记忆让 Agent 跨会话保持连续性:记住用户、项目和过去的经验
  • 记忆类型:用户画像、反馈修正、项目状态、外部引用——各有不同的写入时机和使用场景
  • 存储方案:文件系统(Claude Code)、向量数据库(RAG)、键值存储(LangGraph Store)
  • 关键挑战:何时写、写什么、如何检索、如何防止记忆过时

12.1 为什么需要长期记忆

没有长期记忆的 Agent,每次对话都是"失忆"重启:

会话 1: 用户说"我是数据科学家,用 Python"
会话 2: Agent 问"请问你用什么语言?"
会话 3: 用户说"别用 mock 测试,上次出过事"
会话 4: Agent 又生成了 mock 测试

用户不得不反复教育 Agent 相同的事情。长期记忆解决的就是这个问题——让 Agent 跨会话积累对用户和项目的理解

12.2 记忆类型分类

Claude Code 的记忆系统定义了四种类型,这个分类法值得借鉴:

类型内容写入时机使用场景
user用户角色、偏好、知识水平了解到用户信息时调整交互风格和建议深度
feedback用户的纠正和认可用户说"不要这样做"或"就是这样"避免重复错误,保持好的做法
project项目动态、截止日期、决策了解到项目状态时理解任务背景和优先级
reference外部资源的指针发现外部信息源时知道去哪里找信息

什么不应该存入记忆

同样重要的是知道什么不该存:

  • 代码结构、文件路径——直接读代码更准确,记忆会过时
  • Git 历史——git log 是权威来源
  • 调试方案——修复已在代码中,commit message 有上下文
  • 临时任务状态——属于当前会话,不跨会话
  • CLAUDE.md 中已有的内容——避免重复

原则:如果能从代码或工具实时获取的信息,不要存入记忆。记忆只存那些无法从代码推断的人类知识。

12.3 存储方案

方案一:文件系统(Claude Code 的做法)

Claude Code 用纯 Markdown 文件存储记忆:

~/.claude/projects/{project-hash}/memory/
├── MEMORY.md              # 索引文件,列出所有记忆
├── user_role.md           # 用户画像
├── feedback_testing.md    # 测试偏好
├── project_deadline.md    # 项目截止日期
└── reference_linear.md    # 外部系统指针

每个记忆文件有 frontmatter:

markdown
---
name: testing-preferences
description: 用户要求集成测试用真实数据库,不用 mock
type: feedback
---

集成测试必须连接真实数据库,不使用 mock。
**Why:** 上季度 mock 测试通过但生产迁移失败,导致线上事故。
**How to apply:** 写测试时默认使用测试数据库连接,只在单元测试隔离纯逻辑时才 mock。

MEMORY.md 是索引,每行一条,控制在 200 行以内:

markdown
# Memory Index
- [Testing Preferences](feedback_testing.md) — 集成测试用真实数据库不用 mock
- [User Role](user_role.md) — 数据科学家,Python 为主,新接触前端

优势:

  • 人类可读可编辑
  • Git 友好,可版本控制
  • 无需额外基础设施
  • 索引文件轻量,每次对话加载成本低

劣势:

  • 语义搜索能力弱(只能关键词匹配)
  • 记忆多了索引文件膨胀
  • 并发写入需要处理

方案二:向量数据库

将记忆文本转为 embedding,存入向量数据库,检索时用语义相似度:

python
from langchain.vectorstores import Chroma
from langchain.embeddings import OpenAIEmbeddings

# 存储
memory_store = Chroma(
    collection_name="agent_memory",
    embedding_function=OpenAIEmbeddings()
)
memory_store.add_texts(
    texts=["用户偏好:集成测试不用 mock"],
    metadatas=[{"type": "feedback", "created": "2026-04-15"}]
)

# 检索
results = memory_store.similarity_search(
    "应该怎么写测试?",
    k=3
)

优势: 语义检索强大,能找到措辞不同但语义相关的记忆 劣势: 需要额外服务,embedding 有成本,检索结果可能不精确

方案三:键值存储(LangGraph Store)

LangGraph 的 Store 抽象提供了命名空间化的键值存储:

python
from langgraph.store.memory import InMemoryStore

store = InMemoryStore()

# 按命名空间组织
store.put(("user", "preferences"), "testing", {
    "value": "不使用 mock,连接真实数据库",
    "reason": "上季度 mock 导致生产事故"
})

# 检索
items = store.search(("user", "preferences"))

优势: 结构化,命名空间隔离,适合程序化访问 劣势: 不如文件系统直观,持久化需要额外配置

12.4 记忆的写入策略

何时写入

最关键的设计问题。写太多记忆会制造噪声,写太少则失去价值。

触发写入的信号:

强信号(立即写入):
- 用户明确说"记住这个"
- 用户纠正了 Agent 的行为("不要这样做")
- 用户确认了一个非显而易见的做法("对,就是这样")

中等信号(考虑写入):
- 了解到用户的角色或背景
- 发现项目的关键约束或截止日期
- 发现有用的外部资源

弱信号(通常不写入):
- 常规的任务执行过程
- 可以从代码推断的信息
- 临时性的调试状态

写入前的去重检查

typescript
async function saveMemory(memory: Memory): Promise<void> {
  // 1. 检查是否已有相似记忆
  const existing = await findSimilarMemory(memory)
  if (existing) {
    // 更新而非新建
    await updateMemory(existing.id, memory)
    return
  }

  // 2. 写入记忆文件
  await writeMemoryFile(memory)

  // 3. 更新索引
  await updateMemoryIndex(memory)
}

记忆的结构化格式

好的记忆不只是记录事实,还要记录原因应用方式

❌ 差的记忆:不要用 mock 测试
✅ 好的记忆:
  规则:集成测试必须连接真实数据库
  Why: 上季度 mock/生产不一致导致迁移失败
  How to apply: 写测试时默认用测试 DB,纯逻辑单元测试除外

有了"Why",Agent 在边界情况下可以做出合理判断——比如纯函数的单元测试不需要数据库,这不违反规则。

12.5 记忆的检索策略

每次新对话开始时,Agent 需要决定加载哪些记忆。

全量加载(小规模记忆)

Claude Code 的做法:每次加载完整的 MEMORY.md 索引文件。因为索引文件控制在 200 行以内,token 成本可接受。

typescript
// 每次会话开始时
const memoryIndex = await readFile('MEMORY.md')
systemPrompt += `\n# Memory\n${memoryIndex}`

需要详细信息时,再按需读取具体的记忆文件。

相关性检索(大规模记忆)

当记忆量超过索引文件能承载的范围时,需要按相关性检索:

python
def retrieve_relevant_memories(
    query: str,          # 当前用户消息
    context: dict,       # 当前上下文(项目、文件等)
    max_memories: int = 5
) -> list[Memory]:
    # 多路召回
    keyword_results = keyword_search(query, limit=10)
    semantic_results = vector_search(query, limit=10)
    recency_results = get_recent_memories(limit=5)

    # 合并去重
    candidates = merge_and_dedupe(
        keyword_results, semantic_results, recency_results
    )

    # 按相关性排序
    scored = [(m, score_relevance(m, query, context)) for m in candidates]
    scored.sort(key=lambda x: x[1], reverse=True)

    return [m for m, _ in scored[:max_memories]]

12.6 记忆的生命周期管理

记忆会过时。上个月的项目截止日期、已离职同事的职责、已重构的代码结构——这些记忆如果不清理,会误导 Agent。

时效性标记

markdown
---
name: release-freeze
type: project
created: 2026-04-10
---

代码冻结至 2026-04-15,不合并非关键 PR。
**Why:** 移动端团队在切发布分支。

过了 4 月 15 日,这条记忆就应该被标记为过期或删除。

验证后再使用

Claude Code 的规则:记忆中提到的文件路径、函数名、配置项,在推荐给用户之前必须先验证

"记忆说 X 文件存在" ≠ "X 文件现在存在"

记忆是某个时间点的快照。在据此行动之前,用 Glob/Grep/Read 验证当前状态。

定期清理

typescript
async function cleanupMemories(): Promise<void> {
  const memories = await listAllMemories()

  for (const memory of memories) {
    // 检查时效性
    if (memory.type === 'project' && isLikelyStale(memory)) {
      await archiveOrDelete(memory)
      continue
    }

    // 检查引用有效性
    if (memory.referencesFile && !await fileExists(memory.referencesFile)) {
      await markAsStale(memory)
    }
  }
}

12.7 隐私与安全

长期记忆必须处理敏感信息问题:

绝不存储:

  • API Key、密码、token
  • .env 文件的内容
  • 个人身份信息(PII)——除非用户明确要求

存储时注意:

  • 记忆文件不应提交到公开仓库
  • 路径选择:~/.claude/ 而非项目目录
  • 如果用云端存储,需要加密

Claude Code 的做法: 记忆存储在 ~/.claude/projects/{project-hash}/memory/ 目录下,项目路径被哈希处理,记忆文件不在项目目录内,不会被意外提交到 Git。

12.8 实践:构建文件记忆系统

一个最小可用的记忆系统实现:

typescript
interface Memory {
  name: string
  description: string  // 用于索引检索的一句话描述
  type: 'user' | 'feedback' | 'project' | 'reference'
  content: string      // 记忆正文
}

class FileMemoryStore {
  constructor(private dir: string) {}

  async save(memory: Memory): Promise<void> {
    const filename = `${memory.type}_${slugify(memory.name)}.md`
    const content = [
      '---',
      `name: ${memory.name}`,
      `description: ${memory.description}`,
      `type: ${memory.type}`,
      '---',
      '',
      memory.content,
    ].join('\n')

    await fs.writeFile(path.join(this.dir, filename), content)
    await this.updateIndex()
  }

  async loadIndex(): Promise<string> {
    const indexPath = path.join(this.dir, 'MEMORY.md')
    if (await fs.stat(indexPath).catch(() => null)) {
      return await fs.readFile(indexPath, 'utf-8')
    }
    return ''
  }

  async loadMemory(filename: string): Promise<Memory | null> {
    const content = await fs.readFile(
      path.join(this.dir, filename), 'utf-8'
    )
    return parseMemoryFile(content)
  }

  private async updateIndex(): Promise<void> {
    const files = await fs.readdir(this.dir)
    const entries: string[] = ['# Memory Index', '']

    for (const file of files.filter(f => f !== 'MEMORY.md')) {
      const memory = await this.loadMemory(file)
      if (memory) {
        entries.push(
          `- [${memory.name}](${file}) — ${memory.description}`
        )
      }
    }

    await fs.writeFile(
      path.join(this.dir, 'MEMORY.md'),
      entries.join('\n')
    )
  }
}

12.9 本章小结

长期记忆让 Agent 从"每次失忆的工具"进化为"持续学习的助手":

  1. 分类存储——用户、反馈、项目、引用四种类型,各有适用场景
  2. 选择性写入——只存无法从代码推断的人类知识
  3. 结构化格式——规则 + Why + How to apply,让 Agent 能处理边界情况
  4. 验证后使用——记忆是快照不是事实,行动前先验证
  5. 生命周期管理——记忆会过时,需要定期清理
  6. 隐私优先——永远不存储敏感信息

下一章我们将看看如何在多轮对话中管理会话状态,让 Agent 在复杂的交互流程中保持一致性。

基于 VitePress 构建