news 2026/10/21 17:18:48

Claude Code 记忆系统源码分析:extractMemories 与 autoDream 如何协作

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Claude Code 记忆系统源码分析:extractMemories 与 autoDream 如何协作

1. 从一次“记忆丢失”说起:extractMemories 与 autoDream 到底在做什么

如果你用 Claude Code 写过几天代码,大概率遇到过这种场景:昨天刚跟它强调过“这个仓库的集成测试不要 mock 数据库”,今天开新会话,它又给你写了一个 mock 版本。你以为是模型变笨了,其实是记忆系统没把这条信息接住。

Claude Code 的记忆系统是一套文件驱动的外部记忆机制,核心不在内存里,而在磁盘上。它由两个关键机制协作完成:extractMemories负责“每轮对话结束后把值得记的东西写下来”,autoDream负责“定期把积累的记忆整理、合并、裁剪”。一个管增量写入,一个管存量整理,两者靠时间分离和独立锁避免打架。

这套机制适合谁?适合所有想让 Claude Code 在长期项目里保持上下文一致性的开发者,尤其是多 worktree、多会话并行、需要跨天延续上下文的团队。它不适合把记忆当数据库用的场景——因为它的写入没有并发保护,索引有 200 行硬上限,compact 之后还有游标边界问题。

我试过在自己的一个 Go + React 混合项目里复现整套读写流程,踩了几个坑,下面按“配置 → 触发 → 验证 → 排障”的顺序拆开讲。先给结论:记忆目录默认在~/.claude/projects/<sanitized-git-root>/memory/,MEMORY.md是纯索引,正文在独立的.md文件里,每轮对话结束后后台分叉代理负责提取,满足“距上次整理 ≥24h 且新增 ≥5 个会话”才触发自动整理。

理解这套链路之前,先记住一个数字:5。相关记忆筛选最多注入 5 条,提取代理最多跑 5 轮。这个数字贯穿全文,很多边界问题都跟它有关。

2. 前置准备:TaoToken 接入与记忆目录初始化

在复现记忆读写流程之前,得先让 Claude Code 能正常跑起来。我用的是 TaoToken 作为模型接入层,它的 Base URL 和 Key 配置方式和 Anthropic 官方兼容,改一下环境变量就能用。

先拿 Key。打开 https://taotoken.net/api-keys ,创建一个 API Key,复制出来。注意这个 Key 只在创建时完整显示一次,丢了就得重建。

然后配置 Claude Code 的接入。Claude Code 读取的是环境变量,最稳妥的方式是写进 shell 配置文件,而不是每次手动 export。以 zsh 为例,编辑~/.zshrc:

export ANTHROPIC_BASE_URL="https://taotoken.net/api" export ANTHROPIC_AUTH_TOKEN="sk-你的TaoToken密钥" export ANTHROPIC_MODEL="claude-sonnet-4-5-20250929"

这里有个容易踩的坑:ANTHROPIC_BASE_URL不要带末尾斜杠,也不要带/v1,Claude Code 内部会自己拼路径。我第一次配的时候多写了个/v1,结果所有请求 404,排查了半小时。

如果你用的是 Codex 或 Cline 这类工具,配置位置不一样。Codex 读的是~/.codex/auth.json,结构长这样:

{ "OPENAI_API_KEY": "sk-你的TaoToken密钥", "OPENAI_BASE_URL": "https://taotoken.net/api" }

Cline 的 MCP 配置则在 VS Code 的 settings 里,走的是cline_mcp_settings.json。不管哪个工具,三件套必须齐全:Base URL + Key + Model ID。少一个都会报 401 或 model not found。

配置完先验证一下能不能通。用 curl 直接打一次模型对话接口:

curl https://taotoken.net/api/v1/messages \ -H "x-api-key: sk-你的TaoToken密钥" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{ "model": "claude-sonnet-4-5-20250929", "max_tokens": 64, "messages": [{"role": "user", "content": "回复 OK 两个字母"}] }'

