断断续续啃了一个多月,这个系列终于走到了第五篇。前四篇里,我把Claude Code的对话调度、工具注册、权限校验和命令解析挨个拆了一遍,基本摸清了“一次请求从进来到出去”的主链路。但有个问题始终绕不过去——一个只能“聊完就忘”的Agent,凭什么能连着好几个小时跟着你改代码、跨文件重构、始终保持上下文一致?答案就是它背后的记忆体系。
这篇专门拆Claude Code的分层记忆架构,讲清楚它如何把对话过程中产生的临时状态、项目级的长期规则、用户级的全局偏好,分别管理、按需加载、冲突仲裁。适合三类人读:正在做Agent应用、被上下文管理折磨的开发者;想给自己的AI工具加记忆能力的后端工程师;以及单纯好奇Claude Code项目目录里那一堆配置文件和存档文件到底各司何职的人。读完你会明白,“知识持久化”没有玄学,无非是一套务实的读写策略,加上几条挺聪明的取舍规则。
1. 记忆体系全景图:三层分类与各自职责
1.1 为什么不能只靠一个“大列表”存所有记忆
先说个残酷的事实:单纯把对话历史攒成一个数组,往模型上下文里一塞,这种方案在小demo里跑得通,一旦进入真实项目立刻崩。
我见过不少自建Agent项目,记忆方案简单粗暴——把所有历史消息拼成一大段文本,每次请求全量带上。结果就是:token开销随轮数线性暴涨,单次请求的计费数字越来越难看;对话超过一定长度后,模型开始“选择性失忆”,早期的重要决策全被淹没;更糟糕的是,修改一条旧规则要重新拼接整个文档,写代码变成了一场字符串手术。
Claude Code的思路不是这样。我在源码里看到的是一条清晰的工程路线:把记忆按生命周期和价值密度拆开。这背后其实是计算机体系结构里最经典的“分层缓存”思想在Agent场景下的迁移。你可以把三层记忆类比成书桌、书架和档案馆——手头正在用的资料放书桌(立刻能取),工作相关的材料放书架(随拿随用),需要长期留存的历史档案进档案馆(按需调阅)。如果所有东西都堆在书桌上,后果可想而知。
记忆分层与计算机存储体系对照表 运行时上下文 | CPU/寄存器 | 易失、极快、容量小 会话级存档 | 内存 | 断电即失、速度中等 持久化规则 | 磁盘文件 | 永久保存、按需加载1.2 三层的划分逻辑
Claude Code的记忆体系,按我读源码时梳理的结果,大致可以分成这么三层:
第一层是运行时上下文,对应的是单次会话中模型实际看到的那部分内容。它由消息队列、工具调用记录、系统提示词拼装而成,每一轮对话都会全量重建。这一层的核心矛盾是容量:模型上下文窗口有上限,必须精打细算。
第二层是项目级持久记忆,以项目目录下的规则文件为核心载体。这一层解决的是“换了个新会话,项目背景还在不在”的问题。只要工作目录正确,新会话启动时就能把项目规范、技术栈说明、代码风格约束重新读进来。
第三层是用户级全局记忆,存放跨项目的个人偏好和通用指令。它独立于任何具体项目,常驻用户主目录下,保证你在这台机器上的任何新项目里,一些基础行为习惯都会被尊重。
这三层各自的职责边界很清楚:运行时管“此刻的状态”,项目级管“这个项目的规矩”,全局级管“你这个人做事的风格”。代码里虽然有交叉引用,但职责划分不模糊。
1.3 记忆的生命周期:写入、驻留、淘汰、回读
任何记忆系统的核心问题都是同一个:什么时候该记住,什么时候该忘掉。
Claude Code的处理方式,如果抽象成代码逻辑,是四条不断循环的操作:
- 写入:会话中的关键决策、工具执行结果、用户明确表达的偏好,会被记录到对应的记忆载体。
- 驻留:运行时上下文中保留最近N轮的关键信息,其余老消息折叠压缩。
- 淘汰:超出预算的旧内容要么被摘要替换,要么被移出上下文,但不删除落盘副本。
- 回读:新会话启动时,按“全局规则 -> 项目规则 -> 运行时续接”的顺序重新装配记忆。
整个循环里最见功底的是第3步的淘汰策略。我在源码里看到的不是简单“从头删掉最老的”,而是保留对话骨架、压缩过程细节。具体实现我们放在第二章细说。
2. 第一层:运行时上下文中的“瞬时记忆”
2.1 消息序列的数据组织方式
Claude Code的会话核心,在内存里维护的是一条结构化消息链。每个节点保存着消息来源、消息类型、文本内容和关联的工具调用结果,然后统一交给一个序列化模块,拼装成模型接口要求的消息数组。
读源码时有个细节值得注意:消息并不是只存用户说了什么和助手回了什么。工具调用的入参和返回值也会进入消息链,并且各自带一个角色标识。这样做的好处是,模型在后续推理时能完整回溯“我上一轮调了哪个工具、返回了什么结构、据此得出了什么结论”,而不是只看到一句孤零零的“文件已保存”。
一条消息节点的字段骨架(逻辑示意) sender: "user" | "assistant" | "tool" content: string tool_call_id: string # 关联到具体某一次工具调用 timestamp: number summary_of_previous: string | null # 折叠摘要时填充这个结构看起来平淡无奇,但决定了后面摘要压缩、滑窗淘汰、会话恢复的实现复杂程度。数据结构设计得好,后面所有策略都有操作空间;设计得扁平,后面只能对着一个大字符串发愁。
2.2 Token预算分配与滑窗淘汰策略
运行时上下文最核心的机制是token预算。Claude Code在装配每轮请求时,会先算出一个总预算,然后按优先级逐块分配:系统提示词和当前轮次的用户消息最先保障,接着是最近的几轮对话,再往前的历史则按重要程度参与分配。
我读源码时最直观的感受是,它的淘汰粒度不是“轮”,而是“块”。每一块由若干连续消息构成,淘汰时按块移除,这样可以避免把一轮对话里的问题和回答拆得七零八落。触发条件也很硬:当预计算出的提示词token数超过预算阈值的某个比例时,折叠流程自动启动。
token预算分配优先级示意(从高到低) 1. 系统级指令与全局规则(不可折叠) 2. 当前轮用户消息与正在执行的工具调用 3. 最近N轮“热对话”(原样保留) 4. 较早轮次的“温对话”(允许折叠为摘要) 5. 历史搜索记录 / 项目规则明细(按需才加载)这个优先级其实挺反直觉的——很多自研Agent倾向于给历史对话很高的权重,生怕模型忘了前面的决策。但Claude Code的选择是:超过热区的东西统统先压缩,真要细问时可以再查原文。这样换来的是单次请求响应更稳定、计费更可控。实际测试下来,压缩处理后,长会话的回复质量反而比硬塞译文要好。
2.3 旧对话折叠成摘要:摘要压缩策略
折叠策略是这套体系里最有意思的部分。它先把一段连续对话切成小块,逐块生成要点摘要,再把多条摘要合并成一条更精炼的梗概,层层上卷。每一步摘要生成都会在消息链上挂一个“这句其实是从哪几条原文提炼出来的”指针,方便需要细节时反向追溯。
这个设计暗合一个工程经验:摘要不能只做一次。单次把两小时对话压成三句话,丢失的信息量是灾难性的。分块压缩、逐级上卷,每层丢掉的都是相对次要的过程描述,关键的决策依据和明确的用户指令会在更上层保留下来。
我自己的一个模拟项目里复刻过这套折叠逻辑,把同样的分级压缩方案套在一个文档问答Agent上。结果有两个:一是token成本下降约四成;二是用户明确说过的偏好,比如“以后回复尽量用表格”,在对话进行到第两百轮时依然能准确执行。这在原来全量拼接的方案下基本做不到。
3. 第二层:项目级记忆——以文件为载体的持久化
3.1 CLAUDE.md:项目记忆的主入口
项目级记忆的落地场景,就是每个项目根目录下的CLAUDE.md文件。这个文件名你只要用过Claude Code就不陌生,但源码层面的加载逻辑远比“读一个文件塞进提示词”复杂。
我读到的实现大致是这样:会话启动时,运行时模块会从当前工作目录开始逐级向上查找CLAUDE.md,找到后读取内容,并解析其中的指令块、约束块、任务说明块。解析结果不是一股脑塞给模型,而是先做分类:哪些属于“全局风格指令”,哪些属于“具体功能约束”,哪些是“流程类要求”,然后分别挂载到系统提示词的不同位置。
这里有个很实用的设计:CLAUDE.md支持多文件合并。项目根目录放通用规范,子目录里可以放针对该模块的专用说明。加载时,运行时模块会按目录层级从下往上回溯,把各级文件合并成一份“项目记忆包”。模块级说明只在该目录下的会话中生效,避免无关规则污染其他模块的上下文。
3.2 从目录读取到规则合并的完整流程
规则合并的具体流程,可以简化成下面几步:
- 运行时模块定位当前工作目录,确认项目根路径。
- 列出从项目根到当前目录之间所有包含CLAUDE.md的层级。
- 按“先根后叶”的顺序逐级读取,浅层文件作为基础规则,深层文件作为覆盖规则。
- 合并出的文本经过预处理:剔除无效空行、标注指令来源文件路径、计算其token占用。
- 将合并结果注入系统提示词的“项目知识区”。
这个过程每次新开会话都会重新执行。很多人改完CLAUDE.md发现不起作用,十有八九是没重启会话,或者工作目录定位错了、加载的根本是另一个层级的文件。这个坑我在后面第七章还会专门展开讲。
还有个容易忽略的细节:CLAUDE.md里用注释语法标记的段落会被单独解析出来,当作“只给开发者看、不给模型看”的元信息。这个机制非常实用——你可以在文件里给未来的自己写维护说明,模型不会把这些内容读进上下文去消耗token。
3.3 记忆文件的更新时机与版本管理
项目记忆是写进git的文件,天然带着版本管理的能力。这一点我觉得是整个设计里最聪明的决策——它没有造一个私有的记忆数据库,而是直接复用代码仓库现有的协作和版本控制工具。
在实际操作中你会发现,CLAUDE.md的更新流程非常有“代码味道”:新增需求时加一段指令,重构完成后删掉过时约束,重大设计变更时改写技术背景说明。一切变更都有commit记录,出了问题可以回滚。这比把记忆存在某个数据库表里、变更不可追溯的方案高出一个段位。
源码层面上,运行时模块对这份文件的态度是“只读重用”。它负责加载和解析,但不会自动改写文件内容——除非你显式要求执行记忆更新操作。这里的取舍很明确:自动写入会造成文件内容漂移,用户不可控;只读加载则保证了规则的稳定性和可审计性。我把这理解为Claude Code的一个设计原则:记忆的写入权必须掌握在用户手里,模型只能利用记忆,不能偷偷修改记忆。
4. 第三层:跨项目的全局记忆与优先级仲裁
4.1 用户级全局记忆的加载场景
全局记忆存的是“你这个人”的偏好,跟具体项目无关。它常驻在用户主目录下,和我前面提到的项目级记忆互不干扰。
从源码层面的加载逻辑看,会话启动时最先加载的就是全局记忆。它的解析方式和项目级CLAUDE.md类似,但注入位置不一样——项目规则注入在“项目知识区”,全局偏好则直接参与系统提示词的“人格与风格区”构建。这带来的效果就是:你在任何项目里写代码,模型回复代码注释的风格、使用中文还是英文、偏向详细解释还是简洁结论,这些偏好都会稳定生效。
有意思的是全局记忆也可以共享团队规则。源码里支持把用户主目录下某份规则文件当作“团队共识”来加载,所有成员同步同一份基线规范。这个场景特别适合一个团队接入同一套Agent工作流时使用——个人偏好放本地,团队规范放共享目录,互不冲突。
4.2 多来源指令的优先级规则
多层记忆同时存在,最棘手的问题就是冲突:全局说“回复尽量简洁”,项目说明书里写“必须逐步展开解释设计决策”,到底听谁的?
源码里的仲裁逻辑,规则非常朴素:越具体的优先级越高。按优先级从高到低排序:当前对话轮次内用户明确说的最新指令,高于项目记忆里的约束;项目记忆里的内容,高于全局记忆里的通用偏好;全局记忆中的明确偏好,高于模型默认行为。
有一个反直觉的细节是:当前对话轮用户的“最新指令”优先级最高,会覆盖项目规则。这意味着你在对话里说一句“这次例外,不要用表格”,模型会遵从这个即时指令而不是项目规则。这个设计合理之处在于它尊重了“人的即时意图高于预先设置”的原则。但是在多轮长对话中,临时指令如果没有被记忆系统捕获,下一轮可能就失效了。所以我给了一个建议:长期有效的特殊偏好,最终还是写进CLAUDE.md比较稳妥。
4.3 项目隔离:为什么记忆不能随便串门
全局记忆负责跨项目共享,项目记忆负责单项目沉淀,但两者之间有条看不见的隔离线。
我在阅读源码时发现,项目记忆被绑定到“工作目录”上,一旦目录切换,加载出来的项目规则就完全不同。这个绑定关系在实现上非常简单:就是拿当前工作目录的路径作为查找键。但这带来一个现象,同时也容易埋下一个坑。
好处是:你在A项目里积累的模块说明不会污染B项目的上下文,B项目的Agent不会莫名其妙知道A项目的内部接口规范。这对多项目并行开发至关重要。坏处是:如果你习惯在一个目录里同时操作两个项目,比如某个常见的聚合仓库模式,记忆系统可能分不清你当前到底在跟哪个项目对话,加载出的规则集就可能是两份的并集。
这是多项目使用者最常遇见的鬼打墙问题。后面第七章节会有更细致的排查方法。
5. 读写路径拆解:一次请求如何携带三层记忆
5.1 写路径:记忆从哪来、写到哪去
记忆不是凭空产生的,写路径决定了一套系统能积累什么。Claude Code的写路径主要有三条:
一是用户显式写入。你编辑CLAUDE.md、修改全局偏好文件,这是最高质量的记忆来源,语义清晰、目的明确,系统只需要负责读取。
二是运行时自动记录。每次工具调用的入参和返回结果,都会被追加到当前会话的消息链中。这部分不会自动落盘到项目记忆文件,但会保留在会话存档里,是后续摘要压缩的原料。
三是半自动的操作确认。“要不要把这条经验写进项目记忆?”这类确认操作,源码中实际上是触发一个文件写入动作,把对话中提炼出的一段文本追加到CLAUDE.md。这个流程巧妙的点在于,它把“模型理解用户意图”和“更新持久记忆”这两件事分开了——先由模型给出建议,再由人来确认。
我自己用下来最大的体会是,写路径必须可控。自动写入很爽,但风险也大:模型可能把一次性的临时指令当成长期规则写盘,导致后续所有会话都被带偏。
5.2 读路径:请求到来前的记忆装配流程
读路径是Claude Code每次请求开始前必经的流水线。
第一步,启动时加载系统级模板,包括默认的行为规范、能力声明、模型使用指南。第二步,往同一个上下文池子里注入全局记忆,让用户的通用偏好成为最底层的人格底色。第三步,根据当前工作目录定位项目根,加载项目级规则文件,按层级合并后注入。第四步,从会话存档里恢复最近的续接信息——如果你在恢复一个之前的会话,这里会把当时的消息链尾部重新装配进来。第五步,新用户消息进入,系统把所有内容拼装成最终提示词。
完整流程走下来你会发现,三层记忆装配是有严格顺序的,但最终都落到同一个上下文池子里。这个“先全局、再项目、再会话”的顺序,保证了每一个更具体的层能覆盖掉更通用层的默认行为。
5.3 冲突与淘汰:当记忆互相矛盾时怎么办
记忆矛盾是实际操作中躲不开的问题。比如项目规则里写着“禁止使用某个依赖库”,但全局偏好里没有这条限制,而对话历史里你早就让模型绕过这个限制用了别的方案。
源码里对冲突的处理分两部分:加载期的仲裁和运行期的覆盖。加载期仲裁会按前面说过的优先级规则处理静态冲突——以项目规则覆盖全局偏好。运行期覆盖更灵活——对话中出现了一条最新的、与既有规则矛盾的指令时,模型默认执行最新指令,并且不会反向修改已加载的规则文件。
这种做法相当务实:它把“规则冲突”从预防转向了“实时响应”。不会因为一条新指令就回写磁盘,也不会在冲突时直接拒绝执行。代价是可能在长对话中产生“这一次性指令漂移”的问题。我的解决办法是,重要的一次性指令立刻手动补写进项目记忆,把“临时豁免”转成“正式规则”。
6. 源码阅读地图:从入口到核心实现的调用链
6.1 三个关键入口文件
如果你要自己动手读源码,建议从这三条线入手。以我手上这份源码版本为例,路径在不同版本间会略有差异,但模块职责划分是一致的:
第一条线是CLI启动入口。它负责解析启动参数、初始化运行时环境、确认工作目录,是整个记忆加载流程的起点。第二条线是会话管理器。它内部维护消息链、上下文组装、记忆装配。读这里重点看一个方法:如何把全局规则、项目规则、已有会话历史合并成最终提示词。第三条线是存档服务。它负责会话状态的持久化和恢复,解决“断开之后还能不能续上”的问题。
我建议阅读顺序:先看启动入口摸清目录结构,再跳进会话管理器看记忆装配流程,最后切到存档服务看落盘格式。三块看完,整个记忆体系的骨架就立起来了。
6.2 核心数据结构与字段设计
源码里记忆相关的数据结构,最有研究价值的是一张“会话快照表”。它记录了会话当前的消息状态、已经折叠的摘要、引用的项目规则文件路径、当时注入的全局记忆版本。
会话快照的核心字段(逻辑示意) session_id: string project_root: string messages_snapshot: [] summaries: [] rules_version: int persisted_at: timestamp另一个值得关注的结构是“规则索引表”。全局规则、项目规则、当前会话瞬时指令分别用不同标识记录,并保存各自的来源路径和优先级序号。在实际读取时,规则索引表是决定各个记忆来源如何排序的关键。
6.3 扩展记忆来源:接口预留与自定义接入
源码中专门留了记忆扩展接口。第三方可以实现一个记忆提供方,再通过配置切换加载来源。这个设计非常有利于团队内部做知识库整合——比如把公司内部的代码规范库作为项目记忆的一部分加载进来。
不过我对这个扩展点的建议是谨慎。记忆加载是每次会话启动必经的路,任何外部依赖的引入都会增加启动耗时和故障面。我见过一个团队把项目记忆改造成从内部的在线文档平台拉取,结果每次启动会话都要等待网络请求,体验反而更差。记忆扩展适合做“低频更新、高价值内容”,不适合接高频变动的在线服务。
7. 实操问题速查:记忆不生效、上下文超限、跨项目串扰
7.1 修改了规则文件却不生效
这个是我看到最多人踩的坑,也是我自己最开始遇到的问题。改完CLAUDE.md,满怀期待地继续对话,结果模型还在沿用旧规则。排查思路按顺序走:
第一步,确认当前工作目录是不是项目根目录。如果在一个子目录里开会话,加载的可能是子目录级规则,而不是根目录那份。第二步,确认会话有没有重启。运行时装配记忆发生在会话启动阶段,正在进行的会话不会重新加载规则文件。第三步,确认是否有另一份同名文件在“上游”被加载了。先全局后项目的加载顺序里,高优先级文件会覆盖低优先级文件的同名规则。
7.2 对话越长回复越差,怎么判断是不是上下文超限
如果你发现对话进行到很深的位置时,模型开始重复问一些前面已经给过答案的问题,基础决策突然记不起来,大概率是运行时记忆进入了折叠阶段,早期细节被摘要替换了。
判断方法很直接:查看单次请求的token花费和上下文占用。如果上下文占用长期超过预算阈值,说明折叠机制在频繁触发,摘要丢失的信息量在增加。这时候有两种调整路径:一是把关键约束固化进CLAUDE.md,让项目规则层帮运行时托底;二是手动开启一个“续接模式”,直接从当前会话档案恢复上下文,而不是重新开一个空会话。
7.3 多项目并行时记忆串扰
两边项目同时开工,窗口切来切去,结果这边项目的Agent忽然说出那边项目的代码结构,这种串扰现象多数时候不是模型出幻觉了,而是记忆加载错了项目上下文。
排查方法先看会话启动时锁定的项目根路径是不是预期的那个。再检查是不是在两个项目被嵌套在同一目录层级下,触发了标记识别失败的问题。根治办法是让每个项目拥有独立的项目根目录,并且保持各自的CLAUDE.md不跨项目引用。
7.4 查看记忆内容的两个实用入口
排查记忆问题最直接的办法,就是去看系统实际注入了什么。这里分享两个我常用的入口:
第一个入口是会话的调试日志。启动会话时打开调试开关,运行时会把最终拼装好的提示词和记忆装配顺序打出来,一眼就能看出全局规则、项目规则、会话历史各自占了多大块、排了什么顺序。第二个入口是直接查看存档目录下的会话快照文件。快照里记录了当前会话折叠了哪些摘要、引用了哪个版本的规则文件,能帮你确认到底是加载问题还是折叠问题。
这两个入口配合,绝大多数记忆相关的问题都能在五分钟内定位。
我个人读完这套源码最大的感受是:Claude Code的记忆体系并没有发明什么玄乎的新概念,它只是把“分层缓存”“冷热分离”“写读分离”这些计算机系统里最古老的思想,老老实实地用在了Agent的知识持久化上。真正拉开差距的是对各种边角情况的处理——摘要怎么折叠才不丢关键信息、规则冲突时谁说了算、临时指令和长期规则怎么共存。这些细节在源码里落得很细,每一处都是实际使用中会真正遇到的痛点。
最后分享一个自己的小习惯:我会在每份CLAUDE.md的顶部维护一个三行的“记忆索引”,写清楚这份文件里有哪些分区、每段规则大概在哪个位置、最近一次主要更新是什么时间。看起来挺土,但它带来的收益很大——模型加载规则时能更快定位关键段落,我自己回溯记忆变更时也不用从头读整份文件。小成本,长期回报,值得一试。