claude-mem 深度解析:为 Claude 构建持久记忆系统的架构设计与工程实践
1. 项目缘起与核心定位第一次看到claude-mem这个名字我的直觉是这应该是一个给 Claude 系列模型做“记忆层”的项目。事实也确实如此。它要解决的核心问题非常具体——大语言模型在跨会话场景下没有持久记忆。你每次打开一个新对话模型对之前发生过什么一无所知所有上下文都得重新喂一遍。对于偶尔聊两句的用户来说这不算什么但如果你把 Claude 当成日常开发助手、写作搭档或者知识管理工具这种“每次归零”的体验就非常割裂了。claude-mem做的事情本质上是给 Claude 外挂一套可检索、可持久化的记忆系统。它把历史对话、关键事实、用户偏好、项目上下文这些信息存下来在需要的时候按相关性召回再注入到当前会话的提示词里。这样模型就能“记得”你上周让它重构的那个模块叫什么名字也能“记得”你偏好用 TypeScript 而不是 JavaScript。这个项目适合几类人一是把 Claude 深度集成进工作流的开发者二是想给自己的 AI 助手加记忆能力的独立开发者三是对 RAG检索增强生成和 Agent 记忆机制感兴趣的技术爱好者。哪怕你只是想搞清楚“AI 记忆”到底是怎么实现的这个项目也是一个很好的解剖样本。我花了大概两周时间把它的核心逻辑跑通、改了一版、又在实际项目里用了一段时间。下面把我理解到的设计思路、关键实现细节、踩过的坑以及一些可以直接抄的配置完整地梳理一遍。2. 整体架构与设计思路拆解2.1 为什么不做“全量上下文”而选择检索式记忆最朴素的想法是既然模型记不住那我每次把历史对话全部塞进 prompt 不就行了这个方案在小规模下可行但很快会撞到两个墙。第一是上下文窗口的物理限制即使窗口开到 200K token几十轮对话加上代码文件也会撑爆。第二是成本和延迟token 越多推理越慢、越贵而且大量无关历史会稀释当前任务的注意力模型反而更容易跑偏。claude-mem选择的是检索式路线存的时候做压缩和结构化取的时候做相关性排序只把最相关的片段注入上下文。这个思路和 RAG 一脉相承但针对“记忆”这个场景做了专门优化。记忆和普通文档检索的区别在于记忆有时效性、有层级事实 / 偏好 / 事件、有衰减旧信息权重降低这些都需要在存储和召回阶段单独处理。2.2 三层记忆结构的设计考量项目把记忆分成三层这个划分我认为是整个设计里最关键的一步短期记忆Session Memory当前会话内的上下文生命周期就是这一次对话。它保证同一轮对话里模型不会自相矛盾。长期记忆Long-term Memory跨会话持久化的事实和偏好比如“用户的项目用 pnpm 而不是 npm”“用户不喜欢冗长的解释”。这部分存在数据库里长期有效。工作记忆Working Memory针对当前任务临时组装的记忆集合是短期和长期记忆经过检索、排序、裁剪后的结果最终注入到 prompt 里。为什么要分三层而不是一锅炖因为不同层级的记忆写入频率、读取频率、过期策略完全不同。短期记忆每轮都写长期记忆只在检测到“值得记住”的信息时才写工作记忆每次请求都重新组装。混在一起会导致存储膨胀和召回噪声。这个分层思路其实借鉴了认知科学里的人类记忆模型落地到工程上就是三套不同的存储和生命周期管理。2.3 存储选型为什么是向量库加结构化存储的组合纯向量库能解决语义相似度召回但解决不了精确查询。比如你想召回“用户上次提到的那个函数名”这是精确匹配需求向量检索反而不如关键词索引。claude-mem用的是向量库 关系型/文档型存储的混合方案向量库存 embedding 用于语义召回结构化存储存原始文本、时间戳、标签、类型等元数据用于过滤和精确查询。我实测下来这种混合方案在记忆场景下比纯向量方案召回准确率高不少。原因是记忆查询往往带有强过滤条件比如“只召回最近 7 天的偏好类记忆”这种条件用元数据过滤先缩小范围再做向量排序效率和准确率都更好。3. 核心细节解析与实操要点3.1 记忆的写入什么该记什么不该记这是整个系统里最容易被低估的环节。如果什么都往长期记忆里塞很快就会被噪声淹没如果太保守又记不住有用的东西。项目里用了一套基于规则的过滤器加一个可选的模型判断。规则层面主要看几个信号用户是否明确表达了偏好“我喜欢 / 我习惯 / 以后都”、是否陈述了稳定事实“我的项目叫 X”“我用的是 Y 框架”、是否是纠正性反馈“不对应该是……”。这些信号命中后候选记忆会被打上标签。模型判断层面会让 Claude 自己对一段对话做一次“是否值得长期记忆”的二分类。这一步会增加一次额外的模型调用但能显著降低误记率。我的经验是规则过滤先跑一遍把候选量降下来再让模型做最终判断这样成本可控。注意写入时一定要带时间戳和来源会话 ID。后面做记忆衰减和溯源时这两个字段是刚需。我一开始图省事没存来源结果后来想清理某次错误对话产生的记忆时根本定位不到。3.2 记忆的召回相关性排序的几个维度召回不是简单的向量相似度排序。项目里综合考虑了四个维度我把它整理成一张表维度说明权重建议语义相似度当前 query 与记忆 embedding 的余弦相似度0.5时间新鲜度越近的记忆权重越高按指数衰减0.2记忆类型匹配偏好类 query 优先召回偏好类记忆0.2使用频次被召回且被模型实际引用的次数0.1权重不是固定的项目里做成了可配置。我在实际使用中把时间新鲜度的权重调高到了 0.3因为我做的项目迭代快两周前的技术决策可能已经过时了。这个权重需要根据自己的场景调没有万能值。排序之后还有一个去重和裁剪步骤。语义相近的记忆会被合并最终注入的记忆总量控制在预算内比如 2000 token。这一步很关键否则召回十条相似记忆既浪费 token 又干扰模型。3.3 记忆的衰减与清理记忆不是越多越好。项目实现了基于时间的衰减机制每条记忆有一个“强度值”随时间推移和未被引用而下降低于阈值就归档或删除。同时如果某条记忆被频繁召回并确认有用强度会回升。这个机制我一开始觉得多余直到我的记忆库涨到几千条召回质量明显下降才意识到清理的重要性。没有衰减机制的记忆系统最终一定会退化成垃圾场。这是我从这个项目里学到的最有价值的一课。4. 实操过程与核心环节实现4.1 环境准备与依赖安装项目主体是 Python 写的依赖不算复杂。我用的环境是 Python 3.10建议不要低于 3.9因为用到了不少类型注解的新特性。python -m venv venv source venv/bin/activate pip install -r requirements.txt核心依赖包括向量库客户端、embedding 模型接口、以及一个轻量的 Web 框架用于提供记忆服务接口。如果你用的是本地 embedding 模型还需要额外装模型运行时。我一开始用的是在线 embedding 接口后来换成本地模型主要是为了控制成本和数据隐私。配置通过环境变量注入关键几项export MEM_DB_PATH./data/memory.db export MEM_VECTOR_DIM1536 export MEM_RECALL_TOP_K8 export MEM_TOKEN_BUDGET2000 export MEM_DECAY_HALF_LIFE14MEM_DECAY_HALF_LIFE是衰减半衰期单位天。设成 14 意味着两周后记忆强度减半。这个值根据你的信息更新频率来定我做快速迭代项目时设成 7做长期知识管理时设成 30。4.2 记忆写入的完整流程写入流程分四步我按实际代码逻辑走一遍。第一步是对话切分。不是把整轮对话当成一个记忆单元而是切成更细的片段每个片段聚焦一个主题。切分用的是一个基于语义边界的算法简单说就是检测话题转移点。第二步是候选提取。对每个片段跑规则过滤器命中偏好、事实、纠正信号的片段进入候选池。第三步是模型判定。把候选片段批量送给模型让它输出“是否值得记忆”以及“记忆类型”和“摘要”。这里有个技巧批量送比逐条送省很多 token我一般攒够 10 条一起送。第四步是去重写入。写入前先做一次相似度检查如果已有高度相似的记忆就更新它的强度和时间戳而不是新增一条。这一步能有效防止重复记忆堆积。def write_memory(fragment, mem_type, summary): existing vector_store.search(fragment.embedding, top_k1) if existing and existing[0].score 0.92: existing[0].reinforce() return existing[0].id return vector_store.insert( textsummary, embeddingfragment.embedding, metadata{type: mem_type, ts: now(), source: fragment.session_id} )4.3 记忆召回的注入实现召回发生在每次请求模型之前。流程是拿当前用户输入做 embedding走混合检索排序裁剪最后拼成一段结构化的记忆文本注入 system prompt。注入格式我改过好几版最终稳定在这个结构[相关记忆] - (偏好) 用户偏好简洁回答不喜欢冗长解释。 [3天前] - (事实) 用户的项目使用 pnpm 作为包管理器。 [7天前] - (事件) 上次讨论中确定了用 Zustand 做状态管理。 [2天前]每条记忆前面标类型后面标时间。类型标签帮助模型判断这条信息的性质时间标签帮助模型判断时效性。实测下来带类型和时间标签的注入比纯文本注入模型引用准确率高出一截。注意注入的记忆要放在 system prompt 里不要放在 user message 里。放 user message 里模型有时会误以为这是用户当前说的话产生混淆。4.4 参数调优的实测记录我把几个关键参数调优的过程记录一下供参考。MEM_RECALL_TOP_K从 5 调到 8 再到 12最终定在 8。5 的时候经常漏掉关键记忆12 的时候噪声明显增多8 是一个平衡点。但这个值和你的记忆库规模有关库越大top_k 相对要小因为大库里相似记忆更多。MEM_TOKEN_BUDGET从 1000 调到 3000最终定在 2000。1000 的时候记忆被裁得太狠3000 的时候挤占了正常对话空间。2000 大约对应 8 到 10 条记忆比较合理。衰减半衰期我前面说了按场景定。这里补充一点衰减只作用于召回权重不直接删除记忆。删除是另一个独立的归档流程阈值更低。这样设计的好处是一条记忆即使权重降到很低在特定精确查询下仍可能被召回。5. 常见问题与排查技巧实录5.1 记忆召回不准的排查路径这是最常见的问题。我整理了一个排查顺序现象可能原因排查方法该召回的没召回embedding 质量差检查 embedding 模型是否匹配语言召回了一堆无关的相似度阈值太低提高召回阈值加元数据过滤召回内容对但排序乱权重配置不合理调整四维度权重记忆明显过时还在召回衰减未生效检查时间戳字段和衰减配置我遇到过一次召回全乱的情况排查半天发现是 embedding 维度配置和实际模型输出维度不一致导致相似度计算全是噪声。维度不匹配不会报错只会静默产生垃圾结果这个坑很隐蔽。5.2 记忆库膨胀的处理用了一段时间后记忆库涨得很快召回变慢变差。我的处理办法是三层清理第一层是自动衰减归档第二层是定期合并相似记忆第三层是手动审查删除。自动衰减前面说了。定期合并是每周跑一次脚本把相似度高于 0.9 的记忆合并成一条保留最新的时间戳和最高的强度。手动审查是每月一次把低强度记忆导出来人工过一遍删掉明显无用的。提示清理前一定要备份。我有一次合并脚本写错了把不同项目的记忆合并到一起导致召回时项目上下文串了排查了很久。5.3 模型不引用注入记忆的问题有时候记忆明明注入了模型却不用。原因通常有两个一是注入位置不对二是注入格式让模型误以为是背景噪声。解决办法是把记忆放在 system prompt 的靠前位置并且用明确的分隔标记。另外可以在 system prompt 里加一句指令“以下记忆与当前对话相关请在适当时引用。”这句话能显著提升模型的引用率。我实测加了这句之后引用率从大概六成提升到八成以上。5.4 多项目记忆串扰如果你用同一个记忆库服务多个项目很容易出现 A 项目的记忆被召回进 B 项目的对话。解决办法是给记忆打项目标签召回时强制按项目过滤。项目标签可以在写入时从会话元数据里取也可以在配置里手动指定。我现在的做法是每个项目一个独立的记忆库文件物理隔离最省心。虽然多占点磁盘但避免了串扰问题维护起来也简单。6. 扩展方向与个人实践体会claude-mem的基础版本跑通之后我做了几个扩展效果不错分享出来。第一个扩展是记忆的可视化面板。写了个简单的 Web 页面把记忆按类型、时间、强度展示出来支持搜索和手动编辑。这个面板对我理解记忆系统的行为帮助极大很多问题都是通过看面板发现的。第二个扩展是记忆的导入导出。支持把记忆导出成 JSON也支持从 JSON 导入。这样可以在不同机器之间迁移记忆也可以把某个项目的记忆分享给同事。第三个扩展是基于记忆的主动提醒。当检测到当前对话涉及某个已有记忆相关的话题时主动在回复里提示“根据之前的记录……”。这个功能让助手感觉更“懂我”但要注意别过度提醒否则会显得啰嗦。我个人在实际操作中的体会是记忆系统的价值不在于记住多少而在于在对的时候想起对的事。一开始我追求记忆库越大越好后来发现精准的小记忆库比臃肿的大记忆库好用得多。现在我更关注写入的质量和召回的精度而不是数量。最后再分享一个小技巧给记忆加一个“置信度”字段写入时由模型评估这条记忆的可靠程度。召回时把置信度纳入排序权重。这样那些模型不太确定的记忆就不会轻易干扰对话而高置信度的记忆会优先被使用。这个改动不大但对召回质量的提升很明显。