返回里能看到content数组带一段文本,就说明接入通了。这一步不通,后面记忆系统根本不会触发,因为 extractMemories 是挂在对话结束钩子上的。

接入通了之后,确认记忆目录。默认路径是~/.claude/projects/<sanitized-git-root>/memory/,其中sanitized-git-root是把 git 根路径里所有非字母数字字符替换成-。比如你的项目在/Users/zhang/my-project,目录就是-Users-zhang-my-project。

想改路径有两个办法。一是环境变量CLAUDE_COWORK_MEMORY_PATH_OVERRIDE,优先级最高;二是在settings.json里配autoMemoryDirectory,支持~/展开。注意autoMemoryDirectory只信任 policy/local/user 来源,不信任提交到仓库的projectSettings——这是防止恶意仓库把记忆目录重定向到~/.ssh之类的敏感位置。

初始化目录:

mkdir -p ~/.claude/projects/-Users-zhang-my-project/memory touch ~/.claude/projects/-Users-zhang-my-project/memory/MEMORY.md

MEMORY.md必须是纯索引,每行一条指针,格式固定:

- [用户角色](user_role.md) — 数据科学家,关注可观测性 - [测试规范](feedback_testing.md) — 集成测试必须用真实数据库

上限 200 行 / 25KB,超了会被截断并注入警告。这个上限后面会引出“孤儿记忆”问题,先记住。

3. 可复制配置:extractMemories 与 autoDream 的触发参数

这一节给可直接复制的配置片段。记忆系统的行为受settings.json和几个特性开关控制,我把关键项列出来。

先看settings.json里跟记忆相关的部分。路径是~/.claude/settings.json(用户级)或项目级.claude/settings.json:

{ "autoMemory": true, "autoDream": { "enabled": true, "minHours": 24, "minSessions": 5 }, "autoMemoryDirectory": "~/.claude/projects/-Users-zhang-my-project/memory" }

autoMemory控制 extractMemories 的总开关。autoDream.enabled控制自动整理,minHours和minSessions是双重门控——必须同时满足“距上次整理 ≥24 小时”和“新增 ≥5 个会话”才会触发。这两个值实际由远程特性tengu_onyx_plover控制,本地配置只是默认值。

如果你用 KAIROS 助手模式(assistant: true),autoDream 会被禁用,改由/dream技能按计划任务执行。KAIROS 模式下记忆写入走的是追加日志:

# 自动记忆 本会话是长期运行的。工作过程中,将任何值得记住的内容追加写入今日的日志文件: {memoryDir}/logs/YYYY/MM/YYYY-MM-DD.md

日志是纯追加的,不重写不整理,由夜间进程蒸馏成MEMORY.md和主题文件。日志路径写成YYYY/MM/YYYY-MM-DD.md模式而不是当天实际路径,是因为这段提示词会被 prompt cache 缓存,跨午夜不会失效——模型从currentDate上下文附件里拿当前日期。

再看记忆文件的格式。每个记忆文件都有 YAML frontmatter:

--- name: 测试规范 description: 集成测试必须使用真实数据库,禁止 mock type: feedback --- 规则:集成测试必须连接真实数据库。 Why: 上个季度 mock 测试通过但生产迁移失败,造成事故。 How to apply: 所有涉及数据库迁移的测试用例,禁止使用 mock 框架。

type有四种:user、feedback、project、reference。其中feedback和project强制要求正文包含Why和How to apply,目的是让未来的模型能判断边界情况,而不是盲目执行规则。

description字段很关键——它是findRelevantMemories筛选时唯一读取的内容,只读 frontmatter 前 30 行,不加载正文。所以description写得准不准,直接决定这条记忆会不会被选中注入。

extractMemories 的触发挂在对话结束钩子上,代码逻辑大致是:

// 每轮对话结束后 if (inProgress) return inProgress = true try { await executeExtractMemories() } finally { inProgress = false }

