1. 项目缘起与核心定位
第一次看到 claude-mem 这个名字,我下意识把它拆成了两半:claude 和 mem。前者指向对话式 AI 助手的使用场景,后者是 memory 的缩写,也就是记忆。合在一起,这个项目的意图就很清楚了——给 AI 助手装上一套可持久化的记忆系统,让它不再每次对话都从零开始。
做过 AI 应用开发的人都知道一个痛点:大模型的上下文窗口是有限的,而且每次新开一个会话,之前聊过的内容就全部丢失了。你昨天跟它讨论过的项目架构、上周定下来的命名规范、上个月踩过的某个坑,它统统不记得。每次都要重新贴一遍背景资料,效率极低。claude-mem 要解决的就是这个问题——把对话中产生的关键信息抽取出来,存到一个可以随时检索的地方,下次对话时按需注入,让 AI 助手拥有跨会话的长期记忆能力。
这个项目适合谁?三类人最值得关注。第一类是重度使用 AI 助手做开发的工程师,每天要跟 AI 来回几十轮对话,记忆断层带来的重复劳动最让人抓狂。第二类是在做 AI 应用产品的开发者,需要给自己的产品加上记忆模块,claude-mem 的思路可以直接借鉴。第三类是对 RAG、向量检索、上下文管理感兴趣的技术爱好者,这个项目把记忆的写入、存储、检索、注入整条链路都串起来了,是一个很好的学习样本。
我花了大概两周时间把这个项目的核心逻辑跑通并做了几轮改造,下面把整个思路、实现细节和踩过的坑完整梳理一遍。文章会比较长,但每一段都是实操中验证过的内容,不是纸上谈兵。
2. 整体架构设计与选型考量
2.1 记忆系统的三层结构
claude-mem 的架构可以拆成三层:采集层、存储层、检索注入层。这个分层不是拍脑袋定的,而是照着记忆这件事本身的逻辑来的。
采集层负责从对话流中识别哪些内容值得记住。不是每句话都有记忆价值,"好的""明白了"这种废话存进去只会污染检索结果。采集层需要做筛选和结构化,把一段对话压缩成一条条独立的记忆条目。
存储层负责把这些记忆条目持久化。这里的选择很多,可以用纯文本文件、SQLite、向量数据库,甚至直接调云服务。claude-mem 默认走的是本地文件加向量索引的方案,兼顾了隐私和检索效率。
检索注入层是每次新对话开始时触发的。它根据当前对话的上下文,去存储层里捞出最相关的若干条记忆,拼接到系统提示词或者首轮消息里,让 AI 助手"想起来"之前的事。
这三层之间是松耦合的,你可以只换存储层而不动采集逻辑,也可以只改检索策略而不碰存储格式。这种设计的好处是迭代成本低,我后来把默认的向量检索换成了混合检索,只改了一个模块,其他部分完全没动。
2.2 为什么不用简单的全文拼接
有人可能会想,搞这么复杂干嘛,直接把历史对话全部拼到提示词里不就行了?我一开始也是这么想的,实测下来问题很大。
首先是 token 成本。历史对话越积越多,每次请求都带上全部历史,token 消耗线性增长,钱包扛不住。其次是注意力稀释。上下文里塞了太多无关信息,模型对真正重要的内容反而关注不够,回答质量会下降。最后是窗口限制,再大的上下文窗口也有上限,历史对话迟早会撑爆。
所以必须做检索,只捞相关的。这就引出了下一个问题:怎么判断哪些记忆是相关的?这就是检索策略要解决的核心问题。
2.3 存储格式的取舍
记忆条目用什么格式存,这个决定影响后续所有环节。我试过三种方案,对比如下:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 纯文本 Markdown | 人类可读,方便手动编辑 | 检索只能靠关键词匹配,召回率低 | 记忆量少、以人工查阅为主 |
| 结构化 JSON | 字段清晰,方便程序处理 | 需要额外的索引层才能检索 | 记忆量大、需要程序化检索 |
| 向量数据库 | 语义检索,召回率高 | 需要嵌入模型,有额外依赖 | 记忆量大、追求检索质量 |
claude-mem 最终选的是 JSON 存原始记忆加向量索引存嵌入向量的组合方案。原始记忆用 JSON 存,保证可读可编辑;向量单独存一份,用于语义检索。这样既保留了人工干预的空间,又拿到了语义检索的能力。
提示:如果你只是个人使用,记忆量在几百条以内,其实纯 Markdown 加关键词检索就够了,不必上向量数据库。向量检索的收益要到记忆量上千条以后才明显。
3. 核心模块拆解与实操要点
3.1 记忆采集:怎么判断什么值得记
采集层是整个系统的入口,它的质量直接决定了后面所有环节的上限。垃圾进垃圾出,如果采集层把废话都存进去了,检索层再厉害也捞不出有用的东西。
claude-mem 的采集策略是基于规则加模型判断的混合方案。规则部分负责快速过滤,比如过滤掉长度小于一定阈值的消息、过滤掉纯表情或纯标点的消息。模型判断部分负责识别语义上的记忆价值,比如"我们决定用 PostgreSQL 而不是 MySQL"这种决策性内容,就比"今天天气不错"更值得记。
具体实现上,采集层会在每轮对话结束后触发一次判断。判断的提示词大概长这样:
请判断以下对话片段是否包含值得长期记忆的信息。 值得记忆的信息包括:技术决策、项目约定、个人偏好、重要事实、待办事项。 不值得记忆的信息包括:寒暄、确认性回复、临时性讨论、重复内容。 如果值得记忆,请提取成一条简洁的陈述句,并标注类别。这个提示词我改了好几版。最初版本太宽泛,模型把什么都往里塞。后来加了"不值得记忆"的负面示例,准确率明显提升。再后来加了类别标注,检索时可以按类别过滤,效果更好。
实操心得:采集阈值不要设太低。我一开始把阈值设成 20 个字符,结果"好的收到"这种也被存进去了。后来调到 50 个字符,并且加了语义判断,噪音少了很多。宁可漏记,不要错记,因为错记的条目会持续污染检索结果。
3.2 记忆存储:文件组织与索引维护
存储层的设计要考虑两个问题:怎么组织文件,怎么维护索引。
文件组织上,claude-mem 按日期分目录,每天一个 JSON 文件,文件名就是日期。这样做的好处是写入简单,追加就行,不需要读取整个记忆库。缺点是跨天检索时要遍历多个文件,但这个开销可以接受。
每条记忆条目的结构大概是这样:
{ "id": "mem_20240115_001", "timestamp": "2024-01-15T10:30:00Z", "category": "decision", "content": "项目数据库选用 PostgreSQL,理由是支持 JSON 字段和全文检索", "source": "conversation_20240115", "tags": ["database", "architecture"] }索引维护是存储层的难点。向量索引不能每次写入都全量重建,那样太慢。claude-mem 用的是增量索引方案:新记忆写入时,只计算这条记忆的嵌入向量,追加到索引文件末尾。检索时加载整个索引,做一次相似度计算。记忆量在几千条以内时,这个方案的检索延迟在可接受范围内。
注意:增量索引有个坑,删除记忆时索引不会自动更新。如果你删了某条记忆,记得同步删除对应的向量,否则检索时可能捞到已经不存在的记忆 ID,导致注入失败。我在这上面踩过一次,排查了半天才发现是索引和原始数据不一致。
3.3 检索注入:怎么捞得准又不撑爆上下文
检索注入层是每次新对话开始时跑一次的。它的任务是根据当前对话的初始内容,从记忆库里捞出最相关的 N 条记忆,拼接到提示词里。
检索策略上,claude-mem 默认用的是向量相似度检索。把当前对话的第一条消息做嵌入,然后跟记忆库里所有向量算余弦相似度,取 top-K。K 的取值很关键,太小了可能漏掉重要记忆,太大了会撑爆上下文。默认值是 5,我实测下来 5 到 8 之间比较合适。
但纯向量检索有个问题:它对语义相似但用词不同的情况处理得好,对精确匹配反而弱。比如你记得"项目用的是 PostgreSQL",但当前对话里说的是"那个关系型数据库",向量检索能捞到。但如果当前对话里明确提到了"PostgreSQL",而记忆库里有一条标题就是"PostgreSQL 配置",纯向量检索可能因为其他语义相近的记忆排在前面而漏掉这条。
所以后来我改成了混合检索:向量检索取 top-20,关键词检索取 top-20,两路结果合并去重后按加权分数排序,取最终 top-5。加权分数是向量相似度乘以 0.7 加上关键词匹配度乘以 0.3。这个权重是我试了几组之后定的,0.7 比 0.3 的效果最好,但不同场景可能需要调整。
注入的格式也有讲究。直接把记忆条目原样拼进去,模型有时候会分不清哪些是记忆哪些是当前对话。claude-mem 的做法是加一个明确的分隔标记:
以下是你之前记住的信息,供参考: [记忆开始] 1. 项目数据库选用 PostgreSQL... 2. 用户偏好用 TypeScript 而不是 JavaScript... [记忆结束] 以上是历史记忆,请结合当前对话回答。这个格式我用了很久,模型对记忆的引用准确率明显比不加标记高。
4. 完整实操流程与关键环节实现
4.1 环境准备与依赖安装
claude-mem 本身是个轻量项目,核心依赖不多。我用的环境是 Python 3.10,主要依赖三个库:用于调用嵌入模型的 SDK、用于向量计算的 numpy、用于文件存储的标准库 json 和 pathlib。
安装命令很简单:
pip install numpy openai python-dotenv嵌入模型我选的是 text-embedding-3-small,理由是便宜且效果够用。如果你对成本敏感,也可以用本地的 sentence-transformers 模型,但需要额外装 torch,环境会重不少。我试过本地模型,检索质量跟 API 模型差距不大,但推理速度慢很多,记忆量大了之后每次检索要等好几秒,体验不好。所以最终还是用 API 模型。
提示:嵌入模型的维度要跟索引文件里存的维度一致。如果你中途换了模型,记得重建整个索引,否则相似度计算会出错。我换模型时忘了这茬,检索结果全是乱的,排查了好久。
4.2 记忆写入的完整流程
写入流程从对话结束触发。假设你刚跟 AI 助手聊完一轮,采集层会拿到这轮对话的完整文本,然后走以下步骤:
第一步,规则过滤。去掉长度不足 50 字符的消息,去掉纯确认性回复。这一步很快,纯字符串操作。
第二步,模型判断。把过滤后的对话片段送给模型,让它判断是否有记忆价值,并提取成结构化条目。这一步是异步的,不阻塞主流程。
第三步,去重检查。新提取的记忆条目要跟最近 100 条记忆做相似度比对,如果相似度超过 0.95,认为是重复,跳过不存。这一步是为了防止同一件事被反复记住。
第四步,写入存储。把新条目追加到当天的 JSON 文件,同时计算嵌入向量追加到索引文件。
第五步,更新元数据。记录最后写入时间、记忆总数等统计信息,方便后续监控。
整个流程走下来,单条记忆的写入延迟大概在 1 到 2 秒,主要花在模型判断和嵌入计算上。如果你追求速度,可以把模型判断和嵌入计算并行化,能省一半时间。
4.3 检索注入的完整流程
检索注入在新对话开始时触发。流程如下:
第一步,取当前对话的第一条消息作为查询文本。如果第一条消息太短(比如只有"你好"),就取前三条消息拼接。
第二步,计算查询文本的嵌入向量。
第三步,加载索引文件,计算查询向量跟所有记忆向量的余弦相似度,取 top-20。
第四步,对查询文本做关键词提取,用关键词去记忆库里做匹配,取 top-20。
第五步,两路结果合并去重,按加权分数排序,取 top-5。
第六步,把 top-5 记忆按注入格式拼接,插入到系统提示词或首轮消息里。
这个流程里,第三步和第四步是耗时的。索引文件加载可以缓存,不用每次重新读。余弦相似度计算用 numpy 的矩阵运算,几千条记忆的检索延迟在 100 毫秒以内,可以接受。
4.4 参数调优的实操记录
参数调优这块我花的时间最多,因为默认参数不一定适合你的使用场景。下面是我调参的过程记录,供参考。
top-K 的取值:默认 5。我试过 3、5、8、10。3 的时候经常漏掉重要记忆,10 的时候上下文里塞了太多无关内容,模型回答质量反而下降。5 到 8 之间比较平衡,我最终定在 6。
相似度阈值:低于这个阈值的记忆不注入。默认 0.7。我试过 0.6 和 0.8。0.6 的时候噪音太多,0.8 的时候经常一条都捞不到。0.7 比较合适,但如果你记忆库质量高,可以调到 0.75。
去重相似度阈值:默认 0.95。这个值我基本没动过,0.95 能过滤掉大部分重复,又不会误杀相似但不同的记忆。
混合检索权重:向量 0.7,关键词 0.3。我试过 0.5 比 0.5,关键词的噪音太大。0.8 比 0.2 又太偏向向量,精确匹配的场景吃亏。0.7 比 0.3 是我试下来最平衡的。
调参这件事没有标准答案,跟你的记忆内容类型强相关。如果你的记忆以技术决策为主,关键词匹配的权重可以高一点。如果以个人偏好为主,向量检索的权重可以高一点。建议先跑一段时间,收集一些检索不准的案例,再针对性调整。
5. 常见问题与排查技巧实录
5.1 检索不准的排查思路
检索不准是最常见的问题,表现是明明记得存过某条信息,但新对话时就是捞不出来。排查思路按以下顺序走:
先确认记忆是否真的存进去了。去当天的 JSON 文件里搜一下关键词,看有没有对应的条目。如果没有,问题出在采集层,可能是被规则过滤掉了,或者模型判断为不值得记忆。
如果记忆存进去了,再确认索引里有没有对应的向量。打开索引文件,看条目数量跟记忆数量是否一致。如果不一致,说明索引写入出了问题,可能是嵌入计算失败但没报错。
如果索引也正常,那就是检索策略的问题。手动算一下查询文本跟目标记忆的相似度,看是否低于阈值。如果低于阈值,说明语义差距太大,向量检索捞不到,需要靠关键词检索兜底。如果关键词也没匹配上,说明查询文本里没有出现记忆里的关键词,这种情况只能靠提高 top-K 或者降低阈值来缓解。
提示:排查时建议开一个调试模式,把每次检索的候选列表和分数都打出来。这样一眼就能看出是候选里根本没有目标记忆,还是目标记忆在候选里但分数太低被挤掉了。这两种情况的解法完全不同。
5.2 记忆冲突的处理
记忆冲突是指新记忆跟旧记忆矛盾。比如旧记忆说"项目用 MySQL",新记忆说"项目改用 PostgreSQL 了"。如果两条都注入,模型会困惑。
claude-mem 的处理策略是:新记忆写入时,检查是否有同类别且语义相近的旧记忆。如果有,把旧记忆标记为"已废弃",检索时默认不返回已废弃的记忆。但旧记忆不删除,保留在存储里,方便追溯。
这个策略的关键是"语义相近"的判断。我用的是类别相同加相似度超过 0.85 作为条件。实测下来,这个条件能抓住大部分冲突场景,但偶尔会误判。比如"项目用 PostgreSQL"和"项目用 PostgreSQL 的 JSON 字段"会被误判为冲突,其实后者是前者的补充。这种情况需要人工干预,把误判的旧记忆恢复。
5.3 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 记忆存不进去 | 规则过滤太严 | 检查消息长度和内容 | 放宽过滤阈值 |
| 记忆存不进去 | 模型判断为无价值 | 查看模型返回结果 | 调整判断提示词 |
| 检索捞不到 | 索引未更新 | 对比索引和记忆数量 | 重建索引 |
| 检索捞不到 | 相似度低于阈值 | 手动计算相似度 | 降低阈值或提高 top-K |
| 检索结果噪音多 | 阈值太低 | 查看候选分数分布 | 提高阈值 |
| 检索结果噪音多 | 记忆库质量差 | 抽查记忆条目 | 清理低质量记忆 |
| 注入后模型不引用 | 注入格式不清晰 | 检查提示词格式 | 加明确分隔标记 |
| 写入延迟高 | 模型调用慢 | 计时各步骤耗时 | 并行化或换本地模型 |
5.4 几个容易忽略的坑
第一个坑是时区问题。记忆条目的 timestamp 如果没统一时区,跨天检索时会漏掉或重复。我一开始用本地时间,后来发现跨时区协作时全乱了,改成 UTC 才解决。
第二个坑是并发写入。如果你同时开多个对话窗口,采集层可能同时触发写入,导致 JSON 文件损坏。解决方案是加文件锁,或者用一个队列串行化写入。我用的队列方案,简单可靠。
第三个坑是嵌入模型的速率限制。记忆量大时,批量写入可能触发 API 的速率限制,导致部分记忆写入失败。解决方案是加退避重试,失败后等几秒再试。我设的是指数退避,初始 1 秒,最多重试 3 次。
第四个坑是记忆库的冷启动。刚开始用的时候记忆库是空的,检索什么都捞不到,体验很差。解决方案是前几次对话手动喂一些背景信息,让记忆库先积累起来。或者导入一些历史文档作为初始记忆。
6. 扩展方向与个人实践体会
claude-mem 的基础版本跑通之后,我做了几个扩展,效果不错,分享出来供参考。
第一个扩展是记忆的自动摘要。记忆条目积累多了之后,同类别的记忆可以定期合并成一条摘要。比如十条关于数据库配置的记忆,可以合并成一条"数据库配置要点"。这样检索时一条摘要就能覆盖多个细节,减少注入的条目数。
第二个扩展是记忆的重要性评分。每条记忆写入时打一个重要性分数,检索时按分数加权。重要性高的记忆优先注入。评分可以基于类别(决策类高于偏好类)、引用次数(被检索到多次的记忆加分)、时间衰减(越新的记忆分数越高)。
第三个扩展是记忆的可视化。我写了个简单的网页,把记忆库按类别和时间线展示出来,方便人工审查和清理。这个工具对维护记忆库质量很有帮助,建议你也搞一个。
我个人在实际操作中的体会是,记忆系统的核心难点不在技术实现,而在质量控制。技术方案再先进,如果记忆库里全是噪音,检索效果也好不了。所以采集层的过滤和去重要舍得花时间调,宁可少记,不要错记。另外,记忆系统不是一劳永逸的,需要定期维护和清理,就像整理房间一样,不定期收拾就会越来越乱。
最后再分享一个小技巧:如果你用的是支持系统提示词的 AI 助手,把记忆注入到系统提示词里比注入到首轮消息里效果更好。系统提示词的权重更高,模型对它的关注度更强,引用记忆的准确率会提升。我实测下来,同样的记忆内容,放系统提示词里的引用率比放首轮消息里高大概两成。