claude-mem:给Claude编程助手打造跨会话记忆层

📅 发布时间:2026/10/12 10:37:49
claude-mem:给Claude编程助手打造跨会话记忆层
如果你经常用 Claude 这类 AI 编程助手处理同一个项目大概率遇到过这种场面上次对话里明明已经让它整理好了部署流程这次新开一个会话它又像第一次见面一样问东问西。我开发 claude-mem就是为了把这块“临时工记忆”变成“长期硬盘”。它是一个运行在本地的命令行工具会自动捕捉每轮会话里值得保存的信息并在新会话开始前把相关记忆重新塞回给模型。文章下面会完整交代我的设计思路、核心实现、参数取舍和踩坑记录适合被上下文重置折磨的 AI 工具重度用户也适合想自己动手做工具的人参考。1. 为什么要给 Claude 加记忆先看清问题本质很多人的第一反应是“Claude 本身不是有记忆吗”。这里要区分一下模型在单次会话内确实能记住聊天窗口里的内容但跨会话就不是一回事了。1.1 会话隔离与冷启动上下文窗口的致命短板以我日常使用的 Claude 终端版为例每次启动都是一次冷启动。会话中的消息会被发送给模型推理但服务端不会保留你的历史对话所有状态都依赖客户端自己维护。一旦会话结束窗口关闭下一次启动时模型对你说过的每句话都一无所知。这种设计对普通聊天没什么但对编程场景非常致命。我身边一位同事就经常踩这个坑周一花了一晚上调好了数据库连接池的参数顺便让 Claude 总结出了项目在本地的启动方式周二早上新开会话同一个项目它又开始一本正经地猜端口号。你气得想翻聊天记录可记录要么太长不好找要么早就被新会话冲掉了。这种冷启动带来的效率损耗是实打实的。每次都要重新解释一遍“这个项目怎么跑”“哪些目录不能动”“为什么要用这个框架”等于把已经解决过的问题反复再解决一遍。1.2 现有方案的三个困境以及 claude-mem 的机会我见过不少人为了对抗这种失忆用过各种土办法但都不够顺手手动维护笔记把关键结论粘贴到 Markdown 文件里定期更新。问题是一旦忘了写或者写得不够结构化下次根本派不上用场。复制粘贴历史对话把上一轮聊天记录直接贴进新会话。上下文窗口有限越贴越乱而且无关信息太多会干扰模型判断。用系统自带的会话恢复功能官方确实支持--resume继续上一次对话但它只能恢复一个旧会话。你在同一个项目里同时处理三个分支、五个需求时这个功能基本帮不上忙。claude-mem 的思路不是去和模型本身融合而是在模型外围加一个“记忆层”。它把值得记住的内容提取成结构化条目存进本地文件然后在新会话的每次提问前自动把与当前问题最相关的几条记忆注入进上下文。之所以采用这个方向是因为与其把整段历史塞回去不如只塞和当前问题有关的那几段。上下文窗口是稀缺资源记忆系统要做的是筛选而不是搬运。2. claude-mem 的核心设计两段式记忆结构与检索链路确定要做一个记忆层之后首先要想清楚记忆应该放在什么粒度上。我最终采用了“项目级 用户级”两段式结构这是整个工具最核心的架构决策。2.1 项目级记忆与用户级记忆的分工所谓项目级记忆就是只属于当前代码仓库、只在当前项目下生效的知识用户级记忆则是跟着你这个人走、在哪个项目里都通用的偏好和习惯。维度项目级记忆用户级记忆默认存储位置.cmem/project/~/.claude-mem/作用范围当前项目所有会话当前用户所有项目典型内容部署命令、架构决策、依赖坑、目录约定注释语言偏好、常用命令别名、代码风格要求生命周期随项目长期保留全局长期保留读取优先级项目级高于用户级用户级作为默认背景为什么一定要拆开因为如果不拆很容易出现“跨项目污染”。比如我习惯在 A 项目里用 pnpm在 B 项目里用 npm如果把“包管理器是 pnpm”记成用户级记忆B 项目里的 Claude 就会给出错误指令。反过来把“我习惯写中文注释”这种个人偏好放进项目级记忆换个项目又要重新教一遍也是一种浪费。两条记忆链互不干扰但在召回阶段可以合并。项目级记忆优先级更高用户级记忆作为兜底。这样设计之后大多数情况下模型都能拿到最贴合当前场景的信息。2.2 从“保存”到“召回”的完整闭环记忆系统不能只有存储还得有闭环。claude-mem 的完整链路可以拆成五个环节会话捕获在会话结束或用户发出特定指令时工具读取最近的消息抽出候选记忆。文本清洗过滤掉编译错误、测试输出、无意义闲聊只保留结论性内容。向量化存储把清洗后的文本转成向量存入本地索引同时保留原始 Markdown 文本文件。查询召回用户每次发新消息前把当前消息转成向量和已有记忆做相似度计算。注入上下文把命中的记忆按固定模板拼接成一段文字在模型处理用户问题时一起提供。这个闭环最大的好处是不需要模型自己主动“记”所有动作都是外部脚本在适当时机触发的。模型的职责仍然是回答问题而我们只是给它递了一张便签纸。这也让 claude-mem 可以适配不同厂商的模型只要对方能读取额外上下文就行。2.3 为什么选本地文件 向量索引而不是厚重数据库在存储选型上我纠结了很久。第一版想过用关系型数据库比如 SQLite因为功能完整也想过直接用向量数据库比如 ChromaDB因为检索方便。但最终选择了最简单的方案JSONL 文件 内存余弦相似度计算。数据量不大。个人项目跑一年有效记忆也就几千条。对这种规模线性扫描一千条向量做相似度计算耗时接近零根本没必要上重型数据库。依赖少启动快。用户部署环境里越少依赖越不容易出问题。一个纯 Python 包配一个本地模型就能跑安装体验很好。方便备份和同步。JSONL 文件可以直接用 Git 管理项目级记忆天然适合放进代码仓库。后期可替换。我把存储层封装成接口了哪天真的需要上 SQLite-VSS只需要改一个实现类上层的捕获和召回代码完全不用动。很多开源工具一上来就设计得特别重结果用户连编译都过不去。我的原则是先保证 10 秒内能跑起来再去谈扩展性。3. 安装与接入3步把 claude-mem 接到工作流光有架构还不够得能真正被用起来。我先说最基础的接入流程整个过程大概十分钟不需要改一行业务代码。3.1 环境要求与一条命令安装claude-mem 是一个 Python 命令行工具推荐 Python 3.10 以上版本。macOS 和 Linux 直接用 pip 安装pip install claude-mem cmem version安装完成以后在项目根目录执行初始化命令cmem init --project-dir . --storage-dir .cmem/memories当你看到命令输出创建了.cmem/目录说明基础环境已经就绪。这里提醒一句如果你用的是 Windows建议放到 WSL 里跑因为后面接 Claude 的会话钩子时主要依赖 bash 脚本纯 PowerShell 环境下有些环境变量拿不到。3.2 配置文件的逐项说明初始化会生成一个config.yaml里面有几个选项直接决定记忆质量我挑关键的来讲capture: min_length: 20 max_length: 2000 filters: - ^\\s*(Error|Traceback|panic): - ^\\[(INFO|DEBUG|WARN)\\] keywords_marker: #mem embedding: model: bge-small-zh-v1.5 threshold: 0.42 top_k: 5 recall: injection_mode: system template: | Below are memories from previous sessions: {memories}capture这一段是捕获规则。min_length和max_length用来过滤太短或太长的内容。短文本通常是零散对话长文本更像文档而非记忆两者都不适合直接塞进库里。filters是正则表达式白名单之外的兜底凡是匹配到的行直接丢弃。keywords_marker是手动标记位用户如果觉得某句话特别重要就在行尾加上#mem这样即使它不满足普通规则也会被强制保存。embedding这一段控制向量化模型和召回参数。threshold是最关键的它决定了“多相似才算相关”这个值我会在后面专门讲。top_k是召回条数上限个人经验是 5 条足够再多反而会稀释注意力。recall这一段定义注入格式。injection_mode可以选system或user意思分别是把记忆塞进系统提示词还是用户提示词。我习惯用system因为 Claude 对系统提示的权重更高。3.3 自动接入 Claude 的会话钩子配置文件搞定后用户最关心的肯定还是怎么自动生效。claude-mem 提供一个命令自动帮你把会话钩子写进 Claude 终端版的自定义设置里cmem install-hooks安装之后相当于注册了两个钩子SessionEnd会话结束前执行cmem capture --last-session把本次会话的关键内容捞出来。UserPromptSubmit用户发出新消息前执行cmem search --query $PROMPT --append把相关记忆以追加方式带给模型。原理上有点像给编辑器加宏命令。模型本身并不可控但事件钩子是确定的只要触发就一定会执行对应的脚本。这比在系统提示里反复强调“请记住之前的对话”要可靠得多。如果你用的 Claude 配置格式支持自定义 hook那么等效的手动配置大概是下面这个样子{ hooks: { SessionEnd: [ { matcher: .*, hooks: [ { type: command, command: cmem capture --last-session } ] } ], UserPromptSubmit: [ { matcher: .*, hooks: [ { type: command, command: cmem search --query \$PROMPT\ --append } ] } ] } }这个配置里matcher: .*表示所有会话都触发。实际团队场景中你可以改成matcher: .*\\.(py|js|ts)$把记忆范围精确到特定类型的对话。3.4 验证接入是否成功装完之后一定要做一次完整验证别急着用。先在一个会话里丢一句#mem 部署流程是 docker compose up -d服务跑在 8080 端口这里的#mem标记会触发强制保存。然后退出会话再新开一个会话输入这个项目怎么启动正常情况下终端里会出现类似这样的日志[claude-mem] recalled 2 memory entries (threshold0.42, top_k5)如果没有这行输出或者 Claude 依然没有提到“docker compose up -d”直接跳到第 5 节对着排查清单看。4. 实操过程核心环节的代码实现与参数选择架构和接入讲完了现在到最硬核的部分核心代码到底怎么写参数到底怎么选。这一节不追求给你完整源码而是把我当时写关键模块时踩过的坑和验证过的方法论分享出来。4.1 捕获阶段的过滤规则别把废话记进长期记忆很多人以为记忆系统最难的是存储和召回其实最脏最累的是捕获过滤。一开始我偷懒把会话全文都塞进记忆库结果第二天检索出来的全是“好的”“没问题”“报错信息”真正有价值的结论反而被淹没。所以我写了一个很朴素的启发式过滤器核心逻辑就是“宁缺毋滥”import re import hashlib from datetime import datetime def capture(raw_text: str, *, project: str) - list[dict]: entries [] for line in raw_text.splitlines(): line line.strip() if len(line) 20 or len(line) 2000: continue if re.match(r^\s*(Error|Traceback|panic):, line): continue if claude-mem in line: continue if not (line[0].isalpha() or line.startswith(#)): continue entry_id hashlib.sha1(line.encode()).hexdigest()[:12] entries.append({ id: entry_id, text: line, project: project, scope: project, created_at: datetime.now().isoformat(), access_count: 0, priority: P2, }) return entries这里的几条规则是逐步调出来的长度过滤解决“太短没有意义”的问题。实测中长度低于 20 个字符的句子九成都是应答语。错误日志过滤解决“报错污染记忆”的问题。Error:和Traceback:开头的行虽然可能重要但往往只是过程噪音真正的解决方案会在后续对话里出现不需要把报错本身存下来。首字符约束解决“碎片内容入库”的问题。一个正常结论通常以文字或#开头以括号、符号、数字开头的行大多是代码块或输出。#mem手动兜底解决“自动过滤把重要内容误杀”的问题。只要你主动标了就无条件保存。这套规则不算聪明但它的优势是“白名单式逻辑”可解释、可调试。相比让大模型来抽取规则引擎在小规模工具里反而更稳定也能在完全离线的环境下运行。4.2 嵌入模型选型与相似度阈值用 200 条样本做回归测试记忆要被召回本质上是一个“语义匹配”问题。把文本变成向量然后算余弦相似度这是最通用的做法。我当时在本地嵌入模型之间纠结了很久最后用 200 条真实会话记录做了一组回归测试。模型内存占用中文效果百条向量平均耗时all-MiniLM-L6-v2约 90MB一般25msbge-small-zh-v1.5约 100MB好30msbge-base-zh-v1.5约 400MB很好55ms如果你和我一样主要写中文注释和文档直接选bge-small-zh-v1.5。它在中文语义上的表现比通用英文模型好一大截内存占用又不像 base 版那样离谱。如果你的场景是纯英文用all-MiniLM-L6-v2也完全够用。真正需要花心思的不是选模型而是定阈值。我拿 100 对“相关记忆”和 100 对“不相关记忆”分别算相似度画出分布找到错误率最低的交叉点。最终我选择了0.42。这个值意味着得分超过 0.42 就认为“相关”并召回低于就放弃。阈值太高会漏掉很多表达方式不一样但意思相同的记忆阈值太低会每次都塞进来一堆无关片段让 Claude 分心。个人经验是先从 0.45 起步跑一周看日志如果经常出现“没召回”再把阈值降到 0.40反过来如果经常召回无关条目就往 0.50 调。4.3 召回结果如何拼进上下文才不“喧宾夺主”召回只是第一步怎么把记忆注入上下文也很讲究。最直观的方式是把所有候选记忆拼成一个大字符串。但我前期发现这样做 Claude 容易过度依赖记忆有时明明当前上下文给出了新结论它还是要沿用旧记忆。我的解决办法是在注入前加一句“如果记忆与当前上下文冲突以当前上下文为准”同时在每条记忆后面附上元信息MEM$(cmem search --query $1 --limit ${TOP_K:-5} --format compact)然后拼出这样的模板[记忆片段] - 项目部署docker compose up -d 启动端口 8080项目记忆相似度 0.87 - 包管理器项目统一使用 pnpm不要用 npm用户记忆相似度 0.76 请在回答时优先参考以上历史记忆。如果它们与当前对话上下文冲突以当前上下文为准。注意这里有个细节每条记忆都带了“相似度”。这是因为当模型看到“0.87”这样的高分时会更重视这条记忆而看到“0.61”这种边缘分数时会自然地把它当作弱参考。这个技巧比简单排序有效得多。另外一个硬规则单次注入不超过 5 条。上下文窗口是有限的塞得越多模型反而越难抓住重点。我试过把 top_k 调到 10结果回答质量不升反降因为模型在几条冲突记忆中间来回摇摆。4.4 遗忘与去重长期记忆最容易被忽略的两件事长期记忆系统如果没有“遗忘机制”很快就会变成一个垃圾场。我第一版就没做去重结果同一条部署命令因为表达方式不同被存了十几遍召回的时候满屏都是重复信息。后来加了两个机制去重每条新记忆入库前先和已有记忆算一遍相似度如果分数超过 0.9就认为是在说同一件事不新增条目只把旧条目的access_count加 1。过期淘汰每个记忆自带created_at和access_count。一个新条目如果 30 天内从来没被召回过就进入候选删除列表反之每次被召回都会刷新计时。同时提供一个手动维护命令cmem forget 记忆ID cmem stats --top-unusedstats --top-unused会列出最久未被访问的记忆方便你定期清洗。我自己的习惯是每两周跑一次看看哪些记忆已经过时比如某个临时部署方案已经失效了就手动删掉。记忆体系的理想状态不是“无限增长”而是“精准命中”。控制在一两千条以内召回准确率会明显更高。5. 常见问题与排查技巧实录这里的内容全部来自我实际使用和给朋友调试时的经验。工具越简单出问题时反而越隐蔽很多坑不在代码逻辑而在环境细节。5.1 记忆一直没生效先查 hooks 和路径现象新会话里无论怎么提问都看不到[claude-mem] recalled的日志。排查顺序很重要别一上来就怀疑模型。我固定的排查步骤如下执行cmem log --tail看有没有capture和search的触发记录。手动执行cmem search --query 部署确认记忆库里是否真的存了数据。查看当前工作目录的绝对路径确认cmem init时创建的.cmem目录就在项目根目录下。检查 hook 配置里的环境变量CLAUDE_PROJECT_DIR是否存在很多 hook 脚本拿不到“当前项目路径”就会把记忆写到错误的位置。我遇到最多的排障案例都是因为用户在子目录里执行了初始化然后又在根目录打开会话两边各自建了一套.cmem数据完全没对上。这种问题看配置文件没用直接find . -name store.jsonl一下就能发现。5.2 中文语义召回不准调阈值不如换模型现象记忆库里有相关内容但由于提问的措辞和记忆不一样召回结果为空。首先说一个常见的错误操作遇到召回不准就去把阈值调低。我试过把 0.42 一路调到 0.30结果召回的准确率急剧下降模型经常把“什么时候上线”和“升级依赖版本”当成同一件事。更有效的办法是换嵌入模型。all-MiniLM-L6-v2对英文效果好但对中文的表达方式理解很弱经常出现同义词匹配不上的情况。换成bge-small-zh-v1.5之后同样的问题基本消失阈值甚至还可以往 0.45 调。如果你的对话里充满了项目专有名词、缩写、内部代号光靠语义向量是不够的建议直接启用 claude-mem 的混合召回模式BM25 关键词倒排检索 向量语义检索两者结果做加权合并。BM25 保证“项目名”“端口号”“命令”等关键词能精确命中语义向量保证“换个说法也能找到”。5.3 记忆文件越来越大自动压缩与清理策略现象.cmem/memories/store.jsonl越来越大启动时加载速度明显变慢。记忆文件不是不能增长但不能无限制增长。我的经验是当 JSONL 超过 5MB 时需要跑一次压缩cmem compact --merge-threshold 0.75 --drop-empty 90--merge-threshold 0.75会把相似度超过 0.75 的记忆合并成一条两个语义相近的表达只保留文字更长、最近访问次数更多的那条。--drop-empty 90表示删除 90 天从未被访问、且没有优先级标记的记忆。压缩前一定要先备份哪怕只是cp store.jsonl store.jsonl.bak。因为合并算法再谨慎也可能误伤一些看似重复但信息点不同的记忆尤其是出现“命令 A 和命令 B 看起来差不多但绝对不能相互替代”这类场景。5.4 多人协作时如何共享记忆现象团队里两个人用同一个项目A 记录的记忆 B 完全看不到。claude-mem 的项目级记忆天然适合进 Git。最简单的做法是把.cmem/目录提交到仓库里这样所有团队成员都能拉到同一套记忆。但用户级记忆在~/.claude-mem/绝对不要进仓库否则会泄露个人偏好和本机路径。共享记忆有一个隐蔽的坑Git 合并两个store.jsonl文件时经常冲突因为两个人同时新增了不同记忆行。直接 merge 会让 JSONL 变坏。正确做法是约定“一人合并多人提交”日常各写各的本地记忆月底把项目级记忆导出成一个memories-export.json手动合并后再由一个人提交。claude-mem 的import命令会按 ID 去重重复的条目不会再次插入这样就能安全地把多份记忆合并为一。我在实际使用中还有一个体会最明显claude-mem 的价值不是让 AI 记住所有事情而是推着你把项目里的重要结论不断结构化。以前我每次会话结束都直接关终端现在会下意识补一句#mem 记住 xxx。这等于每天顺手给项目写了一份活文档。最后再分享一个小技巧给记忆条目加优先级标签比如[P1]表示“这是关键结论必须遵守”[P2]表示“这是一般背景信息”。注入模板里按 P1 到 P3 排序关键结论就会比背景信息更早出现在模型眼前。这个改动很轻但对回答质量的提升极其显著。