inProgress是进程内互斥锁,防止同一会话多轮同时触发。但它对跨进程的 autoDream 无效,所以 autoDream 另有一套跨进程文件锁.consolidate-lock。

autoDream 的抢锁逻辑用 PID 文件加二次验证:

await writeFile(lockPath, String(process.pid)) const verify = await readFile(lockPath, 'utf8') if (parseInt(verify.trim(), 10) !== process.pid) return null

两个进程同时抢锁时,最后写入 PID 的胜出,另一个回退。这是乐观锁,不是原子操作,没有 flock,但最终一致性可接受。

配置完这些,重启 Claude Code 让环境变量生效。接下来验证触发链路。

4. 验证请求:观察记忆读写与成功结果

配置好之后,怎么确认 extractMemories 真的在写记忆?我用的办法是手动制造一条明确的 feedback,然后看目录里有没有新文件。

第一步,开一个新会话,给 Claude Code 一条明确的纠正指令:

不要在这个项目里用 npm,统一用 bun。之前混用导致 lock 文件冲突过。

第二步,等它回复完,去看记忆目录:

ls -la ~/.claude/projects/-Users-zhang-my-project/memory/

正常情况下,几秒内会多出一个feedback_*.md文件,MEMORY.md里也会多一行指针。如果没出现,先检查autoMemory是不是 true,再看特性开关EXTRACT_MEMORIES有没有启用。

第三步,验证记忆文件内容:

cat ~/.claude/projects/-Users-zhang-my-project/memory/feedback_package_manager.md

应该能看到 frontmatter 里type: feedback,正文里有Why和How to apply。如果只有规则没有 Why,说明提取代理没按提示词要求写,这种情况在早期版本里出现过。

第四步,验证相关记忆注入。开一个新会话,问一个跟包管理器相关的问题:

这个项目装依赖用什么命令?

Claude Code 在会话开始时会加载MEMORY.md进系统提示,会话中通过findRelevantMemories用 Sonnet 从全量记忆文件里筛最多 5 条相关记忆。筛选时只读 frontmatter,按修改时间排序,最多扫 200 个文件。

筛选用的系统提示词里有一句关键约束:

Return a list of filenames for the memories that will clearly be useful... (up to 5). Only include memories that you are certain will be helpful.

也就是说,不确定有用的不选,宁缺毋滥。如果返回空列表,说明 Sonnet 判断没有明确相关的记忆。

第五步,验证 autoDream。手动触发/dream技能,观察四阶段整理:

/dream

整理提示词分四个阶段:定向(ls 记忆目录、读 MEMORY.md)、收集信号(看日志、找漂移的记忆、grep transcript)、整理(合并到现有主题文件、相对日期转绝对日期、删除被推翻的事实)、裁剪索引(更新 MEMORY.md 保持在 200 行 / 25KB 以内)。

整理完会返回一份摘要,说明合并、更新、裁剪了哪些内容。如果记忆已经很紧凑,它会说“无需变更”。

自动触发的话,得等 24 小时且新增 5 个会话。想快速验证可以把minHours和minSessions临时调小,但注意这两个值可能被远程特性覆盖,本地改不一定生效。

验证过程中有个细节值得注意:findRelevantMemories的筛选结果不持久化,只是临时拼成清单发给 Sonnet,用完即丢。它跟MEMORY.md是两个东西——MEMORY.md是持久化索引,加载进系统提示;筛选清单是内存里临时拼的,不落盘。

5. 常见报错排查:401、local proxy failed 与游标问题

这一节对照真实报错。记忆系统本身不直接报网络错,但接入层出问题会导致 extractMemories 根本不触发,因为对话都没跑完。

报错一:401 Unauthorized

{"type":"error","error":{"type":"authentication_error","message":"invalid x-api-key"}}

原因通常是 Key 没配对,或者 Base URL 带了多余路径。检查三件套:

echo $ANTHROPIC_BASE_URL # 应该是 https://taotoken.net/api echo $ANTHROPIC_AUTH_TOKEN # 应该是 sk- 开头

