记忆系统
Agent 系统作者:Claude Code 的记忆在源码里是 6 种文件类型 × 4 种内容类型 × 3 种 agent 持久化 scope——拆开看每一层的职责和实现。
记忆不是一层,是两套分类叠加
很多人把 “记忆” 理解成一个存储。Claude Code 的源码里是两套正交的分类体系叠加:
| 分类 | 源码位置 | 维度 |
|---|---|---|
| 记忆文件类型 (Memory File Type) | utils/memory/types.ts | 6 种 —— 按”谁写的、文件在哪”分 |
| 记忆内容类型 (Memory Content Type) | memdir/memoryTypes.ts | 4 种 —— 按”记的是什么信息”分(仅用于 AutoMem 里的单条记忆) |
两套分类各自回答不同问题,合起来描述一条记忆的完整身份。下面分别拆开。
第一套:6 种记忆文件类型
// utils/memory/types.ts
export const MEMORY_TYPE_VALUES = [
'User', // ~/.claude/CLAUDE.md —— 用户全局偏好
'Project', // <repo>/CLAUDE.md —— 项目规则,进 git
'Local', // CLAUDE.local.md —— 项目**私有**规则,不进 git
'Managed', // policy / 企业管理配置
'AutoMem', // auto-memory,跨对话持久化
...(feature('TEAMMEM') ? (['TeamMem'] as const) : []), // 团队共享记忆
] as const
每种类型在 system prompt 里有明确的描述后缀(utils/claudemd.ts 第 1169-1177 行),作为注入给模型的元信息:
| 类型 | 描述后缀 | 谁写 | 作用域 |
|---|---|---|---|
| User | "(user's private global instructions for all projects)" | 用户手写 | 跨 project,per user |
| Project | "(project instructions, checked into the codebase)" | 项目开发者手写 | per project,跨用户 |
| Local | "(user's private project instructions, not checked in)" | 用户手写,.gitignore | per project,per user(不共享) |
| Managed | (无特殊后缀——policy 层) | 企业 / 组织管理员 | 机器 / 组织级 |
| AutoMem | "(user's auto-memory, persists across conversations)" | Claude 自己维护 | per project |
| TeamMem | "(shared team memory, synced across the organization)" | 团队共享(feature-flagged TEAMMEM) | 组织级 |
为什么 6 种类型(不是随意分的):
- 谁有权写:User / Local / Project 是人手写的;AutoMem 是 Claude 写的;Managed 是 IT 管的;TeamMem 是团队同步的
- 是否进 git:Project 进,Local 不进——这是一条硬边界
- 可见性:Local 对同事不可见;Project 对所有人可见;User / AutoMem 对你自己可见
- 更新频率:User / Project 人手写慢;AutoMem 按轮次更新
给自研 agent 的启示:记忆”谁写的 × 作用域”是正交维度,合并成一层会让特殊需求无处安放。“企业强制规则”不该和”用户偏好”混在同一个文件。
加载顺序
getMemoryFiles() (utils/claudemd.ts 第 790 行起) 的加载顺序:
1. Managed(policy 级,始终第一)
2. Managed .claude/rules/*.md
3. User(如果 userSettings 启用)
4. User ~/.claude/rules/*.md
5. 从根目录向 CWD 走,每一层检查:
- CLAUDE.md (Project)
- .claude/CLAUDE.md (Project)
- .claude/rules/*.md (Project)
- CLAUDE.local.md (Local)
注意 .claude/rules/*.md 这个额外的规则文件目录——每层(Managed / User / Project / Local)都可以有自己的 rules 子目录。这是比单一 CLAUDE.md 更细粒度的规则组织方式,让你可以把不同主题(比如测试规则、安全规则、风格规则)分文件写。
Nested worktree 的特殊处理
源码里有一段针对嵌套在主仓库里的 worktree 的特殊分支(utils/claudemd.ts 第 868-884 行):
When running from a git worktree nested inside its main repo… Skip Project-type files from directories above the worktree but within the main repo — the worktree already has its own checkout.
场景:你从一个嵌套在主仓库里的 worktree 跑 Claude Code,向上走会踩到主仓库根目录,那里的 CLAUDE.md 会被重复加载。源码显式 skip。
Issue 引用:github.com/anthropics/claude-code/issues/29599——这是真实 bug 修复留下的补丁。这种细节通常只有跑在多 worktree workflow 的大型团队才会碰到,但一旦碰到就是血泪的排查。
单个 memory 文件的硬上限
// utils/claudemd.ts
export const MAX_MEMORY_CHARACTER_COUNT = 40000
单个 CLAUDE.md 或 memory 文件超过 40k 字符就被截断。40k 字符约 6-8k tokens——一个跑了几年、1000 人 contribute 的 repo 的 CLAUDE.md 容易撞到。
Memory 加载的关闭选项
CLAUDE_CODE_DISABLE_CLAUDE_MDS // 硬关所有 CLAUDE.md 加载
CLAUDE_CODE_DISABLE_AUTO_MEMORY // 只关 auto memory(见下面)
--bare / CLAUDE_CODE_SIMPLE // 跳过自动发现但保留 --add-dir
CLAUDE_CODE_REMOTE // 云端 resume,跳过 git status
autoMemoryEnabled (settings.json) // 项目级 opt-out
第二套:4 种记忆内容类型(AutoMem 内部)
AutoMem 里的每一条记忆都有一个 type,来自这个严格的 4 元集合:
// memdir/memoryTypes.ts
export const MEMORY_TYPES = ['user', 'feedback', 'project', 'reference'] as const
源码注释把设计意图写得很清楚:
Memories are constrained to four types capturing context NOT derivable from the current project state. Code patterns, architecture, git history, and file structure are derivable (via grep/git/CLAUDE.md) and should NOT be saved as memories.
核心原则:只记可派生之外的内容。代码结构能从 grep 得到、git 历史从 git log 得到、项目约定从 CLAUDE.md 得到——这些都不进记忆。记忆只存”不在其他地方,但未来有用”的信息。
4 种 type 各自的语义(源码 TYPES_SECTION_COMBINED 的完整描述):
| 类型 | 记什么 | 何时保存 | 何时使用 |
|---|---|---|---|
| user | 用户的角色、目标、职责、知识 | 学到用户偏好 / 背景时 | 工作需要贴合用户视角时 |
| feedback | 用户对做法的纠正或确认 | 用户纠正你、或肯定一个 non-obvious 做法 | 避免同一个纠正来第二次 |
| project | 谁在做什么、为什么、何时做 | 了解到 ongoing work / 约束 / deadline | 理解请求的动机 |
| reference | 外部系统的指针(Linear / Grafana / Slack 等) | 用户提到外部资源及其用途 | 用户引用外部系统时 |
为什么强类型而不是 free-form
不同类型老化速度不同——feedback 几乎永不过期(偏好稳定),project 变化快(任务完成就过期),reference 中等,user 稳定。强类型让 Claude 在读取时可以针对性判断”这条还生效吗”。
project 类型的特别提示:“relative dates in user messages must be converted to absolute dates when saving (e.g., ‘Thursday’ → ‘2026-03-05’)“——相对日期写进记忆后时间就漂了,源码里的记忆规则显式要求转绝对日期。
MEMORY.md 索引:源码级常量
AutoMem 的存储结构:
~/.claude/projects/<sanitized-git-root>/memory/
MEMORY.md # 索引文件(始终加载)
feedback_tool_truncation.md
user_role.md
project_current_initiative.md
...
索引文件的硬约束(memdir/memdir.ts):
export const ENTRYPOINT_NAME = 'MEMORY.md'
export const MAX_ENTRYPOINT_LINES = 200
export const MAX_ENTRYPOINT_BYTES = 25_000
两个上限同时生效——谁先破谁先触发截断:
- 200 行是第一道线——每条索引一行,意味着最多 200 条记忆摘要
- 25000 字节是第二道线,专门治”长行”场景
源码注释解释为什么双上限:
~125 chars/line at 200 lines. At p97 today; catches long-line indexes that slip past the line cap (p100 observed: 197KB under 200 lines).
p100 数据:有人的 MEMORY.md 只有 200 行但 197KB——意味着每行将近 1000 字符。单纯的行数上限抓不住这种 case,所以加了 byte cap。“125 chars/line”是 p97,意味着大多数人的索引是紧凑的。
触发截断时会追加一条警告消息(模型看得见):
WARNING: MEMORY.md is {reason}. Only part of it was loaded. Keep index entries
to one line under ~200 chars; move detail into topic files.
截断发生在 line cap 先,再做 byte cap;byte cap 用 lastIndexOf('\n', MAX_ENTRYPOINT_BYTES)在最近的换行处切,不从中间撕断。
给自研 agent 的启示:无界增长的索引必须配多维硬上限 + 用户可见的截断警告。单一上限(行数 OR 字节)都会漏 case。警告消息要教用户怎么修——“Move detail into topic files” 是 actionable 的建议。
两种 AutoMem 模式:Live Index vs. Daily Log(KAIROS)
默认模式下 Claude 维护 MEMORY.md 作为实时索引——每次要存记忆都读 MEMORY.md 看有没有相关条目、决定是更新还是新增。
但在 KAIROS feature flag 启用时(memdir/paths.ts 注释写的很清楚),运作模式完全不同:
Rather than maintaining MEMORY.md as a live index, the agent appends to a date-named log file as it works. A separate nightly /dream skill distills these logs into topic files + MEMORY.md.
KAIROS 模式的文件结构:
<autoMemPath>/logs/2026/04/2026-04-22.md # 今天的 append-only 日志
<autoMemPath>/MEMORY.md # 由 /dream 定期更新的索引
<autoMemPath>/{topic}.md # /dream 整理出的主题文件
核心思路:写入时便宜(append-only),整理由后台 job 做。自主 agent 每天跑 N 小时,不该在线路上花时间维护索引。
这和 journaling 的模式接近——write-ahead log + 后台 compaction。对自主 agent 是非常合适的模式。
Canonical git root:多 worktree 共享一个 memory
// memdir/paths.ts
function getAutoMemBase(): string {
return findCanonicalGitRoot(getProjectRoot()) ?? getProjectRoot()
}
findCanonicalGitRoot 的意思:同一个 repo 的所有 worktree 共享同一个 auto-memory 目录。
源码注释引用的 issue:anthropics/claude-code#24382——如果不这么做,每个 worktree 都有自己的 memory,从 worktree A 学到的偏好在 worktree B 里用不到。
给自研 agent 的启示:记忆的 project key 应该是 canonical 的。Git repo 的 “身份” 不是路径,是 canonical root(或者远端 URL)。早早把这个想清楚,后期不用做数据迁移。
AutoMem 的环境变量控制
Auto memory 有独立于 CLAUDE.md 的启用链(memdir/paths.ts 第 30-55 行):
Priority chain (first defined wins):
1. CLAUDE_CODE_DISABLE_AUTO_MEMORY env var (1/true → OFF, 0/false → ON)
2. CLAUDE_CODE_SIMPLE (--bare) → OFF
3. CCR without persistent storage → OFF (no CLAUDE_CODE_REMOTE_MEMORY_DIR)
4. autoMemoryEnabled in settings.json (supports project-level opt-out)
5. Default: enabled
5 层优先级,每层有对应的关停途径。用户、组织、云端、项目级都可以独立 opt-out——关闭路径和启用路径一样细致。
Subagent:上下文防火墙(源码视角)
Agent tool 的完整签名
源码 tools/AgentTool/AgentTool.tsx 第 82-102 行:
{
description: z.string(), // 3-5 词的任务描述
prompt: z.string(), // 实际任务
subagent_type: z.string().optional(),
model: z.enum(['sonnet', 'opus', 'haiku']).optional(), // 模型覆盖
run_in_background: z.boolean().optional(), // 后台模式
// 多 agent 参数:
name: z.string().optional(), // 可被 SendMessage 寻址的名字
team_name: z.string().optional(),
mode: permissionModeSchema().optional(), // permission mode 覆盖
// 隔离:
isolation: z.enum(['worktree', 'remote']).optional(), // worktree 临时仓库 / 远端 CCR
cwd: z.string().optional(), // 工作目录覆盖
}
比”subagent 就是隔离的执行”复杂得多——它是一个完整的 sub-Claude 配置系统:
model:子 agent 可以用更便宜的模型(例如 haiku),主 agent 继续用 opusisolation: 'worktree':在一个临时 git worktree 里跑——子 agent 的改动不污染主仓库,完成后可以 review 再 mergeisolation: 'remote'(ant-only):在云端 CCR 环境里跑——完全隔离的文件系统 / 网络run_in_background:后台跑,不阻塞主 agentmode:子 agent 可以有不同的 permission mode(例如子 agent 跑 plan mode,主 agent 在 default)name+ SendMessage:多 agent 间异步通信的原语
两种 subagent 路径
源码 AgentTool.tsx 第 622-633 行显示实际有两种执行路径:
// 默认路径:fresh context,不继承父对话
override: isForkPath ? {
systemPrompt: forkParentSystemPrompt // fork 路径:用父系统提示词
} : enhancedSystemPrompt ? {
systemPrompt: asSystemPrompt(enhancedSystemPrompt) // 默认:专属系统提示词
} : undefined,
// 只有 fork 路径才继承父对话历史:
forkContextMessages: isForkPath ? toolUseContext.messages : undefined,
两种路径:
- 默认(非 fork):子 agent 起一条全新的对话,有自己的 system prompt,看不到父对话历史。这就是”上下文防火墙”
- Fork 路径:子 agent 继承父对话的全部 messages + 父 system prompt——用于需要完整上下文的场景
大多数日常 Agent tool 调用走默认路径,fork 路径是特殊场景的安全阀。
Agent-level 持久化记忆(AgentMemoryScope)
Subagent 还有自己的持久化记忆,独立于主 agent 的 AutoMem(tools/AgentTool/agentMemory.ts):
export type AgentMemoryScope = 'user' | 'project' | 'local'
// - 'user' → ~/.claude/agent-memory/<agentType>/
// - 'project' → .claude/agent-memory/<agentType>/
// - 'local' → .claude/agent-memory-local/<agentType>/
每个 subagent type(比如 code-reviewer、database-migrator)都有自己的 3-scope 记忆目录。agent 之间记忆不串——一个 subagent 学到的偏好不会污染另一个 subagent。
给自研 agent 的启示:subagent 不只是”执行隔离”——它也应该是”学习隔离”。不同 subagent 处理不同领域,学到的经验各自沉淀,互不串门。
记忆使用的三条硬规则
源码里的 memory guidance(注入到 system prompt 里,作为 MEMORY_INSTRUCTION 的一部分)对 Claude 读记忆时的行为有三条硬规则:
1. Trust-but-verify
A memory that names a specific function, file, or flag is a claim that it existed when the memory was written. It may have been renamed, removed, or never merged. Before recommending it [verify].
记忆是时间点的快照,不是当下真相。引用具体标识符前必须 grep / read 验证。
2. 明确排除清单
What NOT to save:
- Code patterns, conventions, architecture (derivable from code)
- Git history, recent changes (
git logis authoritative)- Debugging solutions (the fix is in the code)
- Anything already in CLAUDE.md (no duplicates)
- Ephemeral conversation state (use TaskCreate instead)
告诉 agent “不要存什么” 和告诉它 “存什么” 同样重要——LLM 默认倾向于”能记就记”,不显式排除就会膨胀。
3. 不用就不读
When to access memories: When memories seem relevant, or the user references prior-conversation work. […] If the user says to ignore or not use memory: Do not apply remembered facts, cite, compare against, or mention memory content.
用户可以明确说”别用记忆”——agent 要能无视记忆层。这是用户主权的体现。
给自研 agent 的要点
- 记忆是两套分类叠加:记忆文件类型(谁写、在哪、作用域)+ 记忆内容类型(记的是什么)。合并会让特殊需求无处放
- User / Project / Local 是硬分层。“进不进 git”是硬边界,不同场景要分开存
- Managed / TeamMem 对企业场景是必需——policy 强制规则 vs 团队共享规则,个人偏好装不下
- MEMORY.md 索引要双上限:行数 + 字节。p100 数据会告诉你为什么 single cap 不够
- 强类型的内容分类(user / feedback / project / reference)让记忆可以按类型老化
- Project 类型必须保存为绝对日期——相对日期(“上周”、“下周四”)在记忆里会漂
- 记忆的 project key 是 canonical git root,不是路径——多 worktree / 软链接场景的必需设计
- AutoMem 的启用链要细致:env var / CLI flag / settings.json / 默认——5 层 opt-out 对生产环境是必须
- Subagent 是 “执行 + 学习” 双重隔离:默认隔离上下文,同时有自己的 scope 化持久记忆
- 读记忆前要 verify——把”先 grep 确认”做成 agent 记忆使用的硬规则,不然记忆变成幻觉源
- 显式排除清单:
What NOT to save和What to save同等重要,不写清楚 LLM 会膨胀 - 两种 MEMORY 模式(live index vs. daily log + nightly distill)对不同 agent 工作模式适用——自主 / 长时 agent 倾向后者
延伸阅读
- Claude Code 源码:
utils/memory/types.ts、utils/claudemd.ts、memdir/{memdir,paths,memoryTypes}.ts、tools/AgentTool/{AgentTool,agentMemory}.tsx - 记忆设计 (Memory Design)——跨产品的记忆理论背景
- 系统提示词组装——记忆层在 prompt 里的位置
- 上下文压缩——短期对话 vs 长期记忆的分层