浅尝claude code记忆系统

Claude Code 记忆系统深度解析

基于源码分析:src/memdir/ · src/services/extractMemories/ · src/utils/attachments.ts · src/query/stopHooks.ts


一、从两种维度看记忆类型

Claude Code 的记忆系统由两个维度构成,理解这两个维度是理解整个系统的关键。

维度 A:内容语义类型(记忆"说的是什么")

类型 含义 衰减速度
user 用户角色、目标、背景知识 极慢,几乎永不过期
feedback 对 Claude 行为的纠正或认可 慢,偏好稳定
project 进行中工作、决策、里程碑、截止日期 ,源码注释写了 "decay fast"
reference 外部系统资源指针 中,外部系统相对稳定

维度 B:存储范围类型(记忆"存在哪里")

前三种属于 CLAUDE.md 指令体系 (用户手写规则),后三种属于 AutoMem 体系(Fork Agent 自动维护)。

类型 体系 存储路径 跨项目 进版本控制
User CLAUDE.md ~/.claude/CLAUDE.md
Project CLAUDE.md <project-root>/CLAUDE.md.claude/CLAUDE.md
Local CLAUDE.md .claude/CLAUDE.md.local
Managed CLAUDE.md 外部注入(frontmatter 标记) --- ---
AutoMem AutoMem ~/.claude/projects/<hash>/memory/
TeamMem AutoMem ~/.claude/projects/<hash>/memory/team/ ✗(同步)

两个维度的关系

维度 A(内容语义)× 维度 B(存储范围)是正交的两个轴。AutoMem 是四种内容类型的主要载体 ------Fork Agent 提取记忆时,根据内容语义将其分类写入对应 topic 文件,文件名即反映类型(如 user_role.mdfeedback_testing.md)。

写入决策逻辑(Fork Agent 内部):

  1. 判断内容语义类型(user / feedback / project / reference)
  2. 判断作用域:user 类型始终写私有 AutoMem;其余类型若属团队共识则写 TeamMem,否则写私有 AutoMem
  3. 写入对应 topic 文件 + 更新同目录的 MEMORY.md 索引

二、完整闭环:一次对话的生命周期

先看完整时序图,再逐阶段展开。