如果 Base URL 末尾有/v1或斜杠,去掉。如果 Key 是从别处复制的,注意有没有多余空格。

报错二:local proxy failed

Error: local proxy failed to connect

这个报错通常出现在工具层配置了本地代理但代理没起来。检查你的工具配置里有没有指向localhost的代理设置,有的话去掉,直接用 TaoToken 的 Base URL。Claude Code 本身不需要本地代理。

报错三:reading 'choices' of undefined

TypeError: Cannot read properties of undefined (reading 'choices')

这是 OpenAI 格式和 Anthropic 格式混用导致的。Claude Code 走的是 Anthropic Messages API,返回结构是content数组,不是choices。如果你在 Codex 或 Cline 里配了 Anthropic 的 Base URL 但工具按 OpenAI 格式解析,就会报这个。检查工具的 API 格式设置,或者换用对应的端点。

报错四:OAuth token expired

OAuth token has expired, please re-authenticate

如果你之前用 OAuth 登录过官方账号,环境变量里的ANTHROPIC_AUTH_TOKEN可能被 OAuth token 覆盖。清掉 OAuth 缓存:

rm -rf ~/.claude/oauth

然后重新用 API Key 配置。

记忆系统特有的问题:记忆没写入

对话正常但记忆目录没新文件,排查顺序:

先看autoMemory是不是 true。再看主代理是不是已经直接写过记忆——hasMemoryWritesSince()会检测主代理是否已写过,写过就跳过提取,避免重复。如果你在对话里明确让 Claude Code “记住这个”,它可能直接写了,后台提取就不再跑。

再看提取代理的 5 轮上限。maxTurns: 5是硬上限,策略是第 1 轮并行 Read、第 2 轮并行 Write。如果一次对话触及 10 个主题,5 轮内写不完,未写完的记忆会静默丢失——runForkedAgent正常返回,游标正常推进,没有截断检测或补偿机制。

游标与 compact 的边界问题

lastMemoryMessageUuid是提取代理的游标,记录上次处理到哪条消息。正常完成时游标推进,异常时游标不推进,下轮重新处理。

但 compact(上下文压缩)之后有个未定义行为:compact 把旧消息压缩成摘要,原始消息从context.messages里删除,而游标指向的 UUID 可能已经不在消息列表里。postCompactCleanup清理了 microcompact 状态、context collapse、内存文件缓存,但没有重置lastMemoryMessageUuid。compact 后首次提取的行为取决于countModelVisibleMessagesSince找不到游标时的降级逻辑,源码没有明确处理这个边界。

实际影响:compact 前未被提取的消息细节可能永久丢失。规避办法是在长对话里定期手动触发/dream,或者主动让 Claude Code 把重要信息写进记忆文件,别等后台提取。

多 worktree 并发覆盖

同一 git 仓库的所有 worktree 共享同一个memory/目录。autoDream 有跨进程文件锁保护,但 extractMemories 的记忆文件写入完全没有并发保护。两个 worktree 的提取代理同时写feedback_testing.md,后写的覆盖先写的,无合并无冲突检测。

规避办法:多 worktree 并行时,尽量错开对话结束时间,或者手动指定不同的autoMemoryDirectory。这是设计取舍,源码注释里有“same repo shares memory dir”的说明,但没有承认覆盖风险。

6. 语义一致 CTA:把记忆系统接进你的长期项目

整套链路跑通之后,你会发现 Claude Code 的记忆系统本质上是“文件 + 提示词 + 后台代理”的组合,没有黑魔法。extractMemories 管增量,autoDream 管存量,findRelevantMemories 管按需注入,三者靠时间分离和独立锁协作。

如果你想在自己的项目里稳定用起来,几个实操建议:

把MEMORY.md当索引维护,别往里塞正文。每行一条指针,控制在 150 字符以内。正文写进独立的.md文件,description字段写准,因为筛选时只读这个。

