Claude Code源码阅读:万字解析记忆系统

写在前面
AIGC时代的《三年面试五年模拟》AI算法工程师求职面试秘籍独家资源:https://github.com/WeThinkIn/AIGC-Interview-Book
Rocky最新撰写10万字Stable Diffusion 3和FLUX.1系列模型的深入浅出全维度解析文章:https://zhuanlan.zhihu.com/p/684068402
AIGC算法岗/开发岗面试面经学习&交流社群(涵盖AI绘画、AI视频、大模型、AI多模态、AI Agent、数字人等AIGC面试干货资源)欢迎大家加入:

本次给大家分享的是Memory这部分,Agent系统内,长context下,Memory的维护非常重要,早在之前,我也有不少文章提过讲过Memory的有关技术,今天让我们来重点看看Memory这部分,claude code是怎么做的。
先说明,今天我给大家分享的依据是Claude code开源的第一版代码,即typescript的版本,并非后续有过修改的python版本,确保原汁原味,避免错漏。
目录
-
概况 -
记忆的类型和格式 -
核心流程 -
思考
开始之前有几个说明。
-
里面很多代码我是借助 AI 来阅读的,有些笔记也是 AI 生成的,大家注意鉴别。 -
这次的讲解,我会减少源码的粘贴,更多是讲逻辑和技巧,贴代码只是为了佐证逻辑。
概况
claude code的记忆系统,是一套基于文件的记忆系统,完全落地在本地目录,不依赖云端数据库,支持个人/团队/ 项目三层隔离,让 AI 可以长期保存用户偏好、项目规则、工作习惯、外部资源入口等关键信息,并在后续对话中自动召回使用。
涉及到记忆的部分,代码结构是这样的,除了内部的记忆的处理,还有外部触发,以及相关配置,大家关注的要就是记忆的部分,那看下面这些即可。
src/├── memdir/ # 记忆目录模块│ ├── memdir.ts # 核心协调器,加载入口│ ├── memoryScan.ts # 文件扫描器│ ├── memoryTypes.ts # 类型定义与 Prompt 指令│ ├── paths.ts # 路径解析│ ├── teamMemPaths.ts # 团队记忆路径│ ├── teamMemPrompts.ts # 团队记忆 Prompt│ ├── findRelevantMemories.ts # 相关性检索│ └── memoryAge.ts # 新鲜度检测│├── services/│ ├── extractMemories/ # 记忆提取服务(~35KB)│ │ ├── extractMemories.ts # 主服务,触发逻辑│ │ └── prompts.ts # Prompt 构建│ ││ ├── autoDream/ # 自动整理服务(~27KB)│ │ ├── autoDream.ts # 主服务,三重门控│ │ ├── consolidationPrompt.ts # 四阶段整固 Prompt│ │ ├── DreamTask.ts # 任务状态追踪│ │ └── config.ts # 配置管理│ ││ └── SessionMemory/ # 会话记忆模块(~18KB)│ ├── sessionMemory.ts # 主服务,阈值检测│ ├── prompts.ts # 更新 Prompt│ └── template.ts # 默认模板│└── screens/ └── REPL.tsx # REPL 启动时的记忆加载
大家有个初步的概念即可,后面我会带着大家去看去了解的。
记忆的类型和格式
这一章主要是讲记忆的分类和格式,即最终存储的规范和模式,代码的意义只在于把内容转化为这些格式,都是逻辑代码,所以这一章我尽量少将代码,而是给例子,告诉大家记忆被存成什么样子了。
记忆的分类
在claude中,记忆不再是统一的大锅饭,而是有非常具体的模块化定义,这些定义都放在了这里src/memdir/memoryTypes.ts,记忆的定义则都在变量TYPES_SECTION_COMBINED里面,每一种记忆都会有名称、作用域、使用时机、使用方法、案例。
首先是用户记忆,构建用户画像,理解”用户是谁”以及”如何最有效地帮助他们”,需要记录用户的角色、偏好、职责或知识,而当任务需要基于用户的背景调整解释方式时,就会被拿来使用。下面是例子,记录了用户的身份、经历、近期关心的关注点等。
user: I'm a data scientist investigating what logging we have in placeassistant: [saves user memory: user is a data scientist, currently focused on observability/logging]user: I've been writing Go for ten years but this is my first time touching the React side of this repoassistant: [saves user memory: deep Go expertise, new to React and this project's frontend — frame frontend explanations in terms of backend analogues]
反馈记忆,记录用户的工作指导,保持一致性和响应性,这个记忆会在用户进行纠正、确认、验证任务执行过程的操作时进行记录,记录工作流程,成功失败的都会记录,同时也需要关注原因以及边界,甚至还包括那些“quieter confirmations”,我理解就是默认。
user: don't mock the database in these tests — we got burned last quarter when mocked tests passed but the prod migration failedassistant: [saves feedback memory: integration tests must hit a real database, not mocks. Reason: prior incident where mock/prod divergence masked a broken migration]
项目记忆,目标是理解工作的背景和动机,把握更广泛的上下文,记录谁在做什么、为什么做、何时完成,还有截止日期、法律合规要求、利益相关者需求等,也及时发现正在进行的工作、目标、bug、事件,这里还有一些细节。
-
将相对日期转绝对日期。 -
记录动机。 -
快速衰减。这里是指,要随着项目进展,敏捷更新。
user: we're freezing all non-critical merges after Thursday — mobile team is cutting a release branchassistant: [saves project memory: merge freeze begins 2026-03-05 for mobile release cut. Flag any non-critical PR work scheduled after that date]
最后是参考记忆。存储外部系统的资源指针,便于查找最新信息,如外部资源的路径、获取方式等,注意,此处只记忆查询方法,而不是内容本身。
user: check the Linear project "INGEST"if you want context on these tickets, that's where we track all pipeline bugsassistant: [saves reference memory: pipeline bugs are tracked in Linear project "INGEST"]
除此以外,项目内还约束了,有些内容是不需要记忆的,这个记录在变量WHAT_NOT_TO_SAVE_SECTION里面,甚至还很贴心的给了理由。
-
代码范式、规范、架构、文件路径或项目结构 —— 这些均可通过读取当前项目状态获取。 -
Git 历史记录、近期变更或修改人信息 —— git log/git blame才是权威依据。 -
调试方案或修复方法 —— 修复逻辑体现在代码中,提交信息已包含上下文。 -
任何已在 CLAUDE.md 文件中记录的内容。 -
临时任务细节:进行中的工作、临时状态、当前对话上下文。
记忆的格式
前文有提到,claude code是一个基于文件的记忆结构,现在我们看看,内部记忆是以一个什么样的格式来进行保存的,一个基础的记忆,是这样的一个存储方式。
~/.claude/└── projects/ └── <sanitized-project-root>/ └── memory/ # 自动记忆目录 ├── MEMORY.md # 记忆索引文件(入口) ├── user_expertise_profile.md ├── integration_testing_database_policy.md ├── mobile_release_merge_freeze.md ├── pipeline_bug_tracking_linear_project.md └── team/ # 团队记忆子目录(可选) ├── MEMORY.md ├── coding_standards.md └── api_design_principles.md
里面的user_expertise_profile.md就是一个记忆片段,他是一个markdown文件。他的存储结构,和大家熟知的SKILL.md非常接近。
---name: {{记忆名称}}description: {{一行描述 - 用于判断相关性}}type: {{user|feedback|project|reference}}---{{记忆内容}}
我以用户记忆为例,给出一个具体的记忆片段。
---name: user_expertise_profiledescription: 用户的技术背景和专业知识画像type: user---用户是数据科学家,专注于可观察性和日志分析领域。**技术栈**:- 深度 Go 语言专家(10 年经验)- React 和前端开发新手(本项目首次接触)**协作建议**:- 解释前端概念时使用后端类比- 在日志和监控相关任务中发挥其数据科学优势
这只是一份记忆片段,随着使用,这种记忆片段肯定会越来越多,肯定需要维护,尤其在实际对话过程,我们要有选择的“唤醒”记忆,而不是简单的把他们都放到模型里,于是,就需要构造索引,方便在对话过程中快速找到所需的记忆,并调取使用。
记忆索引
在每个记忆的根路径,有一个MEMORY.md文件,这便是记忆索引,是的,这部分充分证明了,此处并没有用什么数据库。
它内部每一行是就对记忆片段做一个简单的记录。
- [Title](file.md) — one-line hook
-
使用 Markdown 链接语法指向具体的记忆文件 -
破折号后跟一行描述(hook),概括该记忆的核心内容 -
无 frontmatter:索引本身不包含 YAML frontmatter
此处还有行数(200)、大小(25kb)、每行长度(<150字符)的约束。下面给出一个例子。
- [用户技术背景](user_expertise_profile.md) — 数据科学家,Go 专家,React 新手- [测试策略偏好](testing_preferences.md) — 集成测试用真实 DB,不 mock- [移动端发布冻结](mobile_release_merge_freeze.md) — 2026-03-05 起非关键合并冻结- [管道 bug 追踪](pipeline_bug_tracking_linear_project.md) — Linear 项目 "INGEST"- [API 延迟仪表板](grafana_api_latency_dashboard.md) — oncall 监控的关键指标
那么,他是如何维护,如何增删改的呢。他是每次保存新记忆时,都会自动更新,在src/services/extractMemories/prompts.ts内有提及。
## How to save memoriesSaving a memory is a two-step process:**Step 1** — write the memory to its own file using frontmatter format**Step 2** — add a pointer to that file in `MEMORY.md`. `MEMORY.md` is an index, not a memory — each entry should be one line, under ~150 characters: `- [Title](file.md) — one-line hook`. It has no frontmatter. Never write memory content directly into `MEMORY.md`.- `MEMORY.md` is always loaded into your system prompt — lines after 200 will be truncated- Organize memory semantically by topic, not chronologically- Update or remove memories that turn out to be wrong or outdated- Do not write duplicate memories. First check if there is an existing memory you can update before writing a new one.
KAIROS 日志模式
KAIROS 是 Claude Code 的助手模式(Assistant Mode),专为长生命周期会话设计。在这里会有一个日度的记忆。
**路径** (KAIROS 模式特有):```~/.claude/└── projects/ └── <sanitized-project-root>/ └── memory/ └── logs/ └── YYYY/ └── MM/ └── DD/ └── YYYY-MM-DD.md # 每日日志文件```
他有如下特点。
-
append-only(只追加)的日志流 -
/dream 技能可以将日志蒸馏为 MEMORY.md 和主题文件 -
长生命周期会话、高频交互场景 -
避免频繁更新主题文件导致的冲突
他的实现在src/memdir/memdir.ts:327-370,函数buildAssistantDailyLogPrompt,从prompt来看,主要记录如下内容。
-
用户的更正内容与偏好设置。 -
与用户相关的基本信息、其角色或目标。 -
无法从代码中推导得出的项目背景信息(截止日期、突发事件、决策及其理由)。 -
外部系统的指引信息(数据面板、Linear 项目、Slack 频道)。 -
用户明确要求你记住的任何内容。
记录完以后,大概是下面这样的格式。
# 2026-04-01- [10:30] User prefers using `bun` instead of `npm` for package management- [14:15] Database migration failed due to missing index on `users.email` — added index in PR #123- [16:45] User asked to remember: team uses Linear project "INGEST" for tracking pipeline bugs- [18:20] Refactored auth middleware to use Redis sessions instead of JWT (compliance requirement)
这个模式我是挺喜欢的,像是一个速记的日志,他能很大程度避免频繁对上述结构化的记忆进行修改,把临时的修改降级为定时的修改(通过dream模式定期更新)。
完成流程如下。
```┌─────────────────────────────────────────┐│ 长会话进行中 ││ ││ 用户: 帮我修复这个 bug ││ Claude: 正在分析... ││ 用户: 对了,记住我们用 bun 不用 npm ││ Claude: 好的,记录下来 ││ ↓ ││ FileWrite logs/2026/04/2026-04-02.md ││ "- [14:30] User prefers bun over npm" ││ ││ ... 继续对话 ... ││ ││ 用户: 数据库迁移失败了 ││ Claude: 看到错误了,修复中 ││ ↓ ││ FileWrite logs/2026/04/2026-04-02.md ││ "- [16:45] DB migration failed..." │└─────────────────────────────────────────┘┌─────────────────────────────────────────┐│ 夜间 /dream 执行 ││ ││ Phase 1: Orient ││ → ls memory/logs/2026/04/ ││ → 发现 2026-04-01.md, 2026-04-02.md ││ ││ Phase 2: Gather ││ → cat logs/2026/04/2026-04-02.md ││ → 提取: bun preference, DB migration ││ ││ Phase 3: Consolidate ││ → Update user_preferences.md ││ → Update project_context.md ││ ││ Phase 4: Prune ││ → Update MEMORY.md index ││ → (可选) 删除已整理的日志 │└─────────────────────────────────────────┘```
会话记忆
上面基本都是有一定持续性,跨对话的记忆,大家当然别忘了最基础的会话记忆,聚焦于会话本身的长期和短期记忆。他和长期的4大记忆肯定不同,然而和KAIROS 日志还不太明显,我先给出他和KAIROS 日志的差异对比。
|
|
|
|
|---|---|---|
| 作用域 |
|
|
| 文件路径 | ~/.claude/projects/<git-root>/memory/logs/YYYY/MM/YYYY-MM-DD.md |
~/.claude/session-memory.md |
| 持久性 |
|
|
| 写入方式 |
|
|
| 触发时机 |
|
|
| 内容格式 |
|
|
| 主要用途 |
|
|
| 整理机制 |
|
|
| 激活条件 | feature('KAIROS') && getKairosActive() |
feature('tengu_session_memory') |
| 适用场景 |
|
|
他的构造会相对比较简单一些。
-
触发。当上下文窗口超过阈值时,系统优先使用会话记忆进行压缩,构造Session Memory( ~/.claude/session-memory.md)。 -
这个结构是存在模板的,包括标题、当前状态、任务说明、文件和函数、工作流程、错误和修正、代码库和系统文档、经验总结、关键结果、工作记录。 -
更新 session-memory.md。
prompt内,大概是如下内容。
-
只根据用户真实对话内容更新会话笔记文件,不包含本指令、系统提示、CLAUDE.md、历史摘要等信息。 -
仅使用编辑工修改 {{notesPath}}文件,一次性并行完成所有编辑,不调用其他工具,编辑完就停止。 -
关键文件格式内容。 -
必要的约束。 -
如单节内容控制在约 2000 token / 词 以内,超了就精简 -
内容聚焦可执行、可复现的工作信息。 -
必须更新 Current State,保证流程连贯。 -
文件超过 12000 token 时必须压缩。
核心流程
上面主要讲的是记忆的类型和格式,是静态的存储,下面开始讲流程,记忆是怎么触发、提取、检索和更新的。
加载
启动时加载
在src/screens/REPL.tsx里面(Read-Eval-Print Loop),启动服务的时候会初始化。
// 启动时预加载所有记忆文件到 readFileState 缓存const memoryFiles = await getMemoryFiles()for (const file of memoryFiles) {// 如果内容与磁盘不同(被截断/剥离 frontmatter)// 缓存原始磁盘字节,标记为部分视图 readFileState.current.set(file.path, { content: file.contentDiffersFromDisk ? file.rawContent ?? file.content : file.content, timestamp: Date.now(), isPartialView: file.contentDiffersFromDisk })}
这个getMemoryFiles,是src/utils/claudemd.ts内的工具函数,加载Memory文件的,把各类型的记忆和记忆索引都给加载到内存里。
exportasyncfunctiongetMemoryFiles(includeExternal = false): Promise<MemoryFileInfo[]> {const result: MemoryFileInfo[] = []// 收集所有记忆源if (isAutoMemoryEnabled()) {const { info: memdirEntry } = await safelyReadMemoryFileAsync( getAutoMemEntrypoint(), // ~/.claude/projects/<path>/memory/MEMORY.md'AutoMem', )if (memdirEntry) result.push(memdirEntry) }if (feature('TEAMMEM') && teamMemPaths!.isTeamMemoryEnabled()) {const { info: teamMemEntry } = await safelyReadMemoryFileAsync( teamMemPaths!.getTeamMemEntrypoint(), // ~/.claude/team/memory/MEMORY.md'TeamMem', )if (teamMemEntry) result.push(teamMemEntry) }return result}
执行前加载
在每次用户发起新查询之前,系统会重新读取记忆文件,将最新的 MEMORY.md 索引和记忆内容注入到系统提示中。这个触发是在context.ts:getUserContext内。
const getUserContext = defineMemoizedGetter('getUserContext',async () => {const startTime = Date.now()// 禁用检查const shouldDisableClaudeMd = isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_CLAUDE_MDS) || (isBareMode() && getAdditionalDirectoriesForClaudeMd().length === 0)// 获取并过滤记忆文件const claudeMd = shouldDisableClaudeMd ? null : getClaudeMds(filterInjectedMemoryFiles(await getMemoryFiles()))// 缓存结果供后续使用 setCachedClaudeMdContent(claudeMd || null)return { ...(claudeMd && { claudeMd }), currentDate: `Today's date is ${getLocalISODate()}`, } },)
留意到,这里依旧会去跑一次getMemoryFiles。
这里对比一下启动前和执行前的加载有什么区别。
|
|
|
|
|---|---|---|
| 时机 |
|
|
| 目的 |
|
|
| 频率 |
|
|
| 数据来源 |
|
|
| 作用 |
|
|
主要是因为,记忆文件可能在会话期间被修改,而每次查询都是需要知道最新的记忆的,此时可以通过缓存机制平衡性能和实时性。
注入
然后,我们看看整个记忆是怎么被注入到prompt内并被使用的,调用逻辑在这里。
用户输入查询 ↓REPL.tsx:2535 - 并行获取上下文 ├── getSystemPrompt(...) // 构建基础系统提示 ├── getUserContext() // 加载记忆文件 └── getSystemContext() // Git 状态等 ↓prompts.ts:495 - getSystemPrompt 内部 systemPromptSection('memory', () => loadMemoryPrompt()) ↓memdir.ts:419 - loadMemoryPrompt() 此处生成记忆系统的使用指令(如何保存、四种类型、MEMORY.md 索引内容) ├── 检查 KAIROS 模式 → buildAssistantDailyLogPrompt() ├── 检查 TEAMMEM → buildCombinedMemoryPrompt() └── 默认 → buildMemoryLines().join('\n') ↓memdir.ts:199 - buildMemoryLines() ├── 生成记忆指令(如何保存、四种类型、不应保存的内容) ├── buildSearchingPastContextSection(memoryDir) ← 关键! │ └── 读取 MEMORY.md 索引文件 │ └── 扫描最多 200 个记忆文件 │ └── 格式化输出到 prompt └── 返回字符串数组 ↓systemPrompt.ts:115 - buildEffectiveSystemPrompt() asSystemPrompt([ agentSystemPrompt || customSystemPrompt || defaultSystemPrompt, ...(appendSystemPrompt ? [appendSystemPrompt] : []) ]) ↓claude.ts:1358 - 最终组装 systemPrompt = asSystemPrompt([ getAttributionHeader(fingerprint), getCLISyspromptPrefix({...}), ...systemPrompt, // ← 这里包含记忆内容 ...(advisorModel ? [ADVISOR_TOOL_INSTRUCTIONS] : []), ...(injectChromeHere ? [CHROME_TOOL_SEARCH_INSTRUCTIONS] : []), ].filter(Boolean)) ↓claude.ts:1376 - 发送给 API const system = buildSystemPromptBlocks(systemPrompt, enablePromptCaching, {...})
最后的systemPrompt,就会有如下内容。
// systemPrompt 数组包含:[ baseSystemPrompt, // 模型行为定义 toolDefinitions, // 工具定义 loadMemoryPrompt(), // ← 记忆使用指令 + MEMORY.md 索引 getClaudeMds(), // ← 记忆文件实际内容 currentDate, // 当前日期]
如此一来,大家应该能想象到,token的消耗是多么的可怕。
提取
整个提取的过程如下。
handleStopHooks (stopHooks.ts) ↓executeExtractMemories (extractMemories.ts) ↓runExtraction (closure function) ├── 检查互斥性 (hasMemoryWritesSince) ├── 节流检查 (turnsSinceLastExtraction) ├── 扫描现有记忆 (scanMemoryFiles) ├── 构建 Prompt (buildExtractAutoOnlyPrompt / buildExtractCombinedPrompt) └── 执行分叉agent (runForkedAgent) ├── 创建受限工具集 (createAutoMemCanUseTool) ├── 注入现有记忆清单 ├── 执行 LLM 对话(最多 5 轮) ├── 写入记忆文件 └── 更新 MEMORY.md 索引 ↓记录提取事件 (logEvent) ↓追加系统消息("已保存 X 个记忆")
触发条件
记忆的提取,肯定不是随意的,这里handleStopHooks内有大量提取约束的逻辑。总结下来大概有如下内容。
-
源限制。仅在主线程查询 ( repl_main_thread) 或 SDK 调用时触发,子agent不触发。 -
特性门控。可配置的记忆模式触发逻辑。代码里大家会看到 feature('EXTRACT_MEMORIES')、isExtractModeActive()之类的控制。 -
节流。并非每一轮都会触发,需要满足特定轮数积累才会触发。 -
互斥。如果已经提取过就不提了。 -
特定场景约束,例如 /compact、/rewind指令时,上下文压缩执行后的第一轮,特定配置下就不触发。 -
用户可以通过命令 /memory save来触发。
具体的,在内部,会触发这个提取的,会有如下情况。
-
主agent直接写入,即用户明确要求、主动识别,“记住这个”、“把这个加到记忆中”时,就会触发主agent写入。 -
后台提取agent,这个在每次查询结束的时候触发。 -
/dream机制触发。
提取prompt构造
这里的记忆提取,是通过大模型来完成的,那prompt就是重点工作了。简单地可以这么总结。
const userPrompt = teamMemoryEnabled ? buildExtractCombinedPrompt(newMessageCount, existingMemories, skipIndex) : buildExtractAutoOnlyPrompt(newMessageCount, existingMemories, skipIndex)// Prompt 结构:// 1. 角色定义:记忆提取子agent// 2. 可用工具:Read/Grep/Glob/只读 Bash/Edit/Write(仅限记忆目录)// 3. 分析范围:最近 ~N 条消息// 4. 四种记忆类型的完整指令(XML 格式)// 5. 两步保存流程(写文件 → 更新索引)
里面大概是这样的。
exportfunctionbuildExtractAutoOnlyPrompt( newMessageCount: number, existingMemories: string, skipIndex = false,): string{return [ opener(newMessageCount, existingMemories), // 1. 开场白 + 工具限制 + Turn 预算'','If the user explicitly asks you to remember something, save it immediately as whichever type fits best. If they ask you to forget something, find and remove the relevant entry.', // 2. 显式保存指令'', ...TYPES_SECTION_INDIVIDUAL, // 3. 四种记忆类型的 XML 指令,前文有提及 ...WHAT_NOT_TO_SAVE_SECTION, // 4. 不应保存的内容,前文有提及'', ...howToSave, // 5. 两步保存流程(写文件 + 更新索引),前文有提及 ].join('\n')}
继续往深处看。opener有这些内容。(不粘贴代码了,直接说里面有什么)
-
角色限定。 You are now acting as the memory extraction subagent -
分析范围。 Analyze the most recent ~${newMessageCount} messages above and use them to update your persistent memory systems. -
可用工具: Read/Grep/Glob/只读 Bash/Edit/Write(仅限记忆目录) -
轮数预算约束。要求2轮完成记忆提取任务,这里是指第一轮并行读取所有记忆 FileRead,第二轮并行执行所有FileWrite/FileEdit调用。 -
内容来源限制。只能使用最近 ~N 条消息的内容、禁止 grep源码、阅读代码验证模式、执行git命令。 -
如果存在记忆文件,预注入清单,避免提取agent浪费轮数执行。
工具的权限控制,这里单独把代码拿出来,大家可以看看这个约束是什么样的。
exportfunctioncreateAutoMemCanUseTool(memoryDir: string): CanUseToolFn{returnasync (tool: Tool, input: Record<string, unknown>) => {// Read/Grep/Glob 无限制if (tool.name === FILE_READ_TOOL_NAME || tool.name === GREP_TOOL_NAME || tool.name === GLOB_TOOL_NAME) {return { behavior: 'allow' } }// Bash 仅限只读命令if (tool.name === BASH_TOOL_NAME) {if (tool.isReadOnly(input)) {return { behavior: 'allow' } }return denyAutoMemTool(tool, 'non-read-only bash command') }// Edit/Write 仅限记忆目录if (tool.name === FILE_EDIT_TOOL_NAME || tool.name === FILE_WRITE_TOOL_NAME) {const filePath = input.file_path asstringif (filePath.startsWith(memoryDir)) {return { behavior: 'allow' } }return denyAutoMemTool(tool, `write outside memory dir: ${filePath}`) } }}
dream机制
重点说一下dream机制,这是一个定期执行的任务,会定期的对日志进行数据的梳理,而不会伴随query请求和执行来触发。
这里,会有3层门控来进行控制。autoDream.ts
// 三重门控机制(按成本从低到高检查)// 1. 时间门控:距离上次整理 >= minHours(默认 24 小时)const hoursSince = (Date.now() - lastAt) / 3_600_000if (!force && hoursSince < cfg.minHours) return// 2. 扫描节流:避免频繁扫描会话目录const sinceScanMs = Date.now() - lastSessionScanAtif (!force && sinceScanMs < SESSION_SCAN_INTERVAL_MS) return// 3. 会话门控:自上次以来有 >= minSessions 个新会话let sessionIds = await listSessionsTouchedSince(lastAt)sessionIds = sessionIds.filter(id => id !== currentSession) // 排除当前会话if (!force && sessionIds.length < cfg.minSessions) return// 4. 锁机制:确保单进程执行let priorMtime = await tryAcquireConsolidationLock()
在经过这个判断后,就会开始进行dream。dream内部是一个完整的prompt去执行的,主要有如下内容,prompt我就不粘上来了。
-
了解现状,通过 MEMORY.md以及对应内部的内容。 -
收集近期信号,寻找值得持久保存的信息,一般按照每日日志、发生漂移的已有记忆、对话记录检索的顺序进行查看。 -
整合,对每条值得记忆的内容,在记忆目录根目录下新建或更新对应记忆文件,重点关注避免创建近似重复文件、把相对日期改成绝对日期、删除已被更新的内容。 -
修剪或者建立索引,即更新 MEMORY.md。
这里我把一般的记忆和dream做一个对比。
|
|
|
|
|---|---|---|
| 触发频率 |
|
|
| 作用范围 |
|
|
| 执行时机 |
|
|
| 处理深度 |
|
|
| 视野 |
|
|
| 主要动作 |
|
|
| 工具权限 |
|
|
| 用户可见性 |
|
|
查询
记忆存在文件里,我们在合适的时候要把他调取出来,具体这个调用流程是这样的。
用户输入查询 ↓attachments.ts:2196 - getRelevantMemoryAttachments() ├── 提取 Agent @mentions(如果有) ├── 确定搜索目录(agent memory 或 auto memory) └── 并行搜索多个目录 ↓findRelevantMemories.ts:39 - findRelevantMemories() ├── scanMemoryFiles() // 扫描最多 200 个记忆文件 ├── filter(alreadySurfaced) // 过滤已展示的记忆 └── selectRelevantMemories() // Sonnet 侧边查询 ↓memoryScan.ts:84 - formatMemoryManifest() └── 格式化记忆清单:`- [type] filename (timestamp): description` ↓sideQuery API - Sonnet 模型选择 ├── system: SELECT_MEMORIES_SYSTEM_PROMPT ├── user: Query + Available memories + Recently used tools └── output: JSON Schema → { selected_memories: [...] } ↓findRelevantMemories.ts:74 - 返回结果 └── 映射文件名 → 绝对路径 + mtime ↓attachments.ts:2236 - readMemoriesForSurfacing() ├── 过滤已读过的文件(readFileState) ├── 限制最多 5 个 └── 读取文件内容(带截断保护) ↓创建 attachment: relevant_memories ↓注入到系统提示(作为 attachments 部分) ↓主模型看到完整的记忆内容(带 freshness 标记)
此处,getRelevantMemoryAttachments是整个记忆检索的关键流程,总结下来就是这些内容。
-
确定检索目录。 -
并行搜索多个目录。 -
合并结果并去重。 -
读取文件内容。
其中,我们最关注的,应该是并行搜索多个目录这一步,函数是findRelevantMemories。这里的大致流程。
-
扫描记忆文件(每个路径最多 200 个)。同时,还会提前过滤已展示的, alreadySurfaced会记录。 -
调用 Sonnet 进行选择。 -
记录选择率、空选择比例等指标。 -
将文件名映射回完整路径 + mtime。
注意,此处的记忆选择,是通过大模型Sonnet来选择的,在selectRelevantMemories函数内。prompt的设计同样有说法,为了方便阅读,我翻译成了中文。
你需要挑选出在 Claude Code 处理用户查询时有用的记忆内容。系统会提供用户查询,以及一组包含文件名和描述的可用记忆文件列表。返回 Claude Code 处理该查询时明显有用的记忆文件名列表(最多 5 个)。仅根据文件名与描述,把你确定有帮助的记忆加入列表。- 若不确定某条记忆是否有助于处理用户查询,则不要列入。请谨慎筛选、严格判断。- 若列表中没有明显有用的记忆,可返回空列表。- 若提供了近期使用过的工具列表,不要选择这些工具的用法参考或 API 文档类记忆(Claude Code 已在使用这些工具)。但仍需选择包含这些工具的警告、注意事项或已知问题的记忆 —— 工具正在使用时,这类信息恰恰最为关键。
而`user message部分,我也给个例子。
Query: 帮我修复数据库连接超时的问题Available memories:- [user] user_expertise_profile.md (2026-04-01T10:30:00.000Z): User is a senior backend engineer with deep PostgreSQL expertise- [feedback] testing_preferences.md (2026-03-28T15:45:00.000Z): Integration tests must hit real database, not mocks. Reason: prior incident where mock/prod divergence masked broken migrationsRecently used tools: FileReadTool, GrepTool
预期输出是这样的。
{"selected_memories": ["connection_pool_config.md","db_monitoring_grafana_dashboard.md","preferred_debugging_tools.md","testing_preferences.md" ]}
另外,还会再做个后校验,确保选择的记忆都在列表里,避免幻觉。
const parsed: { selected_memories: string[] } = jsonParse(textBlock.text)return parsed.selected_memories.filter(f => validFilenames.has(f))// 只返回存在于原始列表中的文件名(防止幻觉)
这里也体现了渐进式披露的思想,在做记忆选择的时候,只考虑最简单的摘要,也就是前文所说的索引。
选择完以后,就需要注入到推理的prompt里面了,在主流程里,大模型看到有关记忆的部分是这样的。
## Relevant Memories[user] user_expertise_profile.md (2 days ago)_Some content here..._[feedback] testing_preferences.md (5 days ago)_Some content here..._> This memory is 5 days ago. Memories are point-in-time observations,> not live state — claims about code behavior or file:line citations> may be outdated. Verify against current code before asserting as fact.
记忆维护挑战
在核心流程的最后,讲一下记忆维护的挑战。一般的,记忆库的维护会有如下3个难点。
-
内容冲突。多个记忆描述同一事物,会存在冲突。 -
信息过时。记忆内容过于陈旧。 -
重复冗余。相似或相同的记忆存多份
下面我们来看看这些难点claude code是怎么做的。
冲突处理
对于问题,我们一般关注亮点,检测和处理。
检测上,会在创建时(prompt内要求,extractMemories/prompts.ts 和 memdir.ts)、dream模式整理时(autoDream/consolidationPrompt.ts)触发检测。
在发现了冲突后,会遵循如下逻辑进行处理。
-
最新胜出策略。主要是针对同一事物状态冲突的处理。 -
特异性优先。更具体的记忆更有价值。 -
合并去重。相似内容合并为一个文件。 -
明确适用范围。看似冲突的记忆可能是适用范围不同。
信息过时
代码里会做新鲜度检测,对大于1天的添加 <system-reminder> 标签,代码在读取到记忆的时候也会做过期检验,在这/dream模式内也会有,另外则是用户通过prompt的直接提醒。
过时信息的处理如下。
1. 检测到潜在过时 ↓ - 新鲜度警告 (>1 天) - 用户纠正 - /dream 扫描发现矛盾 - 模型使用时发现冲突 ↓2. 验证当前状态 ↓ - 读取相关代码文件 - 检查 git 历史 - 对比记忆内容 ↓3. 判断是否需要更新 ↓ - 确认过时 → 进入步骤 4 - 仍然有效 → 保持不动 ↓4. 执行更新操作 ↓ - 选项 A: 更新现有文件(保留历史信息) - 选项 B: 删除过时文件(完全移除) - 选项 C: 标记为历史参考(添加 deprecated 标记) ↓5. 更新索引 ↓ - 修改 MEMORY.md - 移除过时条目 - 添加新条目(如有) ↓6. 记录遥测 ↓ - tengu_memdir_file_edit - tengu_extract_memories_extraction - tengu_auto_dream_consolidation
思考
源码读下来,收获还是很大的,里面很多小操作都很让人眼前一亮,例如记忆的分类拆解模式,KAIROS日志模式、dream模式、记忆内容的边界约束等,很有借鉴的价值,能很明显感受到开发人员的开发实力和思路的开阔。
不过可能是因为项目还是比较新,功能的开发比较赶,所以没有足够的打磨和优化,未来还有很多提升的空间。
-
现在的记忆个数,约束在200,更多是权宜之计,因为Sonnet吃不下,日志一多效果就容易查。后续估计会借助搜索策略可以缩小范围,减少大模型约束的能力。 -
本地存储,从安全角度是个不错的方案,代码也确实是比较敏感,但在更多领域,我还是更喜欢云端的存储方案,用数据库存储增删改查起来会更方便。 -
记忆的模式还是相对单一,分字段分类型还可以进一步细拆,尤其对于code场景,甚至其他场景,定制的更加精细会对后续的优化有更多提升。 -
很多动作,都是简单的依靠大模型来处理,好消息是很泛用也很简单,可能处理的轮数会很多,很浪费token,时间也很长,效果层面上限也不会很高,在更多更加精细的应用上,微调模型说不定可以有更多的提升价值。
推荐阅读
加入AIGCmagic社区知识星球
AIGCmagic社区里涵盖了海量的AIGC面试面经资源、内推招聘资讯、面试专业答疑、面试干货知识汇总、AIGC商业变现项目集合(AIGC、AI Agent、传统深度学习、自动驾驶、机器学习、计算机视觉、自然语言处理、具身智能、元宇宙、SLAM等)。
那该如何加入星球呢?很简单,我们只需要扫下方的二维码即可。与此同时,我们也重磅推出了知识星球2025年惊喜价:原价199元,前200名限量立减50!特惠价仅149元!(每天仅4毛钱)
时长:一年(从我们加入的时刻算起)


夜雨聆风