
我先跑了好长一段时间每天都开着几个独立的会话来处理同一批任务。结果很清楚Claude在新会话里根本不记得我在上一个会话中反复叮嘱过什么。于是我把这套问题收敛成一个叫claude-mem的小项目用外部存储和自动提取的方式给Claude补了一层可复用的长期记忆。这篇博文就是整个实现过程的记录从存储选型到提取流程再到语义检索、记忆清理谈的都是真实跑通或翻过车的细节。如果你也在用Claude的API或CLI做长线项目或只是被“AI每次都要重新认识我”这件事弄得头大那这篇文章应该正好对得上。下面我按自己做这个工具的顺序把每一步的设计思路、代码片段、实测结果都摊开讲。1. claude-mem出手之前每个新会话都像重新认识一遍搭子先说背景。我日常用Claude的场景大概是三类写技术方案、处理代码重构、整理一段复杂的运维笔记。这些事并不是一次会话就能聊完的往往今天聊前半段明天接着聊后半段。最初我抱着的想法很简单——每次开头把背景再贴一遍不就行了结果发现需要重复交代的东西比我预想的多得多。举个例子一个后端项目里我明确说过“不要动迁移脚本”这件事。当时会话里聊得清清楚楚上下文也覆盖得住。但等第二天我开新会话去改另一个模块时它给我的建议里又出现了“建议修改迁移脚本”的说法。我必须再次强调一遍“这个地方不要动原因上次说过了”。这种往返如果只在闲谈中出现一两次还能忍但如果每次开新会话都要发生基本就是在浪费生命。1.1 内置的记忆方案为什么不够用我用过几个Claude官方和生态里已有的记忆机制比如自定义指令、项目资料库、以及把聊天记录重新放入上下文的做法。它们各有各的适用面但都离我想要的“自动、长期、无需手动维护”有一段距离。第一类自定义指令和全局偏好。适合放那些永远不变的东西比如“回复用中文”、“代码里注释用简体中文”、“输出尽量简洁”。但项目级的事实、临时产生的决定这些没法全部塞进一个固定指令里因为它们是动态的。第二类把历史聊天记录完整作为上下文带回来。理论上可行但成本很高。一是token消耗会随着会话数膨胀二是早期记录里的大部分内容比如寒暄、试错过程、无用备选方案根本不值得再次占用上下文。真正要的只是那些“提炼过的结论”和“用户偏好”。第三类只靠Claude自己的上下文长度硬撑。我观察到的最直接问题是长上下文在谈到后面时前面的一些关键指令确实会被“稀释”。模型不是不记得而是在输出时对早期信息的注意力权重下降。这跟人的短时记忆背书很像看一遍能记住高潮部分但早就忘了开头提过的小规则。所以我拿定的主思路是记忆不一定非要存在于模型内部它可以存在于模型之外——以结构化的方式存下来下一次会话开始之前再把最相关的部分“喂”进去。这本质上就是给Claude做一个外挂记忆皮层而这正是claude-mem要做的事。1.2 claude-mem给自己定的四条原则在设计最开始我给了这个项目四条必须满足的约束记忆必须自动沉淀不靠我手动写摘要。如果工具还要我自己整理那不如不开新会话。记忆必须有多种类型不能简单地按“关键词原文”存一大坨。检索必须考虑语义而不是只能靠两个词硬匹配。记忆数据默认留在我自己的机器上不强制上传到任何中间服务。这四条后来确实避免了不少返工。第一条让我去解决“自动提取”第二条让我细化数据模型第三条直接指向嵌入和向量距离计算第四条则让整个存储层保持轻量、可导出。接下来几节会围绕这四点在实现上的取舍展开。2. 存储结构从“聊天原文”到“一条条结构化记忆”存储是所有记忆功能的地基。如果只把整段聊天记录丢进一个大文件那后续的检索、去重、删除都会变得很难做。所以我一开始就决定每个会话结束后要把里面值得记的内容拆成一条条独立的记忆条目。第一版我试过用现成的向量数据库部署确实不算难但为了一个个人工具去维护一个额外服务有点过于笨重。后来我重新简化改为“JSONL文件 Python脚本扫描 内存中做向量检索”。这可能是整个项目里最影响复现成本的决定。2.1 一条记忆条目的完整字段我给每条记忆定义了下面这些字段。先看代码后面我会逐一说明为什么需要这些字段。{ id: b3f2c1a8-6e4d-4f90-9ba1-23f0c1c4e021, content: 用户的项目X中data/migrations目录下的迁移文件不能再改动之前有两次因为误改导致数据不一致。, category: project_fact, source_session: 2025-06-11T14:32:10_session_14, created_at: 2025-06-11T18:02:33, updated_at: 2025-06-11T18:02:33, importance: 0.85, embedding: [0.013, -0.022, 0.119, ...] }content不用多说是记忆的自然语言描述。category用来区分这条记忆到底属于哪种信息用户偏好、项目事实、执行规则、第三方工具特性等。source_session指向来源会话方便出问题时回溯上下文。created_at和updated_at负责时效性排序。importance是我让Claude在提取时顺带给的一个权重表示这条信息对后续工作的重要程度。embedding则是这条记忆的语义向量用于后续相似度召回。2.2 为什么是JSONL而不是SQLite可能有人会觉得这种结构化数据用SQLite更合适。我也试过确实能用。但最终选JSONL有一个非常现实的原因可读性和可审查性。JSONL的基本思路是每一行一个JSON对象纯文本文件任意编辑器都能打开。当我怀疑某条记忆提取错时可以直接打开文件查看原始内容不存在数据库导出的麻烦。另一个好处是备份简单文件拷走就算备份。对个人工具来说SQLite的复杂查询能力在这里用不上几次反而让数据变得更加不透明。每个会话产生的记忆追加在同一份memories.jsonl里文件按月份归档成多个分片。这样即使跑上一年也不会出现单文件大得完全没法处理。每次会话结束之后程序会重新扫描有更新的分片更新内存里的向量索引。这一步很简单不追求实时同步。3. 会话结束后怎么自动把对话“消化”成记忆存储结构定了之后最难的部分就来了怎么把一整段聊天记录里真正值得记的东西提取出来。我第一个自然的想法是让Claude自己来做这件事。它是对话的参与方理解上下文让它输出结构化摘要顺理成章。但要把这件事做好不能只是丢一句“帮我总结聊天记录”。需要给它一个足够严格的抽取规范让输出格式稳定、字段完整、且不掺杂主观判断。3.1 捕获会话内容方法比预想简单。我主要使用Claude API每次调用的请求和响应本来就以消息列表的形式存在本地日志里。所以我写的提取脚本就是从这些日志里读取当前会话的消息数组然后丢给一次专门的提取调用。如果你用的是官方CLI也有办法。把CLI配置成输出为JSON格式或者直接监听其产生的会话记录文件。核心原则只有一个要在会话结束后拿到完整、未截断的消息内容越完整越容易提取准确。3.2 用一次Claude调用做信息抽取提取用的提示词是我反复调试过的。这里放一个简化版效果已经能跑到让我满意的水平。你是在执行一项记忆提取任务。下面是一段用户与AI的完整对话请从对话中找出以下类型的信息 1. user_preference用户明确的偏好、口味、常用工作方式。 2. project_fact关于用户当前项目的确定事实比如目录结构、模块功能、已做的决策。 3. execution_rule用户在对话中给出的行为约束例如“不要修改X”“每次先跑Y再提交”。 4. tool_note对话中对某个工具、命令、服务的重要使用经验。 要求 - 每条记忆必须是独立完整的句子不携带对话中的闲聊。 - 不要输出已经过时或被用户明确撤回的信息。 - 为每条记忆添加importance字段范围0.0到1.0体现对后续工作的重要性。 - 只输出JSON数组不要额外解释。 对话内容 {conversation_text}这里的关键在于我给了模型非常具体的分类。如果没有这个分类模型很容易把“用户今天喝了杯咖啡”这种废话也当成记忆写进去。有了分类约束它会更倾向于抽取操作性强、可复用的信息。3.3 让提取结果保持干净模型第一次提取出来的结果不会完全干净。我当时遇到的最大问题是重复信息。不同会话里用户反复说同一个偏好模型就会各记一条内容高度相似但不完全一致。如果不处理几天后检索出来的记忆会十条里有四条在讲同一个事。我的做法分两步第一步做一遍规则去重。把字段拼接成指纹讲同一类偏好但措辞相近的条目合并为一条保留updated_at最新的那条。第二步做一遍质量过滤。把所有importance低于0.4的记忆直接丢弃。这一招很有效因为绝大多数“项目寒暄”和“无意义确认”都会被模型打低分。我在实际运行中大约有一半提取出来的原始记忆会被这层过滤拦掉。最后再补充一个细节提取和过滤用的模型不一定要和主对话用同一个。实际上用更轻量的模型做提取会明显省token我当时就用了一个比主对话便宜很多的模型来干这件事效果差异不大。核心逻辑是提取任务对推理深度要求不高关键是提示词写得好不是模型大。4. 检索与注入新会话开始前把最该想起来的记忆找出来记忆存下来只是第一步。真正让记忆产生价值的是在下一次会话开始前自动找出最相关的几条注入到Claude的初始上下文中。这一步的技术路线很清晰用户新会话的第一条消息出来后先把它变成一个查询向量再用余弦相似度去和所有记忆条目的向量做距离计算取TopK条最后拼成一段补充材料放进系统提示词。4.1 检索脚本的实现主体下面是我实现的简化版本。嵌入向量的生成我选择了一款本地开源embedding模型原因很简单不把对话查询发出机器保证隐私边界清晰同时也能省掉网络延迟。import json import numpy as np from sentence_transformers import SentenceTransformer model SentenceTransformer(local-embedding-model-name) memories load_all_memories() # 读取所有记忆条目并解析 def query_memories(query_text, top_k8): query_vec model.encode(query_text) results [] for mem in memories: vec np.array(mem[embedding]) score float(np.dot(query_vec, vec) / (np.linalg.norm(query_vec) * np.linalg.norm(vec))) results.append((score, mem)) results.sort(keylambda x: x[0], reverseTrue) return [mem for score, mem in results[:top_k] if score 0.30]这里有两个地方值得解释。一是阈值0.30这个值不是拍脑袋定的而是我拿过去两周对话记录做了一次抽样回放统计了“相关记忆的余弦得分分布”和“不相关记忆的得分分布”在两条分布交叉点附近取的分离值。二是为什么用余弦相似度而不直接用向量数据库的近似检索因为个人项目的数据量级通常几千条以内全量遍历也就几毫秒的事没必要引入索引构建的复杂度。4.2 把检索结果注入到Claude的上下文里检索出来的记忆条目并不会直接原样贴给Claude。我会把它们组装成一个专门的“记忆快照”段落放在系统提示词的最前面。以下是从用户历史会话中检索出来的长期记忆用memory标签包围。 它们帮助你在新会话里保持连续性但并非每条都适用于当前任务。 请参考其中与你当前任务相关的部分忽略无关内容。 memory [1] 用户偏好代码输出尽量使用Python类型注解避免使用动态类型推断。 [2] 项目事实项目X的数据处理模块位于src/process入口文件是run.py。 [3] 执行规则每次提交前必须运行项目根目录下的scripts/check.sh。 /memory加一层memory标签是为了让模型清楚区分“历史记忆”和“当前对话内容”防止它在回答时无脑复述记忆里的信息。我实测中发现如果直接以普通文本方式混入系统提示词模型偶尔会认为那是用户当前的新要求导致回复方向偏移。4.3 一个实测检索案例有个真实例子让我印象很深。某一次我新开会话问的是“帮我看看项目的定时任务怎么改”。检索出来的第一条记忆是“用户对cron表达式的格式很在意之前要求注释里必须写明执行周期”第二条是“项目X的定时任务配置都在deploy/crontab文件中不要擅自改成systemd timer”。这两条都不是当前问题里的显式关键词但如果用纯关键词匹配就完全碰不到它们。Claude拿到这些记忆后回答一开始就从那两个约束出发直接给出了符合历史偏好的方案。这种体验让我确定语义检索值得做而且这几个维度的分类让召回的内容比单纯“相似句子”精准得多。5. 实测一个月后我看到的体验提升和明显边界做工具不能只看能不能跑通得看长期用下来到底带来什么改变。我从开始使用claude-mem到现在大概跑了三个星期。这段时间内我刻意保留了一些失败案例因为只讲成功会让这篇博文失真。5.1 稳定性最明显的三类提升第一类代码风格的一致性。以前我开新会话写代码它会默认输出它自己的偏好风格我必须花半段prompt去纠正。现在因为记忆快照里存了“类型注解、变量命名、模块组织”等偏好一轮会话里基本不再需要重复纠正。第二类项目事实的复用。部署路径、环境变量意义、哪些服务不能动这类项目内事实在旧会话里出现频率极高也很适合被提取成project_fact。每次新会话都自动带出最相关的几条让我少打了很多重复的背景解释。第三类决策连续性。比如某项技术选型当时讨论出“暂不采用方案B”的结论。凡是这种结论被抽成记忆的场景后续新会话里都不会再有人给我推荐已经被否决的方案。这个帮助是最大的因为它避免了模型反复做已经被否定过的尝试。5.2 翻车点记忆污染和“太听话”也不是没有翻车。最典型的场景是记忆条目在某些情况下会被过度引用。我有一段时间在某个项目里做过一次临时实验使用某个替代库来测试性能。那条记忆被标成了project_fact重要性还不低。结果后面所有涉及该库的会话Claude都会默认我准备用这个库替换正式库实际并不是。这个问题的根源在于我并没有在提取规则里加上“临时性信息和实验性决定应当标记为低重要度或直接丢弃”。后来我在提示词里补了一句“如果这条信息是临时的、实验性的明显有后续推翻可能请将importance设到0.2以下。”这样它就能被默认过滤掉。第二个翻车点是“太听话”。记忆里存了一条“用户偏好输出尽量简洁”。结果在新会话中我特意要求“这次请给我完整推导过程”模型仍然倾向于被记忆里的简洁偏好带偏。这个问题我没法完全消除只能靠调整系统提示词里的措辞告诉它“当前任务的显式指令优先级高于历史记忆”。这个提示加进去之后情况好很多。5.3 时效性陈年记忆需要定期体检长期使用后另一个不容忽视的问题是陈旧记忆的堆积。比如一个项目已经改版旧的做法被弃用了但记忆里还残留着相关的旧约束。短时间内不会出问题但时间一长旧记忆和新上下文会产生冲突。我给每条记忆加了updated_at字段后设定了一个简单的时间衰减逻辑超过一定天数没有再次匹配到任何检索请求的记忆会在检索结果里降低排序权重。这样它不至于完全消失但也不会一上来就干扰新会话。真正的清理还需要靠手动复核所以我给CLI配了几条管理命令。6. 记忆管理遗忘功能必须比记忆功能做得更顺手记忆工具如果只有“写”和“读”那总有一天会因为写得太满而变得没法用。这个道理跟人的脑子一样——记住所有东西的人往往什么都记不牢。所以我在claude-mem里花了不少精力去做删除、修改、导出这些偏“遗忘”方向的功能。6.1 CLI收益我实现的CLI命令大概有这些claude-mem list # 查看当前所有记忆条目可按category过滤 claude-mem query 想找什么 # 手动测试某个查询能召回哪些记忆 claude-mem show id # 查看一条记忆的完整上下文 claude-mem delete id # 删除一条记忆 claude-mem export --format json # 导出全部记忆 claude-mem prune --days 30 # 清理30天前未被任何查询命中的低优先级记忆delete是最常用的。每当模型在回复中引用了某条“错误记忆”我会顺手看一下show id确认问题后直接delete。后续提交前会顺手删除所有敏感内容。6.2 隐私边界和其他限制我一开始就定了本地优先的原则记忆文件只存在于本机目录里提取时调用外部API拿到文本后再回到本地过滤不会定期把整个记忆库送到远端。也没有提供云同步功能因为记忆这东西一旦落在某个中心化服务上数据归属就变得模糊了。还有一个现实限制如果是使用第三方API代理或托管服务来调用模型记忆文件里的原始内容在提取过程中还是会被发送到模型服务方。这一点避不开但只要信息脱敏后再进对话风险就在可控范围内。对于红线等级的内容我建议不要引入任何自动记忆工具直接用隔离环境跑。6.3 后续可以扩展的方向目前这个小型记忆层的设计还有几个地方我觉得可以继续做深一是多用户场景下的记忆隔离与共享也就是让不同团队成员各自拥有私有记忆同时共享项目级记忆二是时间衰减的更精细建模而不是简单用阈值函数三是自动检测记忆冲突比如发现“新会话中的用户指令”与“旧记忆中的行为约束”矛盾时立刻用warning提示用户。这些方向要在真环境里花不少功夫调参但也正说明这个外挂记忆层的路子还没走到头。回看这几周的实践我最大的感受是给Claude加记忆本质上不是在增强模型能力而是在改善我和它之间的协作流程。模型还是那个模型但因为每次开工前我都有办法让它想起“我们上次怎么定的调子”很多重复沟通就被直接消掉了。如果你也想给自己的AI工作流加一层类似的能力不妨从我这里提到的最小闭环开始一个JSONL文件一次自动提取调用一段简单的相似度检索。先跑起来再根据实际会话慢慢加规则。这比一开始就上全套重工具要靠谱得多。