feedback和project类型的记忆,务必写清Why和How to apply。没有 Why 的规则,未来的模型没法判断边界情况,只能盲目执行,反而容易出错。

长对话记得手动/dream,别完全依赖 24 小时 + 5 会话的自动触发。compact 之后的游标边界问题目前没有确定性修复,主动整理更稳。

多 worktree 并行时注意写入覆盖风险,必要时给每个 worktree 配独立的记忆目录。

接入层用 TaoToken 的话,Base URL 固定https://taotoken.net/api,Key 从 https://taotoken.net/api-keys 拿,模型 ID 按需选。三件套配齐,401 和格式错基本就没了。想验证模型对话是否正常,可以直接在 https://taotoken.net/api 的对话入口试一条消息,确认返回结构是content数组而不是choices。

长期跑编码任务或 Agent 的话,Coding Plan 比按量计费更划算,适合把记忆系统当基础设施用的场景。接入文档在 https://taotoken.net/doc ,里面有各工具的完整配置示例。

最后留一个我踩过的坑:别把autoMemoryDirectory配到项目仓库里。它只信任 policy/local/user 来源,不信任projectSettings,配在.claude/settings.json里提交到仓库是不生效的,而且有安全风险。配在用户级~/.claude/settings.json里最稳。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/22 4:17:01

运维总监方法论:故障管理、容量规划与团队建设的实战指南

1. 这活儿根本不是技术岗&#xff0c;是决策岗干了十几年运维&#xff0c;从救火队员一路做到总监&#xff0c;我最大的感悟是&#xff1a;很多人对这个岗位的理解从一开始就偏了。大家总觉得运维总监应该是技术最牛的那个人&#xff0c;什么故障都能秒级定位&#xff0c;什么脚…

作者头像 李华
网站建设 2026/10/21 6:49:33

Codex Desktop额度耗尽导致发送键禁用的原理与绕过方案

1. 项目概述&#xff1a;一个被“静音”的发送键背后&#xff0c;到底发生了什么Codex Desktop 这个名字&#xff0c;最近在不少技术向社群里反复出现&#xff0c;不是因为新功能发布&#xff0c;而是因为一种特别“安静”的异常——用户明明已经配置好第三方中转服务&#xff…

作者头像 李华
网站建设 2026/10/15 3:55:08

EMD-KPCA-LSTM:多维时序特征分解与光伏功率预测实战

简介&#xff1a;这份压缩包提供了一套完整的EMD-KPCA-LSTM多维时间序列预测程序&#xff0c;面向光伏功率预测、回归预测等场景&#xff0c;适合具备Matlab和深度学习基础的研究者、赛题选手或相关专业学生。案例使用北半球光伏功率数据&#xff0c;以太阳辐射度、气温、气压、…

作者头像 李华
网站建设 2026/10/16 6:05:50

Clawith 深度分析报告:OpenClaw AI Agent 的 FastAPI 与 Docker 落地实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/19 0:55:44

计数DP从入门到实战:以666RPG为例详解状态设计与转移

1. 从“666RPG”这个标题说起&#xff1a;计数DP到底在算什么第一次看到“666RPG&#xff08;计数dp&#xff09;”这个标题&#xff0c;很多人会愣一下&#xff1a;RPG不是角色扮演游戏吗&#xff0c;怎么跟DP扯上关系了&#xff1f;其实这里的RPG并不是指游戏类型&#xff0c…

作者头像 李华
网站建设 2026/10/16 4:56:01

Java内存模型JMM实战:volatile、synchronized与final的底层原理与避坑指南

1. 为什么今天还必须啃透JMM——不是为了面试&#xff0c;而是为了写出真正可靠的并发代码Java内存模型&#xff08;JMM&#xff09;这三个字母&#xff0c;几乎每个写Java的开发者都见过。它常出现在面试题里&#xff0c;被简化成“主内存、工作内存、happens-before”几个词&…

作者头像 李华