AI 编程助手的一个根本问题是:每次会话都从零开始。 上一次你告诉它"我们项目用的是 pnpm 不是 npm",下次会话它又会用 npm。
Claude Code 的 Memory 系统解决了这个问题——它让模型能够跨会话记住用户的偏好、项目上下文、和工作模式。
~/.claude/
└─ projects/
└─ {project-hash}/
└─ memory/
├─ MEMORY.md # 入口文件(索引)
├─ user_role.md # 用户角色记忆
├─ feedback_testing.md # 反馈记忆
├─ project_auth.md # 项目记忆
└─ ... # 最多 200 个文件
每个记忆文件使用 frontmatter 格式:
---
name: 用户偏好-测试
description: 用户偏好使用真实数据库而非 mock 进行集成测试
type: feedback
---
集成测试必须使用真实数据库,不用 mock。
**Why:** 上季度 mock 测试通过但生产迁移失败的事故。
**How to apply:** 写测试时,配置连接到测试数据库而非 mock。MEMORY.md 是一个索引文件,不是记忆本身:
- [用户角色](user_role.md) — 高级后端工程师,熟悉 Go 和 React
- [测试偏好](feedback_testing.md) — 集成测试用真实数据库
- [项目认证](project_auth.md) — 认证重写因合规要求驱动关键限制:
- 最多 200 行(
MAX_ENTRYPOINT_LINES) - 最大 25KB(
MAX_ENTRYPOINT_BYTES) - 超出限制时截断并添加警告
用户输入 "帮我写测试"
│
├─ 1. 扫描记忆目录(memoryScan)
│ └─ 读取每个文件的 frontmatter(前30行)
│ └─ 按修改时间排序
│ └─ 最多 200 个文件
│
├─ 2. 构建记忆清单
│ └─ "[feedback] feedback_testing.md (3天前): 集成测试用真实数据库"
│ └─ "[user] user_role.md (7天前): 高级后端工程师"
│
├─ 3. 调用 Claude Sonnet 选择相关记忆
│ └─ 输入:用户消息 + 记忆清单 + 最近使用的工具
│ └─ 输出:JSON { selected_memories: ["feedback_testing.md", ...] }
│ └─ 最多选择 5 个
│
└─ 4. 读取选中的记忆文件全文
└─ 作为 AttachmentMessage 注入上下文
// findRelevantMemories.ts:39
async function findRelevantMemories({
query,
memoryDir,
recentlyUsedTools, // 最近使用的工具(抑制 API 文档类记忆)
alreadySurfacedPaths, // 已经展示过的记忆(避免重复)
}) {
// 排除 MEMORY.md(已在 system prompt 中)
// 排除已经通过 FileRead 读取过的记忆
// 排除本 session 已展示过的记忆
}工具感知过滤是一个巧妙的设计——如果用户刚用 FileRead 读了某个记忆文件,就不需要再通过 Memory 系统注入它。
const MAX_SESSION_BYTES = 60 * 1024 // 60KB 累计上限整个 session 中通过 Memory 注入的总字节数不超过 60KB。这防止了长会话中记忆累积占满上下文窗口。
// query.ts:301-304
using pendingMemoryPrefetch = startRelevantMemoryPrefetch(
state.messages,
state.toolUseContext,
)using 关键字确保了预取的生命周期管理:
- 启动:第一次循环迭代时
- 消费:工具执行后、下一轮 API 调用前
- 清理:generator 退出时自动 dispose(记录遥测数据)
循环迭代开始
│
├─ startRelevantMemoryPrefetch() → 异步启动
│ └─ 调用 findRelevantMemories()
│ └─ 记录 settledAt 时间戳
│
├─ 四层上下文压缩
├─ API 调用
├─ 工具执行
│
└─ getAttachmentMessages()
└─ 检查 pendingMemoryPrefetch 是否已完成
└─ 如果完成 → 创建 AttachmentMessage
└─ 如果未完成 → 跳过(不阻塞)
不阻塞是关键——记忆检索通常需要 1-3 秒(调用 Sonnet),如果阻塞主循环会影响响应速度。通过异步预取,记忆检索和模型调用/工具执行重叠执行。
// dispose 时记录
logEvent('tengu_memdir_prefetch_collected', {
settledAt: prefetch.settledAt,
consumedOnIteration: prefetch.consumedOnIteration,
// -1=从未消费, 0=隐藏(被过滤), N=在第N轮可见
})| 类型 | 用途 | 示例 |
|---|---|---|
user |
用户角色和偏好 | "高级后端工程师,熟悉 Go" |
feedback |
行为反馈(做/不做) | "不要 mock 数据库" |
project |
项目上下文和进展 | "认证重写因合规需求" |
reference |
外部资源指针 | "Bug 追踪在 Linear INGEST 项目" |
- 代码模式和架构(从代码中读取)
- Git 历史(从 git log 读取)
- 调试方案(修复已在代码中)
- CLAUDE.md 中已有的内容
- 当前会话的临时信息
会话进行中...
│
├─ 达到 token/工具调用阈值
│
├─ shouldExtractMemory() → true
│
├─ 启动后台子 agent(非阻塞)
│ └─ buildSessionMemoryUpdatePrompt()
│ └─ agent 分析对话,提取关键信息
│ └─ 写入/更新记忆文件
│
└─ markSessionMemoryInitialized()
└─ 后续按周期更新
Session Memory 不是一次性提取,而是渐进式的:
- 初始化:达到首次阈值后提取
- 增量更新:追踪
lastSummarizedMessageId,只处理新消息 - 周期触发:基于 token 增量和工具调用次数
~/.claude/memories/team/ # 团队记忆目录
├─ onboarding.md # 新人指南
├─ code_style.md # 代码风格
└─ deployment.md # 部署流程
Team Memory 存在单独的 team/ 目录下,通过同步机制在团队成员间共享。
当 Team Memory 启用时,system prompt 包含两层记忆:
个人记忆(~/.claude/memories/)
+ 团队记忆(~/.claude/memories/team/)
→ 合并后注入 system prompt
MEMORY.md 的内容在每次 turn 开始时作为 system prompt 的一部分注入:
// constants/prompts.ts:495
systemPromptSection('memory', () => loadMemoryPrompt())这是一个缓存友好的操作——因为 MEMORY.md 在会话期间通常不变,这个 section 可以被 prompt cache 复用。
findRelevantMemories() 找到的额外记忆通过 AttachmentMessage 动态注入:
System Prompt(包含 MEMORY.md 索引)
+ AttachmentMessage(memory: "feedback_testing.md 的完整内容")
→ 模型同时看到索引和具体记忆
Claude Code 的 Memory 系统是一个多层次、异步、智能的记忆架构:
- 存储层:文件系统 + frontmatter 格式,简单可靠
- 检索层:Sonnet 驱动的相关性匹配,比关键词搜索更准确
- 注入层:静态(MEMORY.md)+ 动态(AttachmentMessage),分级注入
- 提取层:Session Memory 自动提取,无需用户手动操作
- 共享层:Team Memory 支持团队知识传播
这套系统让 Claude Code 不再是一个"每次从零开始"的工具,而是一个越用越了解你的编程伙伴。