用了大半年 Claude Code,最让我崩溃的从来不是代码写得不对,而是它“记性太差”——今天下午刚跟你敲定的技术选型、接口约定、命名规范,睡一觉回来新开个会话,它全忘了,同一个问题能来回解释三遍。后来我自己折腾了一个工具,名字就叫 claude-mem,核心目标很直接:给 Claude 装一套长期记忆,让每次对话结束的时候,自动把重要结论沉淀成结构化文件,下次开新会话之前再读回去。这篇东西不聊虚的,把我踩过的坑、拆过的设计、最后跑通的方案全部摊开讲,适合所有被 AI 编码助手“失忆”问题折磨的开发者。
1. 痛点拆解:Claude Code 的“失忆症”到底是什么
1.1 会话隔离与长期记忆的根本矛盾
Claude Code 这一类终端编码助手,本质上是一个“无状态”的对话引擎。每一次启动会话,它拿到的是当前项目的文件快照、系统提示词,外加你最近几轮输入的上下文。会话一结束,除非手动把关键信息写进某个文件,否则这些讨论结果就像没发生过一样。这个设计本身不算缺陷——上下文有窗口上限,token 有成本,保持会话隔离能避免历史噪音污染新任务。但对长期项目来说,它就成了一个隐蔽的坑。
我举一个实际场景:一个中型 Web 项目,前后端加起来几十个模块,我用 Claude Code 帮我把认证模块从 JWT 方案换成 Session 方案。换的过程里我们讨论了很多细节——为什么放弃无状态、session 存储放在哪、Redis 的 key 格式怎么设计、旧 token 怎么兼容迁移。当时聊得很顺,代码也改完了。结果第三天新开一个会话,想让它继续优化登录接口的并发问题,它居然问我“这个项目用的什么认证方案”。我当时血压就上来了。
这不是个例。代码风格偏好在会话之间丢失、项目约定反复被打破、之前排掉的坑换一种问法又踩一遍。真正的问题在于:AI 的对话记忆是短暂的,而项目的知识沉淀需要是长期的。这两个诉求天然矛盾。
于是市场上的解决方案大致分两派:一派是把所有约定写进CLAUDE.md这类项目文档,让模型每次启动都读;另一派就是给对话过程增加“事后总结”机制,也就是 claude-mem 走的路线。前者依赖你手动维护,写少了没用写多了干扰;后者通过事件触发自动沉淀,省心很多,而且和你实际聊的内容严格对齐。
1.2 claude-mem 解决的问题边界
先说清楚 claude-mem 不是什么。它不是一个插件式的人格里存储系统,也不是一个向量数据库,更不是要在每次请求里塞一堆 RAG 检索结果。它要解决的问题非常具体:在不需要人工干预的前提下,把一次有价值的 AI 对话压缩成若干条长期有效的记忆,并且在下次对话开始前自动、有优先级地注入上下文。
所以它的核心价值有三层。第一层是沉淀:对话结束,自动提炼出项目决策、用户偏好、约定、待办、外部工具知识点,落盘成可读文本。第二层是复用:新会话启动时,把与当前项目相关的记忆按优先级拼进上下文,让模型“想起来”之前的事。第三层是可控:所有记忆都是纯文本,你可以随时打开文件增删改,也可以设置哪些分类不参与注入,甚至完全关闭自动注入,只在需要时手动拉取。
这三层决定了我后续所有设计选择。比如为什么用 Markdown 而不建数据库,为什么按项目目录做隔离,为什么提炼动作放在会话结束而不是对话进行中——这些都是在“沉淀、复用、可控”这三个关键词下推导出来的。
2. 整体设计思路:把记忆做成一棵可读可改的知识树
2.1 三层记忆结构:全局、项目、会话
最开始我踩过一个方向性错误:把所有记忆堆在一个大文件里。结果几天之后那个文件就变成了一锅粥——项目 A 的决策混着项目 B 的约定,你让模型读,它读得头大,你也不愿意手动去整理。后来我重新设计,把记忆拆成三层。
- 全局层(global):跨项目通用的内容,比如你个人偏好的代码风格、常用的工具链、通用禁忌。例如“接口返回结构统一用
{ code, data, message }”这种,放在全局层。 - 项目层(projects):按项目目录隔离的内容,比如某个仓库里的技术选型、目录约定、待办事项。这一层严格跟随当前工作目录命中。
- 会话层(session):单次对话产生的临时上下文,只在当前会话内有效,结束后不会被沉淀为长期记忆,除非内容足够重要被升级到前两层。
对话层的存在非常重要。它避免了一个问题:不是每句闲聊都值得被记住。如果每一次会话的结束总结都自动写入长期记忆,噪音会迅速淹没信号。所以我在设计的时候加了一个“升级判定”环节——提炼出的候选记忆里,只有被判定为“对未来的项目工作有影响”的条目,才会从会话层进入项目层或全局层。
这套三层结构在实践中很容易理解:全局层是你的工作习惯,项目层是你所有项目的共同记忆仓库,会话层是每段对话的短期视野。模型每次启动会话时,注入顺序也是先全局后项目,最后叠加会话过程中的实时上下文。
2.2 Markdown 存储和我们为什么不用数据库
有人问过我:记忆数据为什么不用 SQLite 或者 JSON 存,非要落成 Markdown 文件?这是个好问题,我从两个角度回答。
第一,可读性优先。记忆系统的最终消费者其实是两个:一个是模型,一个是人。模型读文本没有任何问题,但人——也就是你——总会想打开文件看看 claude-mem 到底记住了什么。Markdown 用记事本就能打开,结构一目了然,可以手动改、手动删、手动补注释。如果用数据库,你要么写命令行查询,要么做个管理界面,都属于本末倒置。
第二,版本管理友好。我的项目层记忆目录我直接纳入了代码仓库的.gitignore之外的独立目录,但这不代表不能用 Git 做版本管理。Markdown 文件天然适合 diff,我可以清楚地看到某条记忆是哪一天加的、哪天被标成废弃的。如果是数据库里的二进制文件,做 diff 就是一场灾难。
存储格式上,我规定每个记忆条目用固定的叶子格式。单个记忆文件按分类命名,比如user_preferences.md、project_decisions.md,里面每条记录是一个两行结构:
## 2025-06-12 认证方案确定 - id: 47f3c2a1 - category: project_decisions - status: active - 我们最终放弃 JWT,改用 Session + Redis。迁移期间保留旧 token 校验接口 30 天。这个格式有三个好处:时间戳给了记忆时效信息;id 字段用来做去重和覆盖;status 字段用来标记废弃。模型读取时,规则明确——只关注 status 为 active 的条目。
2.3 记忆文件的目录规范与命名规则
目录规范直接决定了注入逻辑的复杂度。我的实际目录结构长这样:
~/.claude-mem/ ├── config.toml ├── global/ │ └── memories/ │ ├── user_preferences.md │ └── external_knowledge.md └── projects/ └── my-web-app/ ├── meta.json └── memories/ ├── project_decisions.md ├── conventions.md └── todos.mdconfig.toml是全局配置,控制提炼阈值、注入开关、分类映射等;global/memories/目录按分类文件名组织全局记忆;projects/<项目名>/下面挂每个项目的记忆目录。项目名是从当前工作目录推断出来的,默认取目录名,也可以在meta.json里显式指定别名,这样同一个仓库换了一层目录名,记忆还能对上。
这个设计最核心的一点是:记忆与项目目录强绑定。早期版本我试图用“仓库 URL”来识别项目,但在本地多分支、多目录迁移的场景下很容易失效。换成目录名匹配之后,一套简单的字符串匹配就解决了绝大多数命中问题,而且两个同名目录下的记忆永远不会串。
3. 核心机制拆解:记忆是如何被自动提炼的
3.1 会话结束后的自动提炼链路
claude-mem 的触发时机,我最终确定在会话结束,而不是对话进行中。原因有两个:第一,对话进行中做实时提炼,会打断主任务的 token 预算,而且摘要质量不高——你还没聊完,怎么知道哪件事值得被记住?第二,结束后提炼可以一次性拿到完整会话记录,总结的上下文更全,视角也更接近“事后复盘”。
具体链路是这样:每次 Claude Code 会话结束,会触发一个 hook 事件,claude-mem 监听到之后启动提炼流程。整个流程分四步:
- 读取本次会话的原始记录文件,通常是 JSONL 格式的对话日志。
- 调用大模型 API,输入一份“提炼指令”和完整对话日志,输出结构化结果。
- 对结构化结果做后处理:去重、过滤低置信度条目、按分类写入对应的 Markdown 文件。
- 更新
meta.json里的项目元信息,比如“最后一次记忆生成时间”、“当前项目活跃记忆条数”。
这个链路我用伪代码表示大概是:
def on_session_end(session_id): logs = load_session_logs(session_id) if len(logs) < MIN_DIALOGUE_LENGTH: return # 对话太短,不提炼 result = llm.extract_memories(logs, EXTRACT_PROMPT) cleaned = deduplicate(filter_candidates(result)) write_memories(project=current_project(), memories=cleaned) update_meta(project=current_project())有个细节很重要:MIN_DIALOGUE_LENGTH这个阈值。如果对话只有三五轮,基本没有沉淀价值,直接跳过提炼,避免为噪音付费。我实测下来,阈值设在 20 轮对话左右比较合理,低于这个数,提炼出的东西要么是空泛的客套,要么是已经被代码本身表达的内容。
3.2 提炼 Prompt 的核心设计
提炼的质量完全取决于 Prompt。我早期犯过懒,写过一版极简提示词:“请总结这段对话的重要信息”。结果提炼出一堆“用户讨论了登录功能”“用户决定使用 Redis”——全是废话。后来我把 Prompt 拆成三层约束:分类约束、长度约束、时效约束。
分类约束要求模型把每条记忆归入五类之一:project_decisions(项目决策)、user_preferences(用户偏好)、conventions(约定规范)、todos(待办事项)、external_knowledge(外部知识)。长度约束要求每条记忆的正文控制在 80 字以内,并且必须是一句能被后续模型直接理解的话,不允许“根据我们之前的讨论”这种模糊表述。时效约束要求模型判断:这条信息如果三个月后还存在,对项目有没有影响?没有就直接丢弃。
我实际在用的提炼提示词核心段落长这样:
你是一个记忆提炼助手。我给你一段 AI 编码助手的完整对话记录,你的任务是从中提取出对"未来长期工作"有参考价值的要点。 要求: 1. 忽略问答过程中的寒暄、试探、临时性任务讨论。 2. 只保留下面五类信息: - project_decisions:团队/项目在本次对话中确定的技术选型和架构决策 - user_preferences:用户反复强调的工作方式、代码风格偏好 - conventions:明确说出的命名规则、目录规范、提交约定 - todos:清晰的、需要后续完成的事项 - external_knowledge:对话中确认过可用的外部工具、库、服务要点 3. 每条记忆正文不超过 80 字,必须是一句完整、独立、可执行的话。 4. 预估这条信息在 3 个月后是否仍有用;如果基本无用,不要输出。 5. 输出 JSON,字段为:{"memories": [{"category": "...", "content": "..."}], "summary": "一句话总结本次会话"}这个 Prompt 我在真实使用中迭代了五个版本。最初没有“3 个月后是否仍有用”这个约束,结果提炼出大量过期的临时信息,比如“用户今天在调登录接口报错”——这种信息第二天就没价值了。加上时效约束之后,提炼的信噪比明显提升。
3.3 记忆注入策略:优先级、去重与时效
记忆生成到位,剩下的一半功夫在“注入”。claude-mem 的注入逻辑不是把所有记忆一股脑塞进上下文,而是做了一套轻量优先级排序。
规则大致是这样:注入时先读全局层,再读项目层;全局层里,user_preferences优先级最高;项目层里,conventions和project_decisions优先,todos看情况——如果本次会话的主题和某个 todo 强相关才注入,否则不注入。external_knowledge永远排最后。
排序之后还要处理总长度。我设了一个硬上限:每次注入的记忆正文总字符数不超过 3000 字符。超过的部分,按“最近更新时间”从新到旧截断。这个上限是我根据上下文窗口算出来的——如果一段对话的上下文是 200K tokens,塞 3000 个字符的记忆进去大约只占 1000 tokens 左右,几乎感觉不到成本,但对模型行为的引导效果非常明显。
去重逻辑上,我采用的是“摘要相似度 + 关键字碰撞”的两层策略。摘要相似度用简单的字符重叠率来判断,重叠率超过 70% 就视为重复。关键字碰撞是针对project_decisions和todos的:比如新提炼的记忆里有“认证”,旧的也有“认证”,就认为可能是同一件事的延续,新条目优先生效,旧条目标记为 superseded。
时效性方面,我在config.toml里给每个分类配了默认过期时间,比如todos默认 30 天过期,project_decisions默认 180 天。过期条目不会被注入,但不会立即删除——文件里保留,用 status 标记为 expired,你想翻历史还能翻到。
4. 实操配置:从零跑通 claude-mem 的完整步骤
4.1 安装配置与初始化
安装这部分不复杂,前提是你已经有 Claude Code 的使用环境,并且拿到可用的 Anthropic API 密钥。claude-mem 本身是一个命令行工具,执行安装命令之后,它会自动创建~/.claude-mem/目录并生成一个默认配置文件。
npm install -g claude-mem claude-mem initinit命令会做三件事:创建配置目录、生成默认config.toml、检查 Anthropic API 密钥是否可用。如果环境变量里已经有ANTHROPIC_API_KEY,它会直接复用;如果没有,会提示你手动填入。
初始化的config.toml里面,我常用的是这几个核心字段:
[general] min_dialogue_length = 20 # 低于 20 轮对话不提炼 max_inject_chars = 3000 # 注入记忆的总字符上限 auto_inject = true # 新会话自动注入记忆 [extract] base_url = "https://api.anthropic.com" model = "claude-sonnet-4-20250514" # 提炼用的模型,可以用便宜的 max_candidates = 20 # 单次提炼最多生成 20 条候选记忆 [expiration] project_decisions = 180 # 项目决策 180 天过期 conventions = 180 todos = 30 user_preferences = 365 external_knowledge = 365 [projects] my-web-app = "web/auth" # 手动指定项目别名与目录的映射这里有个选型要点:提炼用的模型不需要最强,便宜快速的型号就够用。提炼任务本质上是文本摘要,不是复杂推理,我多数时候用中端型号,不仅节省成本,速度也快,会话结束一两秒就能完成提炼。
4.2 hooks 配置与命令详解
安装好之后,最关键的一步是配置 Claude Code 的 hooks。如果你不配置,claude-mem 就是个孤立的命令行工具,不会自动触发。hooks 的作用是让 Claude Code 在会话生命周期的事件点上执行外部命令。我需要的两个事件:SessionStart(会话开始时)和SessionEnd(会话结束时)。
配置方法是在 Claude Code 的配置文件里声明 hooks。实际配置如下:
claude-mem setup-hooks这条命令会自动往 Claude Code 的配置里写入 hooks 声明。手工写的话,对应的 JSON 片段长这样:
{ "hooks": { "SessionStart": [ { "hooks": [ { "type": "command", "command": "claude-mem inject --project \"$(basename $PWD)\"" } ] } ], "SessionEnd": [ { "hooks": [ { "type": "command", "command": "claude-mem extract --session \"$CLAUDE_SESSION_ID\"" } ] } ] } }解释一下这两个命令:extract负责在会话结束的时候跑提炼流程,需要拿到当前会话 ID 才能去读对话日志;inject负责在新会话启动时读取记忆并注入上下文,注入的方式是往系统提示词末尾追加一段“项目记忆”内容。$PWD 用来识别当前项目目录,这是项目层记忆能否命中的关键。
这里有一个我踩过的坑:SessionEnd 事件并不是所有退出路径都能触发。如果 Claude Code 进程被强制杀掉,或者终端直接关闭没有走正常退出流程,hook 不会执行,这次对话的记忆就丢了。规避办法是在配置文件里开启一个“兜底策略”——每次启动新会话时,如果发现有“上次未提炼”的会话日志标记,先补提炼一次。这个策略帮我捞回了好几次以为丢掉的记忆。
4.3 验证记忆生效与日常使用技巧
配置完之后,验证链路是否通的最快方法很简单:故意在对话里说一个明确的偏好,比如“以后所有数据库查询都强制加 LIMIT 100”,然后正常结束会话,看~/.claude-mem/projects/<你的项目>/memories/下是否出现了user_preferences.md,内容里有没有刚才那句。
我第一次验证的时候,文件是生成了,但发现它记住的和我预想的不太一样——它记住的是“用户要求数据库查询限制结果条数”,然后还额外加了一条“用户可能受到性能问题困扰”。后者明显是模型脑补的,并不存在。这个问题靠 Prompt 里的长度约束和时效约束缓解了不少,但也很难完全消除。好在这套系统的优势在于“可控”——我打开文件手动删掉那条脑补内容,下次注入就干净了。
日常使用中我总结出几个技巧:第一,重要结论在对话里说清楚,比如“这是我们最终的决定,请记下来”,比聊到一半顺带带过更容易被提炼出来。第二,定期翻一下项目层记忆文件,发现过期条目直接手动把 status 改成 expired,这比依赖自动过期更准确。第三,想临时关闭注入时,直接设auto_inject = false,但保留extract,让记忆继续沉淀,只是不再自动注入——这在做专注型任务时很管用,不会让历史记忆干扰模型对当前问题的判断。
5. 踩坑记录:高频问题与排查速查表
5.1 高频问题排查实录
这部分把我实际遇到过的、以及社区里大家最容易遇到的高频问题整理成了一份速查表,供你直接对着排查。
| 现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 会话正常结束但没生成记忆文件 | 对话轮数低于min_dialogue_length | 检查会话日志长度 | 降低阈值或手动执行claude-mem extract |
extract执行了但记忆内容为空 | 对话全是临时性提问,无长期价值 | 查看提炼日志的 summary 字段 | 正常现象,无需处理 |
| 注入的记忆和当前任务无关 | 项目目录匹配错了 | 检查meta.json里的项目名 | 手动指定--project参数 |
| 记忆内容重复出现 | 去重阈值太低或 id 冲突 | 查看记忆文件的 id 字段 | 提高摘要重叠率阈值到 75% 以上 |
| SessionEnd hook 没触发 | 进程被强制杀掉或终端闪退 | 检查兜底策略日志 | 开启补提炼兜底开关 |
| 注入后模型行为明显变怪 | 记忆文件有陈旧或错误条目 | 打开文件人工检查 | 手动修正或删掉问题条目 |
| 记忆写入很慢 | 会话日志太长,提炼 token 数大 | 查看提取耗时统计 | 限制单次提炼的日志输入行数 |
这里最值得展开的是第一和第三个问题。阈值过低会导致提炼任务执行得非常频繁,但产出的几乎都是低价值信息;阈值过高又会漏掉真正重要的内容。我在多台机器、多个项目上观察下来,20 轮是一个比较平衡的点,但如果你的对话普遍较短,可以下调到 15 轮试试,跑几天看效果再回调。
项目匹配错误的坑则更隐蔽。你把项目克隆到另一个目录,比如从~/work/my-app变成了~/work/my-app-v2,目录名变了,旧记忆就匹配不上了。解决的办法就是上面提到的[projects]配置里手动指定映射,把记忆目录固定到my-app,不管代码目录叫什么名字都能命中。
5.2 记忆膨胀:控制 token 成本的实用手段
用了一个月之后,你会面临一个新问题:项目层记忆越来越多,注入的 3000 字符上限根本装不下所有 active 条目。这时候如果只是按时间截断,很可能导致重要的早期决策被顶掉,而那些最近的、琐碎的记忆反而占了位置。
我尝试过几种做法,最终留下的是“分类配额制”。思路是:在 3000 字符总预算内,给每个分类分配固定比例。比如conventions占 40%、project_decisions占 30%、user_preferences占 20%、todos占 10%、external_knowledge不占预算只等剩余空间。这样做的好处是,无论记忆总量增长多少,核心的约定和决策始终有位置,临时性的 todo 和外部知识随时可以被顶掉。
这个策略结合自动过期,实测下来即使记忆库里有几百条记录,注入给模型的内容依然能维持在可控范围内。我也试过用向量检索来动态筛选记忆,效果确实更好,但引入了额外组件和复杂度。对于一个“加记忆”这么简单的需求,重了。所以最终的方案还是偏务实的规则引擎。
再说一下成本。提炼一次对话,假设会话日志有 2000 tokens,加上提示词本身,一次调用大概消耗 2500 tokens 输入、400 tokens 输出。按 API 价格粗算,一次提炼折算下来不到人民币一毛钱。就算一天结束十个会话,成本也处于完全可以忽略的水平。
5.3 多项目、多分支场景下的记忆隔离
最后提醒一个容易被忽略的场景:同时维护多个项目、或者在多个分支之间横跳时的记忆隔离问题。
claude-mem 按目录名隔离项目,这意味着如果你在一个项目的两个分支之间切换,只要目录名不变,记忆就是共享的。这通常没问题,因为项目的约定和技术选型不会因为分支不同而改变。但有一种情况例外:你有一个“大改造”分支,比如把后端从 REST 改成 GraphQL,这个分支上的对话产生了大量关于 GraphQL 的临时决策。如果你切回主线分支开启新会话,这些记忆会被注入,而主线根本不相关。
我的应对办法是分支级记忆前缀:在config.toml里给项目开启分支感知,记忆路径从projects/my-app/变成projects/my-app/feature-graphql/。开启之后,每个分支拥有独立的记忆空间,互不干扰。代价是分支合并之后需要手动合并记忆,但这个操作一般半年碰不上一次,成本很低。
还有一个细节:团队协作场景下,如果多个人共用同一台机器,或者同一个项目目录被多人使用,全局层的user_preferences会变成“多人的平均数”,反而没有任何一个人的偏好能得到尊重。这个问题目前没有完美的自动化方案,我的建议是全局层按用户目录拆分成~/.claude-mem/users/<username>/,各人的偏好走各自的文件,项目层仍然共享。
最后分享一个我近期感受到的变化。以前我总觉得自己养成的“写 CLAUDE.md 习惯”就是给 AI 加记忆的正解,但真正用上 claude-mem 之后才发现,手写的文档和自动提炼的记忆是互补关系——CLAUDE.md 适合放“一开始就确定、长期不变”的规则,claude-mem 负责捕获那些“今天聊出来、明天就忘掉”的动态结论。我现在的工作习惯是:大原则写进 CLAUDE.md,细节约定交给 claude-mem 自动沉淀,每隔一两周翻一次记忆文件做一次轻量清理。这套组合拳坚持下来,Claude Code 在我手上的生产力确实上了一个台阶,至少它不会再一脸无辜地问我“这个项目用的什么认证方案”了。