sequenceDiagram participant 用户 participant Claude主线程 participant SystemPrompt participant SonnetRanker participant MemoryFS participant ForkAgent Note over 用户,ForkAgent: ── 会话 A 启动 ── Claude主线程->>SystemPrompt: 请求加载跨会话记忆摘要 SystemPrompt->>MemoryFS: 读取记忆索引(≤200行/25KB) MemoryFS-->>SystemPrompt: 返回 AutoMem 摘要索引 SystemPrompt-->>Claude主线程: 将长期记忆注入 System Prompt(每次必加载) Claude主线程->>SystemPrompt: 请求加载用户指令规则 SystemPrompt->>MemoryFS: 读取 CLAUDE.md / rules/*.md(从当前目录向上遍历) MemoryFS-->>SystemPrompt: 返回用户手写的行为约束规则 SystemPrompt-->>Claude主线程: 将用户指令注入 User Context Note over 用户,ForkAgent: ── 用户发消息 ── 用户->>Claude主线程: 发送 prompt par 并发执行(不阻塞主对话响应) Claude主线程->>SonnetRanker: 异步启动相关记忆预取(不等结果) SonnetRanker->>MemoryFS: 轻量扫描所有记忆文件元数据(只读前30行) MemoryFS-->>SonnetRanker: 返回文件名、描述、修改时间清单 Note over SonnetRanker: 去除本轮已注入过的文件(防重复) SonnetRanker->>SonnetRanker: 语义评分,精选最相关的 ≤5 个文件 SonnetRanker->>MemoryFS: 读取选中文件完整内容(≤200行/4KB) MemoryFS-->>SonnetRanker: 返回文件内容(超1天附加过期提醒) and Claude主线程->>Claude主线程: 执行工具调用(Bash/Read/Edit...) end SonnetRanker-->>Claude主线程: 将相关记忆作为上下文注入本轮推理 Note over 用户,ForkAgent: ── Claude 推理(含记忆上下文)── Claude主线程->>Claude主线程: 结合记忆与工具结果生成响应 Claude主线程-->>用户: 输出响应 Note over 用户,ForkAgent: ── 对话结束,异步触发记忆沉淀 ── Claude主线程->>ForkAgent: 对话结束后异步派生子 Agent 提取记忆 Note over ForkAgent: fire-and-forget,主线程不等待 ForkAgent->>MemoryFS: 扫描已有记忆文件,获取现有内容清单 MemoryFS-->>ForkAgent: 返回已有 topic 文件列表(防止重复创建) ForkAgent->>ForkAgent: 分析新增消息,按内容语义分类 ForkAgent->>MemoryFS: 将新记忆写入对应分类文件 ForkAgent->>MemoryFS: 更新记忆索引,保持摘要最新 Note over ForkAgent: 推进游标,下次只处理新增消息 Note over 用户,ForkAgent: ── 会话 B 启动(闭环完成)── Claude主线程->>SystemPrompt: 请求加载跨会话记忆摘要 SystemPrompt->>MemoryFS: 读取已含上轮提取结果的记忆索引 MemoryFS-->>SystemPrompt: 返回最新记忆索引(包含上轮对话产生的新记忆) SystemPrompt-->>Claude主线程: 注入 System Prompt → 循环往复

各阶段设计意图

① 会话启动:为什么要同时加载两套内容?

MEMORY.md(AutoMem)和 CLAUDE.md 是两套完全独立的体系,分别承载不同的职责:前者是 Claude 自己从历史对话中学到的东西,后者是用户主动告诉 Claude 的规则。两者都在会话启动时全量加载,是因为它们都属于"无论聊什么都应该知道的背景"------不做相关性筛选,直接进 System Prompt。

② 记忆预取:为什么不等预取完成再开始推理?

预取(prefetch)和工具执行是并发的。Claude 收到用户消息后,立刻启动记忆预取,同时开始执行工具调用。工具调用本身有 I/O 等待时间,预取就在这段时间里悄悄完成。等到第二轮 API 请求时,记忆已经就绪,可以一起发给模型。这样用户感知到的延迟只有推理本身,记忆检索的耗时被完全隐藏在工具执行的等待里。

③ 语义评分:为什么 Sonnet 只看 description 而不看文件内容?

扫描阶段只读每个文件的前 30 行(frontmatter),把所有文件的 description 字段汇总成一个清单,一次 sideQuery 就完成选择。如果让 Sonnet 看完整文件内容,就需要先把所有文件都读进来,既慢又占 token。description 字段正是为此设计的------写记忆时要求 description 精准描述内容,让评分阶段能在不读正文的情况下准确判断相关性。

④ 记忆注入时机:为什么记忆是在两次 API 请求之间注入的?

Claude Code 的核心是一个 while(true) 循环,每次循环对应一次 API 请求。推理(thinking)和响应(response)是同一次 API 请求里流式输出的两个 block,无法在中间插入新内容。因此记忆只能在两次请求之间注入------第一轮请求触发工具调用,工具执行期间预取完成,第二轮请求时记忆和工具结果一起发给模型。对于没有工具调用的简单问答,第一轮直接 break,可能拿不到记忆。

⑤ 记忆提取:为什么用 fire-and-forget 而不是等待结果?

提取记忆只需要副作用(写文件),不需要返回值。用户已经拿到响应,主线程没有理由继续等待。fire-and-forget 让主线程立刻释放,提取在后台异步完成。同时,系统内置了互斥和排队机制:若主 Agent 本轮已自行写过记忆,Fork Agent 跳过;若上一次提取仍在运行,新请求进入等待队列,完成后触发 trailing run。这样既不阻塞用户,也不会产生并发写冲突。

⑥ 游标机制:为什么要记录上次提取到哪条消息?

每次对话结束后,Fork Agent 只需要分析"上次提取之后新增的消息",而不是重新扫描整个对话历史。lastMemoryMessageUuid 游标记录了上次提取的边界,确保增量处理。没有游标,每次提取都要重新分析全部历史,既浪费 token,又可能产生重复记忆。

§1 会话启动 --- System Prompt 构建(同步阻塞)

scss 复制代码
Claude 主线程
  ├─ loadMemoryPrompt()  → 读取 MEMORY.md(≤200行/25KB)→ 注入 System Prompt
  └─ getMemoryFiles()    → 读取 CLAUDE.md / rules/*.md(cwd 向上遍历)→ 注入 User Context

两个函数的本质区别:

loadMemoryPrompt() getMemoryFiles()
读取内容 ~/.claude/.../MEMORY.md(AutoMem 摘要索引) CLAUDE.md / rules/*.md(项目目录树)
谁写入 Extract Agent 自动维护 用户手动编辑
一句话 Claude 自己记住的东西 用户告诉 Claude 的规则

§2 用户发消息 --- 相关记忆并发预取

用户发消息后,startRelevantMemoryPrefetch() 立即并发启动,不阻塞主对话响应

函数名拆解:

  • start --- 立即启动,不等结果
  • Relevant --- 按相关性筛选,不是全量加载
  • Prefetch --- 预取,利用工具执行的 I/O 等待时间

6 步检索流程:

  1. 扫描 scanMemoryFiles():递归读取 memory 目录所有 .md,每文件只读前 30 行(frontmatter),按 mtime 倒序,最多 200 个
  2. 过滤 :去除本 session 已注入过的文件(alreadySurfaced 去重)
  3. 格式化清单 formatMemoryManifest():每行格式 - [type] filename (时间戳): description
  4. Sonnet 评分 sideQuery():独立侧边请求,Sonnet 看到的只有文件名和 description,看不到文件内容 ,输出 JSON {"selected_memories": [...]} 最多选 5 个
  5. 防幻觉校验 :返回的文件名与 validFilenames 做交集,过滤掉不存在的文件名
  6. 读内容:每文件 ≤200 行且 ≤4KB,mtime > 1 天附加过期警告

为什么评分只用 description 而不用文件内容?

性能权衡:扫描阶段只读 frontmatter,把所有文件的 description 汇总成一个清单发给 Sonnet,一次 sideQuery 就完成选择。description 字段正是为此设计的------写记忆时要求 description 精准描述内容,让评分阶段能准确判断相关性。

各层限制:

限制项 数值
每轮最多召回文件数 5 个
每个文件最大行数 200 行
每个文件最大字节 4 KB
每轮最大注入量 ≈20 KB
session 累计上限 60 KB
扫描文件上限 200 个

§3 推理 --- 记忆注入时机

记忆以 relevant_memories attachment 形式追加为 <system-reminder>,在用户消息之前呈现给 Claude。

关键细节:记忆注入发生在两次 API 请求之间,不是推理和响应之间。

Claude Code 的核心是一个 while (true) 循环:

轮次 记忆状态
第 1 轮(用户发消息,prefetch 刚启动) 无记忆
工具执行期间(prefetch 在后台完成) 注入记忆
第 2 轮(工具结果 + 记忆一起发给模型) 有记忆
无工具的简单问答(第 1 轮直接 break) 可能无记忆

推理(thinking)和响应(response)是同一次 API 请求里流式输出的两个 block,无法在中间插入新内容

§4 对话结束 --- Stop Hook 记忆提取(fire-and-forget)

两套独立的提取机制并行运行:

Auto Memory 提取 executeExtractMemories()

  • 每轮对话结束后,handleStopHooks() 无条件触发
  • 互斥:若主 Agent 本轮已自行写过记忆文件,Fork Agent 跳过
  • 并发保护:若上一次提取仍在运行,新请求 stash 到 pendingContext,完成后触发 trailing run

Session Memory 提取 extractSessionMemory()

  • 触发条件:context window 累计 token ≥ 10,000(首次),之后每增长 ≥ 5,000 token 且满足工具调用或对话断点条件
  • 用途:结构化会话摘要,用于 Compact 替代

Fork Agent 提取流程(5 步):

  1. 预扫描现有 topic 文件,生成 manifest 注入提示词(避免重复创建文件)
  2. 构建提示词(含新消息数量、已有记忆清单、四类内容定义、写入规则)
  3. runForkedAgent(maxTurns=5):与主对话共享 System Prompt 前缀(命中 Prompt Cache),工具权限受限(只读 + 仅限记忆目录的写入)
  4. 写入 topic 文件(user_*.md / feedback_*.md / project_*.md / reference_*.md)+ 更新 MEMORY.md 索引
  5. 推进游标 lastMemoryMessageUuid,下次提取只处理新增消息

三、文件格式详解

指令类(CLAUDE.md 体系)--- 纯 Markdown,用户手写

markdown 复制代码
# ~/.claude/CLAUDE.md
不要使用 any 类型。
永远先写测试。
回复用中文。
@./docs/coding-standards.md

支持 @include 引用其他文件,从 cwd 向上遍历目录树,越近优先级越高。

AutoMem 类(topic 文件体系)--- 带 frontmatter,自动维护

topic 文件格式:

markdown 复制代码
---
name: 回复风格偏好
description: 不要在回复末尾总结刚做了什么
type: feedback
---

不要在回复末尾用"总结"段落重复已完成的操作。

**Why:** 用户说"我能看到 diff,不需要你再重复"
**How to apply:** 所有回复结尾直接停,不加总结段

MEMORY.md 索引格式:

markdown 复制代码
- [用户角色](user_role.md) --- 研发架构师,侧重系统设计
- [回复风格](feedback_style.md) --- 不要在末尾总结
- [项目状态](project_auth.md) --- auth 重构由合规驱动

两套体系对比:

维度 指令类(CLAUDE.md AutoMem 类(topic 文件)
文件格式 纯 Markdown,无 frontmatter 带 YAML frontmatter
谁写 用户手写 Extract Agent 自动生成
加载时机 每次会话启动,全量加载 MEMORY.md 必加载;topic 文件按相关性每轮最多召回 5 个
跨项目 User 类全局生效;Project/Local 限当前项目 始终项目隔离

四、检索 vs 提取:并发机制对比

维度 检索记忆(Retrieval) 提取记忆(Extraction)
触发点 用户发消息时 对话结束时
并发机制 Disposable 对象 + 非阻塞轮询 fire-and-forget Promise
取消机制 [Symbol.dispose]() 显式中止 feature gate 控制
错误处理 .catch() 返回空数组 错误被吞掉,不影响主对话
何时完成 工具执行期间 对话结束后

检索为什么非阻塞轮询?

  • 不等 prefetch 完成就开始推理 → 响应延迟最小
  • 工具执行期间 prefetch 在后台完成 → 利用 I/O 等待时间
  • 多轮循环给 prefetch 多次机会 → 最终一定会消费到

提取为什么 fire-and-forget?

  • 不需要结果,只需要副作用(写文件)
  • 节流 + 互斥 + 排队 → 防止重复和冲突

为什么用 Fork Agent 而不是 sideQuery?

  • sideQuery:轻量级评分,Sonnet 够用
  • Fork Agent:完整推理 + 执行,需要 Claude 的完整能力 + 工具权限
  • Fork 共享 prompt cache → 复用主线程的缓存,加速且省 token

五、新鲜度机制

文件年龄 处理
≤ 1 天 直接注入,无提醒
> 1 天 附加:"This memory is X days old. Memories are point-in-time --- verify against current code before asserting as fact."

六、存储路径速查

文件 路径 何时读取 何时写入
MEMORY.md ~/.claude/projects/<hash>/memory/MEMORY.md 每次会话启动 Extract Agent 完成后
topic *.md ~/.claude/projects/<hash>/memory/ 每轮对话 prefetch(≤5个) Extract Agent(按内容语义类型分文件)
CLAUDE.md 项目根 / 父目录 / ~/.claude/CLAUDE.md 每次会话启动 用户手动编辑
team/*.md ~/.claude/projects/<hash>/memory/team/ 同 topic 文件(TEAMMEM flag 开启时) Extract Agent(scope=team)
session-memory ~/.claude/projects/<hash>/session-memory/ auto-compact 触发时 Session Memory subagent

七、设计哲学

整个记忆系统体现了几个核心设计原则:

1. 不阻塞主流程:检索和提取都是异步的,用户感知到的延迟只有推理本身。

2. 精准而非全量:每轮最多注入 5 个文件,用 Sonnet 做语义评分而不是关键词匹配,宁缺毋滥。

3. description 是一等公民:评分阶段 Sonnet 只看 description,这意味着写记忆时 description 的质量直接决定记忆能否被正确召回。

4. 两套体系各司其职CLAUDE.md 是用户的意图表达,AutoMem 是 Claude 的学习积累,两者互不干扰,加载路径也完全分离。

5. 游标机制保证增量lastMemoryMessageUuid 确保每次提取只处理新增消息,避免重复分析历史内容。

相关推荐
郑寿昌2 小时前
AI时代动画游戏教育新变革
人工智能·游戏
LLWZAI2 小时前
不用大改原文,也能安稳通过朱雀 AI
人工智能
星座5282 小时前
零实验、AI融合:文献计量学SCI论文写作技巧(Citespace、VOSviewer的强大应用)
人工智能·citespace·文献计量学·sci·vosviewer
云烟成雨TD2 小时前
Spring AI Alibaba 1.x 系列【51】Graph 整体运行全流程
java·人工智能·spring
墨神谕2 小时前
Agent Skill从使用到原理
人工智能·ai
财经资讯数据_灵砚智能2 小时前
基于全球经济类多源新闻的NLP情感分析与数据可视化(日间)2026年5月13日
大数据·人工智能·python·信息可视化·自然语言处理
零壹AI实验室2 小时前
用AI 10分钟搭建一个监控系统:Prometheus + Grafana 实战
人工智能·grafana·prometheus
志栋智能2 小时前
超自动化巡检:量化运维成效的标尺
运维·网络·人工智能·自动化
AI科技星2 小时前
紫金山天文台与6G 超导太赫兹实验对比【乖乖数学】
人工智能·线性代数·机器学习·量子计算·agi