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.mdMEMORY.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里最稳。