自从claude code源码泄露以来,网上分析源码的文章已经太多,而且点星还不好。但是我阅读了几篇github上的文章以后,我觉得其实分析并不到位,用处很小。
这些分析的文章大多过于细节,关注实现的细枝末节了,比如调了哪个函数,跳到了哪个类去执行。我严重怀疑,都是codex生成出来的,因为那个口吻太像了。
我个人觉得觉得, 一定关注实现的技术原理和设计思想,才会对我们做相关的系统有帮助。因此,我定下这个计划,打算自己动手把相关的功能全拆解一遍。
本文是拆解文章的第一篇,从记忆系统入手。
范围·哪些是记忆?
我在github上看到文章中,对cc系经的记忆模块的分类会将这些全包含进去:project级,抽取到memory文件夹中的记忆内容;claude.md文件中的内容;session summary中的内容;dream相关的内容。
我一开始很疑惑,为啥会把claude.md也算作记忆了?直到我去用codex分析源码,原来这是codex给出的结论。
然后我就明白了,为什么这么短的时间,这帮兄弟大神们,就把claude code拆的干干净净还写出了一系列的文章—原来,直接用的agent,而且大概率连改都没改一下。
真心觉得,还是不要这么着急的好。越是ai时代,越是要静下心来搞点有用的东西。
在本文中,我认为只有这些内容才算作记忆:在对话中被抽取出来,最终会落到memory目录的记忆内容。
对话的summary和claude.md中的内容,不涉及记忆产出,召回和使用策略,如何能算作是记忆呢?
机制·底层实现原理
首先我们探究一下记忆机制的底层实现原理。实际上,底层的模型技术仍然是最基础部分:prompt和工具调用。并没有什么特别的机制。
实际上,绝大部分ai应用的的底层技术都并不难。但实现特定的效果,有时候需要一些想象力才行。
记忆机制的核心原理,简单描述可以是这样:
- 在适当的时机,从模型对话中抽取出一些信息(例如用户偏好,事实性描述),这些信息可以在后续的对话中用到
- 在随后的对话中,在提示词中插入这些信息,并且告诉模型:这些是之前的记忆
- 完成。
另外,实际做起来时,细节实现上也是要费很大功夫的。
协议·记忆功能蓝图
从本质来说,模型并不知道什么是记忆。精确的说,它不知道你需要的记忆是什么(因为在你的应用场景中,记忆跟常识中的记忆概念有关系也有很大差别)。
在所有的实现之前,我们得先给模型一个整体的概括性定义。也就是说,我们得先让模型明白,什么是记忆,有什么用,该怎么对待它。 这是整个记忆系统在llm的调用中产生作用的关键。
我称之为,协议—你与llm之间的协议。
cc把这部分放在了主流程的prompt里,我们可以挨个分析一下,以下是翻译成中文的提示。
记忆的定位
请看prompt:
你拥有一个持久化的基于文件的记忆系统,位于
/xxxxxx/memory/。该目录已经存在 —— 请直接使用写入工具进行写入(不要运行mkdir或检查其是否存在)。您应该随着时间的推移建立这个记忆系统,以便未来的对话能够全面了解用户是谁,如何与您合作,应该避免或重复哪些行为,以及用户提供的工作背后的背景。
网上很多分析的文章,对那段目录写入的提示赞不绝口,认为可以避免llm再多调一次工具,省token。小题大作,纯属扯蛋。 事实上抓过claude包就会知道,多或者少这一次工具调用真的是无关痛痒。而且,我猜你要是在自己的系统里面这么用,绝b翻车。
事实上,我认这段提示词是整个记忆系统中最核心,也是最精髓的地方。假如我们把cc比作一套神经系统,那么这段提示词就是记忆系统中的最有机的那一部分:把所有的工程实现联接到一起。
有哪些值得关注的点?分析如下:
- “你拥有一个…”,这是对背景的描述,这个提示让模型添加了一层意识,明白了这是什么
- “你应该….”,这会给模型一个引导,决定模型在之后的多轮对话中去更新记忆
- “以便未来….”,这给模型理由,会让模型在做决策的时候更精准
分类
这一部分是对记忆的内容的详细描述。在前述说明了基本的定调以后,这部分将需要记哪些,为什么记划分清楚。
我认为这是这是最重要的一部分。类型定义了你这个系统的记忆的特征:应该怎样和记住哪些东西。类型的定义往往是与应用场景相关的。
看其中一种类型的prompt:
<type>
<name>user</name>
<description>
包含关于用户的角色、目标、责任和知识的信息。
好的用户记忆能帮助您根据用户的偏好和视角调整您的未来行为。
阅读和写入这些记忆的目标是建立对用户是谁以及如何最好地为他们提供帮助的理解。
例如,您应该以不同的方式与高级软件工程师合作,而不是与第一次编码的学生合作。
请记住,目标是对用户提供帮助。避免写下可能被视为负面判断或与您试图完成的工作无关的用户记忆。
</description>
<when_to_save>
当您了解用户的角色、偏好、责任或知识时
</when_to_save>
<how_to_use>
当您的工作应该受用户的个人资料或视角的影响时。例如,如果用户请求您解释代码的某一部分,您应该根据他们最有价值的细节或帮助他们建立与现有领域知识的思维模型来回答问题。
</how_to_use>
<examples>
user: 我是一个数据科学家,正在研究我们现有的日志记录
assistant: [保存用户记忆: 用户是数据科学家,当前关注于可观察性/日志记录]
user: 我写 Go 已经十年了,但这是我第一次接触这个仓库的 React 部分
assistant: [保存用户记忆: 精通 Go,新的 React 和这个项目的前端 — 在前端解释时以后端类比为框架]
</examples>
</type>
每种类型,都包含了名称,细节,什么时候保存和什么时候用。还给了几个示例。
一共定义了四种:user,feedback,project,reference。
为什么我说类型是应用场景相关的?显然记忆的类型是不可能穷尽的。作者定义这些类型,一定是根据场景经过了仔细的思考。
分析如下:
- user类型,可以让系统记住用户的偏好和背景,在随后的回复中,agent可以根据用户调整回复和策略。这是一种基础的记忆类型定义。我觉得大部分系统可能都是需要的,这部分可以直接抄。
- feedback类型。这部分是操作型指导记忆。由于cc本身是agent(用来干活的),这部分记忆可以让系统在之后保持与用户偏好的一致。好比你有个下属,那么这个下属,必须了解你希望他如何干活
- project类型。这是比较模糊的一种分类,系统中又会明显指出,关于系统的说明、结构等信息是不存记忆的(因为可以直接获得)。根据提示信息,cc真正想要记住的是,项目的背景、计划(例如这个项目是干什么的,什么时候结束这些在文件里面体现不出来的),还有就是项目的事件信息(谁在什么时候干了什么)。
- reference,外部信息。这部分的意图应该是将信息与当前项目无关,但是又可能能会需要到的外部相关的信息的存储。
显示这些类型的定义是紧跟业务目标的:这是一个code agent, 这四种类型其实就是:偏好+背景。
范围排除
这也是协议的一部分。目的是告诉llm,哪些不是记忆。
不应保存的记忆内容
- 代码模式、约定、架构、文件路径或项目结构 —— 这些可以通过阅读当前项目状态得出。
- Git 历史记录、最近的更改或谁更改了什么 ——
git log/git blame是权威的。
边界澄清
这是prompt的最后一部分。 这部分提示用来给llm,一个概览性的框架,将现有的存储区分出来。
记忆与其他持久性形式
记忆是您在协助用户的对话中可用的多种持久性机制之一。其区别通常在于,记忆可以在未来的对话中被回忆,不应用于存储仅在当前对话范围内有用的信息。
- 使用或更新计划而非记忆的时机:如果您即将开始一个非琐碎的实现任务,并希望与用户就您的方法达成一致,您应该使用计划而不是将此信息保存为记忆。同样,如果您已经在对话中有了一个计划,并且改变了您的方法,应通过更新计划而不是保存记忆来持久化这一变化。
- 使用或更新任务而非记忆的时机:当您需要将当前对话中的工作拆分为离散步骤或跟踪进度时,使用任务而非记忆。任务非常适合持久化当前对话中需要完成的工作,而记忆应保留用于未来对话中有用的信息。
记忆触发的设计与实现
那么在cc中,记忆是什么时候被触发的呢?我看了好几篇网上的文章,并没有讲清楚这个事情。他们只会照着codex给出的内容抄,因此内容混乱。我仔细分析了代码并抓包分析了一下,当前的实现中,入口是通过提示词来让llm感知实现的。
触发原理
从代码和抓包,我们可以确认当前cc实现的记忆入口是基于提示词+工具调用的。证据如下:
简单来说,其原理是:
- 在提示词中写入指令,包含记忆触发的时机,和需要做的动作(前文已分析)
- 定义工具,在场景触发时,将记忆按规则写到文件里面(当前使用Write命令)
触发时机
当前cc中的实现中,触发时机是分布在每种类型的定义中间:
<when_to_save> 当您了解外部系统及其用途时。例如,某些 bug 在特定的 Linear 项目中跟踪,或者某些反馈可以在特定的 Slack 频道中找到。 </when_to_save>
llm在对话中感知到这样的场景时,就会发起一次工具调用。
同时定义了示例,可以让llm更好的理解场景:
user: 如果想了解这些工单的上下文,查看 Linear 项目“INGEST”,我们在那跟踪所有的管道 bug assistant: [保存参考记忆: 管道 bug 跟踪在 Linear 项目“INGEST”中]
user: grafana.internal/d/api-latency 是值班人员查看的板块 —— 如果您正在处理请求处理,这就是会触发人员的板块 assistant: [保存参考记忆: grafana.internal/d/api-latency 是值班人员的延迟监控仪表板 —— 在编辑请求路径代码时查看它]
记忆存储
格式设计
记忆应该以什么样的格式存储呢?cc使用文件+索引的方式来实现。类型提示中,明确写出了单个记忆文件的结构:
<body_structure> 以规则本身为主导,然后是 为什么: 这一行(用户给出的原因 —— 通常是过去的事件或强烈的偏好)和 如何应用: 这一行(这一指导何时/如何生效)。 知道 为什么 可以帮助您判断边缘案例,而不是盲目跟随规则。 </body_structure>
这个提示告诉llm,应该抽取出什么样的记忆。这会产生一个具有结构化的记忆文件存储。
- 记忆结论:比如用户是什么角色,用户需要你干什么,一般就一句话
- 一个原因:可以理解为这条记忆的背景,可以让llm充分理解
- 如何应用:什么时候生效,操作性规则
落盘的时候,每条记忆会产生这样的一个文件,例如<user_role.md>:
name: user role and goals
description: Financial analyst researching China macro — needs data scraping, organization, and visualization tools type: user
金融分析师,研究方向为中国宏观经济。主要需求:
- 抓取宏观经济数据(房价指数、居民中长期贷款等)并整理成 xlsx 格式
- 数据可视化 — 使用 ECharts 生成交互式 HTML 图表进行趋势分析
项目以 Python 为主,数据来源包括国家统计局(stats.gov.cn)等官方渠道。用户使用中文沟通。
然后在一个总的记忆索引文件<Mmemory.md>里面记下来:
- User Role — 金融分析师,研究中国宏观经济,需要数据抓取、整理和可视化工具
而这个memory.md,会在之后的对话中被插入到提示词中。
保存过程
保存过程在提示中定义(以下摘自原版提示词,通过抓包得到):
如何保存记忆
保存记忆是一个两步过程:
步骤 1 — 使用前置信息格式将记忆写入其独立文件(例如
user_role.md,feedback_testing.md):--- name: {{记忆名称}} description: {{一行描述 — 用于在未来对话中判断相关性,因此请尽量具体}} type: {{user, feedback, project, reference}} --- {{记忆内容 — 对于反馈/项目类型,结构应为: 规则/事实,然后是 **为什么:** 和 **如何应用:** 这一行}}步骤 2 — 在
MEMORY.md中添加指向该文件的指针。MEMORY.md是索引文件,不是记忆文件 — 每个条目应该是 150 字符以内的一行:- [标题](file.md) — 一行概述。它没有前置信息。不要直接在MEMORY.md中写入记忆内容。
MEMORY.md会始终加载到您的对话上下文中 — 超过 200 行将被截断,因此保持索引简洁- 保持记忆文件中的名称、描述和类型字段与内容一致
- 按主题而非时间顺序组织记忆
- 更新或删除错误或过时的记忆
- 不要写入重复的记忆。首先检查是否有现有的记忆可以更新,而不是写入新记忆。
这个过程的定义,实际上会引发在对话过程中产生工具调用。
使用记忆
识别与召回
这里要解决的问题是,在特定问题下,到底去使用哪条记忆?cc当前的实现中使用的是类似skill的渐进式发现。
memory.md中包含了记忆文件的索引,每行一个文件,并写出了主要内容。这个索引文件,会在截断200行以后,直接塞入到提示词中。位置在用户message的最后一个,system reminder中。
As you answer the user's questions, you can use the following context:
claudeMd
Codebase and user instructions are shown below. Be sure to adhere to these instructions. IMPORTANT: These instructions OVERRIDE any default behavior and you MUST follow them exactly as written.
Contents of CLAUDE.md (user's private global instructions for all projects):
Contents of MEMORY.md (user's auto-memory, persists across conversations):
- User Role — 金融分析师,研究中国宏观经济,需要数据抓取和整理工具
currentDate
Today's date is 2026-04-05.
IMPORTANT: this context may or may not be relevant to your tasks. You should not respond to this context unless it is highly relevant to your task.
之后,由于已经定义好了协议,模型可以判断,哪些记忆应该起作用。
然后系统在检测到需要相关的记忆时,发起read工具调用,将具体的memory文件加载出来,嵌入到llm的消息列表中。
我想claude是倾向于放弃工程上的复杂化,转而相信模型的能力。毕竟人家模型性能强。以下是识别到时,发生了一次工具调用。
在这个实现方案中,并不需要额外的插入记忆到系统提示中。工具调用后,记忆文件的详细内容会跟着工具调用结果被插入到message列表中。
Dream
dream功能刚出不久。前段时间刚出的时候,自媒体们可兴奋了。唉呀啥dream模式,牛批到爆,到最后也没说出是个啥。
从源码以及运行过程我们可以看到,dream其实是记忆的一个增强过程。简单来说:
- 在会话结束后异步无感运行
- 检察过去的会话,找到需要记忆的内容
- 整合记忆,该更新的更新,该删的删除
dream的流程
与记忆的实现不一样,dream更加偏工程化实现。
具体来说,dream不是通过agent的模型循环来自动触发和工具调用来实现。而是重新定义了agent流程,并使用工程化的方式主动触发。
根据代码,逻辑大概是这样的:
- 在每次对话完成以后,都会开启一次检查。
- 检查上一次dream完成的时间,中间的会话(session)次数等一系列条件。
- 如果满足的话,fork一个子agent,这个agent会把当前主会话的消息全带上(我不太明白这个设计的原因),然后发起一个dream的消息(提示词里面会把过程写清楚)
- 然后,agent会按照流程来,查看所有的会话消息(使用搜索来找到需要关注的用户消息),再检查现存的记忆文件
- 更新记忆文件,完成。
提示策略
我们把梦境相关的提示词全部拆解一下,看看这个功能是如何设计的。
第一部分,定位, 描述场景,目标。
您正在执行一个梦境 —— 对您的记忆文件进行反思式的回顾。将您最近学到的内容合成到持久、组织良好的记忆中,以便未来的会话能够快速定位。
然后是执行过程
阶段 1 — 定位
- 使用
ls命令查看记忆目录中已有的内容- 阅读
MEMORY.md文件,了解当前的索引- 浏览现有的主题文件,以便改善它们,而不是创建重复项
- 如果存在
logs/或sessions/子目录(助理模式布局),请查看其中的最近条目阶段 2 — 收集近期信号
查找值得持久化的新信息。资源按大致优先级排序:
- 每日日志(
logs/YYYY/MM/YYYY-MM-DD.md)如果存在 —— 这些是仅追加的流- 漂移的现有记忆 — 与当前代码库中的事实相矛盾的内容
- 转录搜索 — 如果您需要特定的上下文(例如,“昨天构建失败的错误信息是什么?”),可以用
grep查找 JSONL 转录文件中的狭义术语:grep -rn "<狭义术语>" /Users/rayxiang/.claude/projects/-Users-rayxiang-Documents-investment-research-macro/ --include="*.jsonl" | tail -50不要详尽地阅读transcript文件。只查看您已经怀疑重要的内容。
阶段 3 — 巩固
对于每个值得记住的内容,在记忆目录的顶层写入或更新记忆文件。使用系统提示的自动记忆部分中的记忆文件格式和类型约定 —— 它是保存内容、如何构建内容以及不该保存内容的真实来源。
重点:
- 将新信号合并到现有的主题文件中,而不是创建几乎重复的内容
- 将相对日期(如“昨天”、“上周”)转换为绝对日期,以便它们在时间流逝后依然可解释
- 删除被推翻的事实 —— 如果今天的调查推翻了旧记忆,请在源头修正它
阶段 4 — 修剪和索引
更新
MEMORY.md,使其保持在 200 行以内并小于 ~25KB。它是一个 索引,而非数据倾倒 —— 每个条目应该是一行,不超过 ~150 个字符:- [标题](file.md) — 一行概述。绝不直接将记忆内容写入其中。
- 删除指向现已过时、错误或被取代的记忆的指针
- 降级冗长的条目:如果索引行超过 ~200 字符,表示它包含了应该放在主题文件中的内容 —— 缩短该行,移动细节
- 添加指向新重要记忆的指针
- 解决矛盾 —— 如果两个文件不一致,修正错误的文件
但老实说,对于这个提示词我看并不是很懂,尤其是阶段二的收集部分—llm如何知道应该如集哪些内容呢?我觉得这里的指令写的很模糊。
也许是我没看懂提示词背后的设计。不过,还有另一种可能,就是这个功能目前还并不是很完善,只是初期版本。
证据是,我看了dream的执行过程,里面乱码有点多。
另一条路-extract
网上很多拆解记忆内容的文章,都会穿插另一些实现的逻辑:在对话完成后启动hook,然后启动一次模型调用,将记忆写入到memory文件中。 大概是他们没有仔细的分析和理解,因此把这些实现混到一起,导致别人会看不懂。
经过我的查证,实际应该是有点差异。
- 这套代码在泄漏出来的源码里面确实有extract这条产生记忆的路径,但是有一个编译开关。只有打开了我们才能实际看到它。而且这个开关并不是用户层级的。所以,很可能Anthropic在发布的时候没有打开它
- 证据是,我测试了多次,并没有在对话完成后看到这样一个模型的调用。
- 从代码来看,当前的agent记忆实现模式与尾部extract模式是互斥的,这个可以轻松的从代码中查证。
不过,extract方案仍然有研究价值,但是很遗憾官方没开,我们看不到它真实的运行效果。
实现方案
区别于agent方案。extract方案更加偏工程化。流程简要如下:
- 每次对话完成时(所有工具调用结束),通过hook单独启动一次模型调用
- 将自上次扫描以来的新消息,和当前的记忆文件收集起来
- 拉起一个sub agent,单独抽取记忆内容。
- 更新记忆文件。
提示策略
提示词很长,内容我就不贴了,大概结构可以说一下:
- 角色定义, 说你是一个记忆提取子代理,分析上方x条消息,更新你的持久记忆系统
- 然后是当前已经有的记忆文件内容
- 然后又把最上方的system prompt中的记忆类型定义重复了一遍(一模一样)
- 然后把写记忆的步骤重复了一遍。
我怀疑 这是一个未完成的功能,所以未发布,或者在测试中该团队认为这个方案不合适。理由是,真实实现的时候没有必要把类型定义和步骤再重复一遍。
对了,还有一个问题是,fork agent的时候,系统提示词其实仍然是保留的原版主对话提示词。无论是dream,还是extract,都是将另一套新的指令直接附在user消息里面。
不知道是不是anthroic有意设计,还是他们的模型玩法就是这样的。我会认为这样的过程会导致后续的sub agent产生飘移,毕竟system prompt还是原来的主流程上的。
另一种召回-moth_copse
以上提到的两种记忆模式,差别仅在如何写入记忆上。而对于召回,两种方法都是一样的:都是走agent工具调用模式。
其实系统中还有另一种隐藏的召回模式,代码里叫moth_copse,不知道什么意思。在这种模式下,召回的路径很加主动。大概的流程如下:
- 启动一个异步的模型调用,把用户的query和相关的记忆文件一起交给llm,让模型判断哪些记忆是相关的
- 然后用llm给出的文件,把内容全读取出来
- 塞到主流程提示中,作为记忆的文件列表。装在system reminder里。
注意这里有一个流程上需要注意的地方:这个过程是异步的。在收到query的时候就会去启动llm来筛选记忆,而且不阻塞主流程。该走工具调用或者回复的都继续走。然后llm返回的时候,如果赶上了主流程还没有把这轮跑完,那就塞进去,如果跑完了已经回复了用户了,那就放弃。
很可能是效果不好,或者不够稳定,anthropic官方并没有开启这个功能。