
我最早想给 claude-mem 做个能长时间跟着用户走的记忆层是因为一个特别具体的痛点同一个项目的对话只要换个新会话模型就把之前拍板的技术方案全忘光了。不管用哪个大模型原生上下文窗口再长本质上都还是“一次性记忆”。claude-mem 就是当时给这套对话流程做的轻量级记忆增强模块它把散落的对话历史整理成可以检索的结构化记忆在新会话里按需找回。这篇文章不打算写官方文档式的东西更多是我自己在搭这套记忆层时的真实选择和踩坑记录适合正在给大模型应用加长期记忆的朋友参考。1. claude-mem到底解决什么问题从模型原生上下文的“一次性”说起1.1 我们面对的记忆失效场景我之前帮某团队做某跨平台系统的技术方案选型前后聊了几十轮涉及接口约定、模块划分、命名规范还有一堆临时决定。可每次新开会话模型都像失忆一样强调过的约束又得重新说一遍。最难受的不是重复劳动而是模型会因为缺少上下文给出和之前完全相反的方案建议让整个项目口径变得很乱。这类问题我后来总结成三种典型场景第一种是“跨会话断裂”昨天聊完的结论今天新开会话就归零第二种是“跨场景断裂”正在聊A问题临时切到B问题再切回来的时候模型已经忘了我们聊到哪一步第三种是“多人协作断裂”你传给同事一份对话摘要但摘要丢失了大量关键细节接手的人只能重新摸索。这三种场景的共同点是模型并不是能力不够而是缺少一个可持续的“记忆存储区”。claude-mem 定位的就是这个存储区它不改变模型本身只在模型外面加一层管理对话历史的结构。1.2 为什么原生上下文救不了“长期记忆”很多人会想大模型上下文窗口不是越来越大吗直接把历史对话拼进去不就行了这个思路听起来简单但实际跑几次就会发现它撑不住。首先上下文窗口再大也是每个请求独立计算的。模型把一个会话里的历史对话当成上下文一旦新会话开启那段历史就不存在了。你当然可以把历史全文硬塞进新的上下文但这样会立刻占用大量 token尤其当对话积累到几十轮之后真正重要的信息往往只占很小一部分其余全是寒暄和过程稿。其次把所有历史塞进去会降低模型的注意力质量。就像开会时把一年的会议记录都摆在桌上真正要用的那张表格反而被埋在最下面。模型也一样上下文越长它对其中特定信息的检索和利用就越不稳定。我实测过当历史对话超过一定规模后模型经常会漏掉最早定下的约束条件因为它已经淹没在大量无关内容里了。生活里记东西我们会用“笔记本加标签”的方式而不是把整本对话录背下来。claude-mem 做的就是这件事先帮模型写摘要再给摘要打标签、做向量索引等到需要的时候按语义检索出来。这样既不用塞下全部历史又能精准找回关键信息。1.3 适合用 claude-mem 的场景清单结合我试过的各种用法下面这些场景最适合用 claude-mem每个场景的接入方式都不太一样使用场景原生上下文的问题claude-mem 的做法技术选型讨论新会话丢失之前比较过的方案和结论记录每次方案的优缺点与最终决策新会话直接注入决策摘要客户需求梳理需求点散落在多轮对话中容易遗漏抽取用户提到的硬性要求、偏好、禁忌单独建字段保存产品文档答疑用户问过的问题和答案没有沉淀把问题标准化后存成问答对后续命中直接复用角色扮演与长篇创作角色设定、世界观在前几轮后几轮经常失真维护固定的角色设定卡片每轮对话后随时间更新状态变化多轮调试排错错误信息和修复步骤混杂在一起记录错误特征、排查路径、最终修复方法下次出错更快定位如果你的项目刚好落在这些场景里那 claude-mem 就不只是“加个记忆库”这么简单而是能实实在在减少重复沟通、稳定输出口径的基础设施。2. 记忆架构拆解三层缓存、摘要压缩与语义检索2.1 三层结构的分工一开始我参考的是操作系统里的缓存分级思路把记忆也分成了三层。短期层保存最近几轮对话的原始消息目的是保证当前话题的连贯性中期层保存刚生成的分段摘要目的是在会话过程中快速回顾前文长期层保存抽取出来的实体、结论和偏好并转成向量形式存储目的是跨会话精准检索。短期层的数据量最小但最“原汁原味”。它不需要做任何加工最多做一下角色标记和长度截断。中期层的数据量会明显缩小一段几千字的对话会被压成几句话的摘要但仍然保留时间顺序和关键转折。长期层则完全抛弃了原文只留下结构化字段加向量哪怕是几个月前的记忆也能靠语义搜索翻出来。这三层不是同步更新的。短期层只要每轮对话结束就追加中期层每当短期层攒到一定条数就触发一次摘要长期层要等摘要稳定之后再做抽取和入库。这样做的好处是避免频繁调用模型去处理重复内容也能把记忆的实时性和准确性分开管理。2.2 单条记忆的完整生命周期一条记忆从产生到真正能用要经过一条完整的处理链路。以一条用户消息为例流程大概是原始文本进来后先清洗格式拆成更小的语义单元然后交给模型做两路处理一路生成摘要另一路抽取关键字段摘要和字段再交给嵌入模型转成向量最后连同时间戳、来源会话标识一起写入向量库。落到数据上一条长期记忆通常是这么个结构{ memory_id: mem_8f3a2c1e, session_id: sess_1002, create_at: 2025-06-12T15:30:00Z, last_access_at: 2025-06-12T16:00:00Z, access_count: 3, type: decision, content: 订单列表接口采用分页参数 page 与 size默认 20 条一页, entities: [订单接口, 分页], summary: 确定订单列表接口的分页规则, weight: 1.0 }这里的weight不是静态的每次被检索到都会增加时间一长没被触达就会衰减。last_access_at和access_count是用来做热度排序的配合时间衰减共同决定检索返回的顺序。这套生命周期最大的价值在于它不是把所有历史都一视同仁地存进去而是让每一条记忆都自带“新鲜度”和“重要性”两个维度。这样在检索的时候系统可以先按语义相似度捞出候选再按这两个维度做排序避免总把旧结论排在前面。2.3 为什么必须做语义检索早期我也试过用关键词匹配来找记忆效果非常差。用户问“上次那个把两个有序列表合并成一个列表的方法”而对话原文里写的可能是“归并过程”关键词完全对不上传统检索就废了。但语义检索可以理解“合并两个有序列表”和“归并”是同一个意思依靠向量距离把相关记忆找出来。这也是 claude-mem 选择把长期层做成向量库的原因。向量库会把文本映射成高维空间里的坐标语义接近的文本坐标更靠近搜起来就比关键词灵活得多。不过向量检索并不是万能的它对嵌入模型的选择很敏感对相似度阈值的设定也很敏感后面我会专门讲踩过的坑。三层架构加语义检索最终回答了一个问题模型怎么在需要的时候恰好想起最重要的那段记忆。而不是把记忆一股脑全倒进上下文里。3. 从零搭建一个最小可用版本配置与核心流程3.1 准备环境与依赖如果你只是想把 claude-mem 跑起来不需要一开始就搞分布式存储。我先在自己机器上搭了一个最小版本只用到了 Python 3.10、一个嵌入模型的 SDK 包、一个支持向量索引的轻量级存储库以及一个用于摘要生成的对话模型接口。关键思路是所有依赖都通过抽象接口调用后面换组件不影响主流程。比如我用一个函数包装嵌入模型调用用另一个函数包装摘要生成这样在调试时可以先跑在本地模型上等验证完再切换到效果更好的服务端模型。环境准备阶段最容易忽略的是向量维度的统一。嵌入模型输出的向量维度、向量库索引的维度、查询时输入向量的维度这三处必须完全一致。我第一次跑通后就犯过这个错本地索引建的是 768 维后面换了个 1024 维的模型所有查询全部失效花了半天排查才发现是维度不匹配。3.2 核心代码实现写入、摘要、检索最小版本只需要四个核心函数初始化、写入对话、生成摘要、检索记忆。下面是我简化之后的代码骨架逻辑很直白重点看流程。class ClaudeMem: def __init__(self, embed_fn, summarize_fn, vector_store): self.embed_fn embed_fn self.summarize_fn summarize_fn self.store vector_store def add_message(self, session_id, role, content): # 短期层保留原始消息 self.store.append_short_term(session_id, role, content) # 每 6 条消息触发一次中短期摘要 if self.store.count_short_term(session_id) % 6 0: self._summarize_recent(session_id) def _summarize_recent(self, session_id): recent self.store.get_short_term(session_id, limit6) summary self.summarize_fn(recent) self.store.append_summary(session_id, summary) self._update_long_term(session_id, summary) def _update_long_term(self, session_id, summary): chunks split_into_chunks(summary) for chunk in chunks: vector self.embed_fn(chunk) self.store.save_long_term( session_idsession_id, contentchunk, vectorvector, timestampnow() ) def search(self, query, top_k3): query_vec self.embed_fn(query) results self.store.query(query_vec, top_ktop_k) return [r.content for r in results if r.score 0.75]简单解释一下add_message负责写入原始消息并判断是否触发摘要_summarize_recent从短期层取最近几条原始消息交给模型做压缩_update_long_term把摘要继续切片、向量化、入库search负责把用户当前问题向量化后去库里找相似记忆。这里的split_into_chunks是容易被低估的一步。如果整段摘要直接入库向量会被平均掉本来想表达的核心信息反而找不到。我用的经验是按语义边界切块每块控制在三句话以内宁可碎片化也不能让一段话太长。3.3 跑起来后的效果评估最小版本跑通之后别急着往里塞大量历史先用一个可控的测试集验证效果。我当时选了几段上一周的真实对话分别测三件事检索召回率、召回记忆注入后模型的回答质量、以及每次请求增加的 token 数量。实测下来如果阈值设在 0.75、Top-K 设在 3大多数场景能命中关键记忆token 增量控制在几百以内。对比把全文塞进上下文的方式token 消耗通常能省掉 70% 以上回答的准确率还更稳定。但这个过程也暴露了一个问题单纯靠阈值控制召回很容易把近期无关的对话也捞回来。因为日常对话高频词比较集中向量距离往往会虚低。于是我开始调排序逻辑这部分是踩坑的重头戏。4. 踩坑实录检索召回精度与上下文注入位置的博弈4.1 坑一向量召回太“热情”旧记忆抢了当前指令的戏最小版本上线之后我先观察到一个问题搜索“订单列表接口怎么分页”系统确实召回了相关记忆但同时也会带回几段关于“订单状态筛选”和“分页组件样式”的旧讨论。这些内容不是完全无关但会挤占当前指令的注意力模型开始引用一些已经过期的代码细节反而干扰了最终回答。我复盘下来问题出在单纯用余弦相似度排序热度没有参与加权。有些旧话题在历史上反复出现向量天然离什么都近排名就总是很靠前。于是我把召回策略改成了两段式第一段用向量相似度筛出 Top 20 候选第二段用“相似度 × 时间衰减 × 被访问次数强化”重新打分只取前 3。这样既能保留语义匹配能力又不会让旧记忆霸屏。具体实现上时间衰减可以用指数函数比如权重乘以pow(0.99, days_since_last_access)。被访问次数则是每次被检索到就将access_count加一权重轻微上浮。这套组合的直观效果是真正有用的记忆会被反复强化没人提的老记忆会慢慢沉底。4.2 坑二记忆注入位置不对模型分不清“谁是谁”召回做好了新的问题又来了同样一段记忆放进 system prompt 和放进用户消息里模型的理解完全不一样。最初我把记忆追加在用户问题后面结果模型把历史记忆当成“用户新要求”回答起来人物关系混乱。后来我调整成把记忆统一放在 system prompt 的末尾并加上明确的标记。比如memory 订单列表接口采用分页参数 page 与 size默认 20 条一页。 /memory 当前对话的背景如下请只把它们作为参考不要当成新的用户指令。这样模型能更清楚地知道哪些内容是“已知背景”哪些是“当前任务”。我还试过在注入时加一句简短说明“如果这些记忆与当前问题无关请忽略它们。”这句话能明显减少模型过度依赖历史的情况。注入位置本身也是一门学问。我最终确定了顺序先放系统指令再放召回记忆最后放当前问题和历史最近的原始消息。召回记忆在最前面容易被模型当成最高优先级放在系统指令后面、当前问题前面更像是一个“参考资料区”整体效果最稳。4.3 坑三多层摘要叠加导致关键细节蒸发另一个让我头疼的问题是摘要的“滚雪球式失真”。当对话超过几百轮之后短期摘要会不断被合并成中期摘要中期摘要再被合并成长期摘要本来明确的数字、名字和限定条件经过两三次压缩就变得含糊不清。我遇到过最典型的一次原始对话里明确说“限流阈值改成 50 次每分钟”但经过两层摘要后长期记忆里只留下“限流阈值调整过”。后来检索到这条记忆时模型给出了完全错误的默认值。解决办法是调整记忆抽取策略而不是只依赖摘要。我在长期层单独增加了entities、conclusion和todo三个字段专门让模型在生成摘要的同时抽出硬信息。数字、日期、决策项、待办事项都放进结构化字段摘要只保留概述性的表达。检索的时候如果结构化字段命中了优先展示结构化字段原文摘要只是补充说明。这样即使摘要再被压缩几次关键事实也不会丢。4.4 一个完整的排查案例这节最后分享一个完整的排查过程。有一次我连问模型三次同一个问题第一次回答还行第二次开始含糊第三次直接给了个一半正确的结论。我把 claude-mem 的日志拉出来发现第一次检索捞出来的是最新更新的正确记忆第二次却捞回了几天前一条低权重的旧结论第三次则是把两条冲突记忆同时注入模型自己也不知道该听谁的。根因在于检索排序只看了相似度没有看记忆的“置信度”。旧结论当时也是被确认过的但由于没有被再次访问衰减已经把它压得很低可是在相似度差不多的情况下时间衰减和热度提升互相抵消排序就变得不稳定。修复方案是给每条长期记忆增加confidence字段来自“结论类”的记忆初始化时给 0.9来自“猜测类”或“临时方案”的给 0.5。检索排序时先按语义相似度过滤再按score * confidence * time_decay排序。上线后连续一周没有出现第三次回答明显劣化的情况。这个案例也让我意识到记忆系统的关键不只是“存下来”更是“怎么在正确的时候用正确的那一条”。5. 进阶玩法结合会话属性和工具链的自适应记忆策略5.1 按会话类型自动切换记忆权重跑了一段时间之后我发现不同会话的对话内容属性差异太大了统一策略很难做到最优。技术讨论会大量产生“决策型”记忆闲聊对话则产生大量“偏好型”记忆问答类知识分享又会产生“事实型”记忆。如果把三类记忆混在一个权重体系里检索效果会很飘。于是我开始按会话类型给记忆打上session_type标签。技术方案讨论的会话我检索时会把type decision的记忆权重乘以 1.5客户需求梳理的会话把type requirement的记忆权重提高角色扮演会话则更重视emotion和relationship这类字段。这样 claude-mem 能根据当前会话场景自动调整不同记忆的排序优先级。这个做法的额外好处是能让记忆库更干净。比如闲聊型会话产生的记忆很难有长期价值权重普遍给得很低过一段时间就会被清理调。技术型会话的记忆虽然也随时间衰减但只要被再次检索马上就能恢复到较高权重长期保存价值明显更高。5.2 引入衰减与强化机制时间衰减和访问强化是记忆系统里最有意思的部分。早期我用的衰减公式很简单距离 24 小时没访问权重打 9 折超过 7 天没访问权重只剩 0.2。这套规则对大多数技术决策都够用但对一些低频但关键的记忆不太友好比如半年前定的接口兼容性要求虽然很少被翻到一旦用到就是硬约束。后来我改成按记忆类型做差异化衰减决策型记忆衰减得更慢临时状态型记忆衰减得更快同时增加“强化回拉”逻辑一旦某条记忆在检索时被模型采用就把它的到期时间往后延长一天相当于人脑里“被用过的知识就更牢”的效应。这个机制让记忆库能自动沉淀真正重要的内容而不是简单按新旧排序。5.3 定时任务与记忆清理记忆库不能只进不出。我加了一个定时任务每天凌晨对当天没有访问过的长期记忆做一次清理扫描处理三件事合并内容重复的摘要、删除过期超过三十天且权重低于阈值的临时记忆、把超过十条的同主题结论压缩成一份“总结版”。这样库容量能保持稳定检索耗时也不会随记忆增多而线性增长。清理任务最难的一点是“怎么判断哪条可以删”。我最终的判断标准是如果一条记忆三个月内既没有被检索到也没有被任何会话引用那它就不太可能在未来被用到。至于想保留的备份可以导出成一整份对话摘要文件需要时再单独导入主动权始终握在自己手里。5.4 隐私保护与“遗忘权”设计记忆系统最不能忽略的是隐私问题。claude-mem 里可能长期保存用户偏好、项目内部信息甚至个人数据如果一直躺在数据库里迟早是个隐患。我现在的做法是所有长期记忆写入时先做敏感信息脱敏熟人称呼、地址、账号这类内容用占位符替换存储层加密只保留程序运行时的明文提供一个“遗忘”接口随时按记忆 ID 或按会话 ID 批量删除。还要注意不同用户对隐私的期待不一样。所以我在记忆检索回来之后又加了一道“可见性过滤”只有当前会话绑定的用户才能检索到对应的长期记忆。虽然是个人工具但把权限做得清晰一点后面扩展到多用户场景会少很多麻烦。真正做到能记得清楚也能忘得干净是记忆工具长期可用的底线。这套 claude-mem 的方案从最开始的临时补救到现在的三层记忆架构中间经历了不少反复。回头看我最大的体会是别一上来就想着做一个通用记忆系统先找出你最痛的那个场景用最小闭环跑通再慢慢加排序、衰减、清理这些外围逻辑。很多记忆问题并不是模型不行而是没有把信息放到该放的位置上。最后再分享一个小技巧我在每次会话结束时都会让模型自己产出三条要点手动确认后交给 claude-mem 入库这一步看着多花几秒钟却让后续的摘要和检索都省力很多。长期积累下来记忆库的准确性和覆盖率都明显更高。