Appearance
第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 从"每次失忆的工具"进化为"持续学习的助手":
- 分类存储——用户、反馈、项目、引用四种类型,各有适用场景
- 选择性写入——只存无法从代码推断的人类知识
- 结构化格式——规则 + Why + How to apply,让 Agent 能处理边界情况
- 验证后使用——记忆是快照不是事实,行动前先验证
- 生命周期管理——记忆会过时,需要定期清理
- 隐私优先——永远不存储敏感信息
下一章我们将看看如何在多轮对话中管理会话状态,让 Agent 在复杂的交互流程中保持一致性。