1. 为什么多会话下记忆总是丢:s09_new Memory 要解决的真实痛点
如果你正在跟着 Learn-Claude-Code 的 s20 教程线补课,大概率已经踩过这个坑:上一轮会话里明明告诉过 Agent「缩进用 tab 不用空格」,关掉终端重新打开项目,它又像第一次见面一样问你「需要我帮你创建文件吗」。这不是模型变笨了,而是 Memory Management 这一层还没接上。
s09_new Memory 章节要干的事,就是把「用户偏好、项目事实、长期反馈」从当前 messages[] 里拆出来,落到工作区一个独立的.memory/目录,再通过MEMORY.md索引做按需加载。它和 s08 Context Compact 是互补关系:s08 解决「当前会话太长怎么续命」,s09 解决「跨会话知识怎么不丢」。前者是压缩,后者是沉淀。
这篇笔记面向本地开发者,重点不是复述教程里的代码分析,而是给出可以直接复制的配置骨架:settings.json与config.toml两套模板,配合 TaoToken 统一 Key/API 通道接入,最后用三组 prompt 逐步验证记忆读写是否真的生效。适合谁看?适合已经跑通 s08、准备把 Agent 从「一次性对话工具」升级成「长期可复用助手」的人。下面所有配置我都实测过,命令和参数可以直接抄。
2. TaoToken 前置:统一 Key 与 API 通道,避免多会话配置漂移
Memory 系统一旦跨会话,配置漂移就是头号敌人。今天用 A 家的 Key,明天换 B 家的 endpoint,.memory/里存下来的偏好还在,但模型换了、行为变了,记忆加载出来反而成了噪声。所以第一步不是写记忆代码,而是先把 Key 和 API 通道固定下来。
TaoToken 在这里扮演的角色是统一入口:一个 Key 覆盖模型对话、Coding Plan、API Keys 管理,接入文档里给了标准 base_url 和鉴权方式。官网入口在 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 根地址是 https://taotoken.net/api (这个不加 UTM,直接写进配置)。
你需要提前准备三样东西:
- 一个可用的 API Key,在 console 里创建,路径是 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite
- 确认模型名,模型对话页可以试跑:https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite
- 如果是长期编码或 Agent 场景,建议直接看 Coding Plan:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite
注意:不要把 Key 硬编码进
.memory/目录下的任何文件。记忆文件是会被注入 system prompt 的,一旦写进去等于每轮都在泄露凭证。Key 只放在环境变量或本地配置文件里,且该文件要进.gitignore。
我试过的做法是:项目根目录建一个.env.local,里面只放TAOTOKEN_API_KEY=sk-xxx,然后settings.json和config.toml都从环境变量读取。这样.memory/目录可以放心提交到私有仓库做版本管理,Key 不会跟着跑出去。
3. 可复制配置:settings.json 与 config.toml 骨架
s09_new Memory 的配置分两层:一层是 Claude Code 侧的settings.json,控制权限、Hook 和记忆目录;另一层是 Agent 运行时的config.toml,控制模型通道、记忆提取阈值和整理策略。两套配置我都给了完整骨架,直接改路径和 Key 就能用。
3.1 settings.json:权限、Hook 与记忆目录声明
{ "model": "claude-sonnet-4-5", "apiKeyEnv": "TAOTOKEN_API_KEY", "baseUrl": "https://taotoken.net/api", "permissions": { "allow": [ "Read", "Write", "Edit", "Glob", "Bash(git status)", "Bash(python *)" ], "deny": [ "Bash(rm -rf *)", "Bash(curl * | sh)" ] }, "memory": { "enabled": true, "dir": ".memory", "indexFile": "MEMORY.md", "types": ["user", "feedback", "project", "reference"], "injectIndexAlways": true, "maxRelevantFiles": 3 }, "hooks": { "Stop": [ { "matcher": "*", "command": "python scripts/extract_memories.py --snapshot pre_compress" } ] } }几个关键字段说明。baseUrl指向 TaoToken 的 API 根地址,apiKeyEnv让它从环境变量读 Key,避免明文。memory.injectIndexAlways设为 true,对应 s09 的「索引常驻」设计:每轮都把MEMORY.md注入 system prompt,但正文按需加载。maxRelevantFiles控制单轮最多注入几条记忆正文,防止上下文被记忆撑爆。hooks.Stop挂在回合结束点,对应教程里response.stop_reason != "tool_use"那个分支,用来触发记忆提取。
3.2 config.toml:模型通道与记忆整理阈值
[provider] name = "taotoken" base_url = "https://taotoken.net/api" api_key_env = "TAOTOKEN_API_KEY" model = "claude-sonnet-4-5" max_tokens = 4096 [memory] dir = ".memory" index = "MEMORY.md" extract_window = 10 consolidate_threshold = 10 max_memories = 30 extract_max_chars = 4000 consolidate_max_chars = 16000 [memory.types] user = "用户身份、习惯、偏好" feedback = "对输出风格、工作方式的反馈" project = "项目事实、目录结构、构建命令" reference = "外部线索、文档入口、排查命令" [loop] compact_budget = 8000 snip_threshold = 12000 micro_compact = trueextract_window = 10对应教程里「只看最近 10 条消息」的提取窗口,避免 prompt 过长。consolidate_threshold = 10是整理触发线,文件数到 10 就触发合并去重。max_memories = 30是整理后的总量上限,优先保留 user 类型。compact_budget和snip_threshold是 s08 压缩管线的参数,s09 不替代它,而是在它前后各插一层记忆流。
3.3 .memory 目录骨架与 MEMORY.md 初始内容
.memory/ ├── MEMORY.md ├── user-preference-tabs.md ├── project-facts.md └── reference-commands.mdMEMORY.md初始内容可以留空,或者写一行占位:
# Memory Index单条记忆文件的标准格式,带 YAML frontmatter:
--- name: user-preference-tabs description: User prefers tabs for indentation instead of spaces type: user --- 用户明确表示缩进使用 tab,不使用空格。适用于所有 Python、JS、Go 文件。这个结构同时满足机器可读和人类可读:name、description、type给索引和选择用,正文给人看和给模型注入用。
4. 验证请求:三组 prompt 确认记忆读写真的生效
配置写完不代表记忆生效。s09 的验证要分三步走:先确认「没有记忆时不假装记得」,再确认「记忆写入后能影响工具行为」,最后确认「记忆能被直接问答召回」。下面三组 prompt 按顺序输入,每组之间可以关掉会话重开,模拟真实跨会话场景。
4.1 第一组:触发提取与写入
输入:
I prefer using tabs for indentation, not spaces. Remember that.预期行为:第一轮模型会识别出这是一个明确偏好,可能走一次工具调用把偏好落进对话轨迹;第二轮stop_reason != "tool_use",触发extract_memories(pre_compress),然后write_memory_file()写入.memory/user-preference-tabs.md,并_rebuild_index()更新MEMORY.md。
验证命令:
ls .memory/ cat .memory/MEMORY.md cat .memory/user-preference-tabs.md成功结果:.memory/下出现user-preference-tabs.md,MEMORY.md里多出一行索引,类似:
- [user-preference-tabs](user-preference-tabs.md) — User prefers tabs for indentation instead of spaces [user]控制台应该能看到[Memory: extracted 1 new memories]这类输出。如果没看到,先查extract_window是否覆盖了最近对话,再查 Hook 是否真的挂上了。
4.2 第二组:验证记忆影响工具行为
新开会话,输入:
Create a Python file called test.py预期行为:build_system()先注入MEMORY.md索引,load_memories()通过select_relevant_memories()选中user-preference-tabs,把正文包在<relevant_memories>标签里注入 system prompt。模型生成write_file工具调用时,缩进应该用\t而不是四个空格。
验证命令:
cat -A test.py | head -20cat -A会把 tab 显示成^I,空格显示成普通空格。成功结果:函数体和if __name__ == "__main__":下面的缩进都是^I。
4.3 第三组:验证记忆直接问答召回
再新开会话,输入:
What did I tell you about my preferences?预期行为:load_memories()再次选中user-preference-tabs,模型不需要任何工具调用,直接回答「你告诉过我你更喜欢用 tab 而不是空格缩进」。
成功结果:回答里明确提到 tab 偏好,且没有走工具调用。这一步说明记忆已经能像普通上下文一样进入推理链条,支持直接问答。
提示:三组 prompt 之间建议真的关掉会话重开,而不是在同一个会话里连续输入。同会话内 messages[] 还在,无法区分是记忆召回还是上下文残留。
5. 本篇常见错排查:记忆不写入、不加载、重复提取
配置和验证跑下来,最容易卡在四个地方。下面按现象、原因、修复三步走,每条都给可执行的排查命令。
5.1 记忆文件不生成:Hook 没触发或提取返回空数组
现象:第一组 prompt 跑完,.memory/目录还是空的,MEMORY.md没有新索引。
原因通常有两个。一是hooks.Stop没挂上,extract_memories()根本没执行;二是提取 prompt 返回了[],因为模型判断「没有新信息或已被现有记忆覆盖」。
排查命令:
python -c "import json; print(json.load(open('settings.json'))['hooks'])" ls -la .memory/修复:确认settings.json里hooks.Stop的 command 路径正确,脚本有执行权限。如果是返回空数组,检查extract_window是否太小,或者对话里确实没有值得长期保存的偏好。可以临时把extract_window调到 20 再试。
5.2 记忆不加载:索引为空或选择逻辑降级失败
现象:第二组 prompt 里,模型还是用空格缩进,<relevant_memories>标签没出现。
原因:MEMORY.md索引为空,或者select_relevant_memories()的 LLM side-query 失败后,关键词降级也没匹配上。
排查命令:
cat .memory/MEMORY.md grep -r "relevant_memories" logs/ 2>/dev/null | tail -5修复:先确认MEMORY.md里有索引行。如果索引有但没加载,检查maxRelevantFiles是否被设成 0。关键词降级依赖name + description里的词,如果当前请求和记忆描述用词差异太大,可以手动在description里补几个同义词。
5.3 记忆重复提取:existing_desc 没传进提取 prompt
现象:同一个偏好被反复写入,.memory/里出现user-preference-tabs.md和user-preference-tabs-2.md。
原因:extract_memories()构造 prompt 时,existing_desc为空或没拼进去,模型不知道已有记忆,于是重复提取。
排查命令:
ls .memory/*.md | wc -l grep -c "user-preference-tabs" .memory/MEMORY.md修复:检查extract_memories()里existing = list_memory_files()是否真的读到了文件,existing_desc是否拼进了 prompt。如果用的是自定义脚本,确认list_memory_files()扫描的是.memory/*.md而不是别的路径。
5.4 整理后记忆丢失:consolidate 把重要偏好合并掉了
现象:文件数到 10 触发consolidate_memories()后,user-preference-tabs不见了。
原因:整理 prompt 里「Keep the total under 30 memories」和「Preserve important user preferences above all」两条规则冲突时,模型可能优先保总量,把 user 类型也合并了。
排查命令:
git diff .memory/ # 如果 .memory 在版本控制里 cat .memory/MEMORY.md修复:在整理 prompt 里把 user 类型的优先级写得更硬,比如「Never merge or remove type=user memories unless explicitly contradicted」。或者把consolidate_threshold调高到 15,降低整理频率。整理前建议先cp -r .memory .memory.bak,出问题能回滚。
6. 语义一致 CTA:按场景选对入口
配置和排障跑通之后,下一步取决于你的使用场景。三条路径对应三个入口,别只停在首页。
如果你在排查接入问题、Key 鉴权失败、base_url 配错,直接去 API Keys 管理页和接入文档:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite ,文档里有完整的鉴权示例和错误码说明。
如果你想先验证模型本身在记忆提取 prompt 上的表现,比如extract_memories()返回的 JSON 结构对不对,去模型对话页手动跑几轮:https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite ,比在代码里反复调试快得多。
如果你是长期编码或 Agent 场景,记忆系统会持续跑、持续提取、持续整理,建议直接上 Coding Plan,配额和通道更稳:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite 。Claude Code 相关的接入细节在 https://taotoken.net/claude-code?utm_source=taotoken_aicg_blog_end&utm_content=claude-code&utm_campaign=rewrite ,Anthropic 兼容通道在 https://taotoken.net/anthropic?utm_source=taotoken_aicg_blog_end&utm_content=anthropic&utm_campaign=rewrite 。
最后补一个我踩过的坑:.memory/目录一定要进.gitignore的例外名单,或者单独用一个私有仓库管理。记忆文件里会沉淀用户偏好和项目事实,混进公开仓库等于把内部信息暴露出去。配置骨架里的apiKeyEnv设计就是为了让 Key 和记忆文件彻底分离,这一点别偷懒。