claude-mem记忆层设计:从抽取到检索的工程实践

发布时间:2026/10/25 3:08:13
claude-mem记忆层设计:从抽取到检索的工程实践

1. 从"claude-mem"这个名字说起:它到底想解决什么

第一次看到claude-mem这个命名,我的直觉是:这是一个围绕 Claude 生态做"记忆层"的项目。名字拆开看很直白——claude指向模型侧,mem是 memory 的缩写。合在一起,它要处理的核心命题就一句话:让模型在多轮、跨会话的交互中,记住该记住的东西,忘掉该忘掉的东西。

这件事听起来简单,做起来极其麻烦。我接触过不少做对话系统的团队,大家一开始都觉得"记忆"不就是把历史消息拼进上下文吗?真上手才发现,上下文窗口是有限的,token 是要花钱的,历史越长噪声越大,模型反而越容易跑偏。更别提跨会话场景——今天用户说了一句关键偏好,明天开新会话,模型完全不记得,体验直接断裂。

claude-mem这类项目瞄准的就是这个断层。它本质上是一套记忆管理中间层:在模型和用户之间架一层,负责把对话中产生的信息抽取、压缩、存储、检索,再在合适的时机把最相关的片段喂回给模型。它解决的不是"模型聪不聪明"的问题,而是"模型记不记得住"的问题。

适合谁来读这篇内容?三类人。第一类是想给自己的 AI 应用加记忆能力的开发者,你需要知道记忆层怎么设计、坑在哪;第二类是在做 Agent、客服机器人、个人助理这类长周期交互产品的同学,记忆直接决定产品能不能用;第三类是对 RAG、上下文工程感兴趣的技术爱好者,记忆管理其实是 RAG 思路在"对话历史"这个特殊语料上的变体。

我下面会按"为什么需要独立记忆层 → 记忆怎么存 → 怎么取 → 怎么防止它变味 → 实测中的坑"这条线展开。所有涉及具体实现的地方,我会说明这是基于常见工程实践的合理方案,因为原始项目正文是空的,我补的是"一个合格从业者在这个场景下最可能怎么做"。

2. 为什么不能只靠"把历史全塞进上下文"

2.1 上下文窗口不是免费的午餐

很多人对上下文窗口有个误解,觉得窗口越大越好,那就把所有历史都塞进去呗。我实测过,这条路走不通,原因有三层。

第一层是成本。上下文是按 token 计费的,而且是双向的——你塞进去的每一轮历史,在后续每一次请求里都要重新计费。一个跑了 50 轮的会话,如果每轮都带全量历史,token 消耗是平方级增长的。我见过一个客服场景,单次会话成本从几分钱涨到几块钱,就是因为历史没做裁剪。

第二层是注意力稀释。模型对上下文的注意力不是均匀分布的,中间部分容易被忽略,这就是常说的"lost in the middle"现象。你塞 100 轮历史进去,真正关键的那句"我对花生过敏"可能被淹没在中间,模型该犯错还是犯错。信息多不等于信息有效。

第三层是噪声污染。历史里大量是寒暄、确认、重复表达,这些对当前任务毫无价值,却占着宝贵的窗口位置。把噪声和信号一起喂进去,等于让模型做一道"从垃圾里挑金子"的题,正确率自然下降。

2.2 记忆层要做的三件事

所以claude-mem这类项目的价值,就体现在它替模型做了三件模型自己不方便做的事:

  • 抽取:从原始对话流里识别出"值得记"的信息。不是每句话都值得记,"今天天气不错"不值得,"我下周要去上海出差"值得。
  • 压缩:把抽取出来的信息用更紧凑的形式表达。原始对话可能是五句话,压缩后是一条结构化记录。
  • 检索:在需要的时候,把最相关的记忆片段找出来,而不是把整个记忆库倒给模型。

这三件事对应三个技术模块:信息抽取、记忆存储、相关性检索。下面逐个拆。

2.3 一个容易踩的认知坑

我特别想提醒一点:记忆不等于历史。很多团队做记忆层,做着做着就退化成了"历史消息的数据库",只是把消息存起来再取出来,这跟直接拼上下文没本质区别,只是换了个存储位置。

真正的记忆层,输出给模型的东西应该和原始历史长得不一样。它应该是提炼过的、结构化的、带时间戳和置信度的。如果你做出来的记忆层,取出来的内容和原始对话一模一样,那这个层就是白做的。判断标准很简单:记忆层取出的内容,token 数应该显著少于原始历史,但关键信息保留率要高。

3. 记忆怎么存:从"流水账"到"结构化档案"

3.1 存储粒度的选择

记忆存什么粒度,是个关键决策。我见过三种主流做法,各有取舍。

按轮次存:每一轮对话存一条记录。优点是实现简单,缺点是粒度太细,检索时容易召回一堆无关的轮次,而且单轮信息往往不完整。

按会话存:整个会话压缩成一条摘要。优点是紧凑,缺点是粒度太粗,一个长会话里可能包含多个主题,压成一条会丢失细节。

按"记忆单元"存:这是我认为最合理的做法。把对话中识别出的独立事实、偏好、事件,各自存成一条记忆单元。比如"用户偏好简洁回复"是一条,"用户下周三有会议"是另一条。粒度适中,检索精准。

claude-mem这类项目,我推测它采用的是第三种,因为只有这种粒度才能支撑起"精准检索"这个核心价值。下面我按记忆单元的思路展开。

3.2 一条记忆单元应该长什么样

我实际设计过记忆单元的结构,分享一个我认为比较实用的字段设计:

字段类型说明
idstring唯一标识,建议用内容哈希,天然去重
contentstring记忆正文,压缩后的自然语言
typeenum事实/偏好/事件/指令,分类影响检索权重
embeddingvector向量表示,用于语义检索
keywordslist关键词,用于混合检索
created_attimestamp创建时间,用于时效性衰减
last_accessedtimestamp最近访问时间,用于热度排序
confidencefloat置信度,抽取不确定时降低权重
sourcestring来源会话标识,便于溯源

这个结构里,type和confidence是两个容易被忽略但很重要的字段。type让检索时可以按类型加权——比如当前是任务执行场景,事件和指令类记忆权重调高;当前是闲聊,偏好类权重调高。confidence则处理"模型不确定这句话是不是重要信息"的情况,抽取时给个低置信度,检索时自然降权,避免误记污染。

3.3 抽取环节:怎么判断"值得记"

抽取是记忆层最考验功力的地方。我的经验是,不要指望一次 LLM 调用就能完美抽取,要用规则 + 模型的组合。

规则层负责快速过滤明显不值得记的内容:纯寒暄、纯确认("好的""收到")、纯情绪表达。这些用关键词和长度规则就能筛掉一大半,省下模型调用成本。

模型层负责从剩下的内容里抽取结构化记忆。这里 prompt 设计很关键。我常用的抽取 prompt 结构是这样的:

从以下对话片段中抽取值得长期记忆的信息。 只抽取以下类型: 1. 用户的稳定偏好(如"喜欢简洁回复") 2. 用户的事实信息(如"住在杭州") 3. 未来的事件安排(如"下周三开会") 4. 明确的指令要求(如"以后都用中文回复") 不要抽取:寒暄、确认、情绪表达、临时性内容。 输出 JSON 数组,每条包含 content、type、confidence。 如果没有任何值得记的内容,输出空数组。

这个 prompt 的关键在于给了明确的"不要抽取"清单。只告诉模型"抽什么",它容易过度抽取;加上"不抽什么",准确率明显提升。这是我踩过坑之后总结的——早期版本只写正向指令,结果模型把"用户说了句谢谢"都记下来了。

3.4 压缩环节:怎么让记忆更紧凑

抽取出来的记忆单元,往往还是偏长。比如原始对话是"我最近在做一个数据分析的项目,用的是 Python,主要处理一些销售数据,数据量大概几百万行",抽取出来可能还是这么长。压缩的目标是变成"用户在做 Python 数据分析项目,处理百万级销售数据"。

压缩我一般用两种手段结合。一是模型改写,让模型用最简洁的语言重述,明确要求"不超过 30 字"。二是去重合并,如果新记忆和已有记忆语义高度相似,就合并而不是新增。合并时保留信息更全的那条,更新时间戳。

去重这一步特别重要。我见过没做去重的记忆库,用户提了三次"我喜欢喝咖啡",库里就有三条几乎一样的记录,检索时全被召回,白白占窗口。去重的阈值我一般设在余弦相似度 0.9 左右,太高会漏合并,太低会误合并。

4. 记忆怎么取:检索策略决定体验上限

4.1 纯向量检索的局限

很多人一提检索就想到向量相似度,觉得 embedding 一算,top-k 一取就完事。我实测下来,纯向量检索在记忆场景有几个明显问题。

问题一:时效性无法体现。用户三个月前说"我在学吉他",和昨天说"我在学吉他",向量相似度几乎一样,但后者显然更该被召回。纯向量检索对时间不敏感。

问题二:类型无法区分。当前是任务场景,应该优先召回指令类记忆,但向量检索不管类型,可能召回一堆偏好类记忆。

问题三:精确匹配失效。用户问"我上次说的那个项目叫什么",这需要精确匹配项目名,向量检索对专有名词的匹配反而不如关键词检索。

4.2 混合检索:向量 + 关键词 + 规则

我的方案是混合检索,三路并行再融合排序。

第一路是向量检索,负责语义相关性。用记忆单元的 embedding 和当前 query 的 embedding 算相似度,取 top-N。

第二路是关键词检索,负责精确匹配。用 BM25 或简单的关键词倒排,匹配专有名词、数字、日期这类向量不擅长的内容。

第三路是规则加权,负责业务逻辑。根据记忆的 type、created_at、last_accessed、confidence 做加权。比如事件类记忆按时间衰减,越近权重越高;指令类记忆不衰减,长期有效。

三路结果用加权融合(比如 RRF 倒数排名融合)合并,再取最终 top-k。这套组合拳下来,召回质量比纯向量高一个档次。我做过对比测试,在记忆检索场景,混合检索的命中率比纯向量检索高出约 20 到 30 个百分点。

4.3 检索结果怎么喂回模型

检索出记忆后,怎么组织进 prompt 也有讲究。我一般用这样的结构:

以下是关于当前用户的历史记忆,供你参考: [偏好] 用户喜欢简洁回复 [事实] 用户在做 Python 数据分析项目 [事件] 用户下周三有会议(记录于 3 天前) 请结合以上记忆回答用户问题。如果记忆与当前问题无关,忽略即可。

这里有两个细节。一是给每条记忆打上类型标签,让模型知道这条记忆的性质,便于它判断如何使用。二是明确告诉模型"无关可忽略",否则模型容易强行把不相关的记忆往回答里塞,反而答非所问。

还有一个我踩过的坑:记忆不要放在 system prompt 里。早期我把记忆塞进 system prompt,结果发现模型对 system 里的内容有一种"必须遵守"的倾向,导致它把过时的记忆也当铁律执行。后来改成放在 user 消息前部,作为"参考信息"而非"指令",模型的使用就自然多了。

4.4 检索数量:不是越多越好

top-k 取多少?我的经验是 3 到 5 条。取太少可能漏关键信息,取太多会引入噪声。而且要注意,记忆检索的 k 值和普通 RAG 不一样——RAG 的文档块信息密度高,多取几条问题不大;记忆单元往往很短,取太多条会显得零碎,反而干扰模型。

如果确实需要更多记忆,我建议先做二次压缩:把召回的 10 条记忆再让模型压成一段连贯的摘要,再喂给模型。这样既保留了信息,又控制了长度。

5. 记忆的"保鲜":衰减、更新与遗忘

5.1 为什么记忆需要遗忘

一个不会遗忘的记忆系统,最终会变成垃圾场。用户三个月前随口说的一句话,和昨天明确表达的偏好,如果权重一样,检索时就会互相干扰。遗忘不是缺陷,是特性。

我设计的遗忘机制分两种:时间衰减和主动清理。

时间衰减针对事件类记忆。每条事件记忆有个"有效期"概念,过了有效期权重快速下降。比如"下周三开会",过了下周三这条记忆基本就没用了,权重降到接近零。实现上可以用指数衰减函数,weight = base * exp(-λ * days_since_created),λ 根据记忆类型调整,事件类 λ 大(衰减快),偏好类 λ 接近零(几乎不衰减)。

主动清理针对低质量记忆。定期扫描记忆库,把 confidence 低、长期未被访问、且内容与其他记忆重复的记录删掉。清理频率我一般设成每周一次,太频繁浪费资源,太稀疏垃圾积累太多。

5.2 记忆冲突怎么处理

记忆冲突是个绕不开的问题。用户上周说"我喜欢喝咖啡",这周说"我戒咖啡了",两条记忆直接矛盾。怎么处理?

我的做法是新记忆覆盖旧记忆,但保留旧记忆的痕迹。具体来说,检测到冲突时,把旧记忆标记为"已失效",新记忆正常入库。检索时失效记忆默认不召回,但如果用户明确问"我以前是不是说过喜欢咖啡",可以临时召回失效记忆来回答。

冲突检测用向量相似度加类型判断。两条同类型记忆,语义相似度高但内容矛盾(可以用模型判断是否矛盾),就触发冲突处理。这一步会增加一些模型调用成本,但值得,因为矛盾记忆喂给模型,模型会精神分裂。

5.3 记忆的更新而非新增

很多场景下,新信息不是新增记忆,而是更新已有记忆。比如用户之前说"我在学吉他",后来说"我吉他学完了,开始学钢琴"。这不是两条独立记忆,而是一条记忆的状态更新。

我的处理方式是:抽取新记忆时,先检索是否有相关旧记忆,如果有,让模型判断是"新增"还是"更新"。更新时保留原记忆 id,更新 content 和 timestamp。这样记忆库不会因为同一主题的多次提及而膨胀。

这个判断逻辑我写在抽取 prompt 里,让模型在输出时带上action: create/update和target_id(如果是 update)。实测下来,模型对这个判断的准确率还不错,偶尔出错也能靠后续的去重兜底。

6. 实测中的坑:那些文档不会告诉你的细节

6.1 embedding 模型的选择陷阱

记忆检索的 embedding 模型,不要盲目用最大的那个。我踩过的坑是:用了一个维度很高、效果很好的通用 embedding 模型,结果发现它对短文本的区分度反而不如小模型。记忆单元往往就十几个字,大模型容易把语义相近的短文本压到几乎相同的向量上,导致检索区分不开。

我的建议是,记忆场景优先选针对短文本优化的 embedding 模型,或者至少做一次实测对比。测试方法很简单:准备 50 条记忆和 20 个 query,看不同模型的 top-3 命中率。别嫌麻烦,这一步省不得。

6.2 抽取的"过度记忆"倾向

模型有过度抽取的倾向,尤其是能力强的模型,它总觉得"这句话可能有用"。我早期版本没做限制,结果记忆库里塞满了"用户说了你好""用户表示同意"这种垃圾。

解决办法除了前面说的"不要抽取清单",还有一个技巧:给抽取结果加长度下限。比如 content 少于 8 个字的记忆直接丢弃。这条简单规则能过滤掉大量无意义记忆。另外,定期人工抽查记忆库,看看抽出来的都是什么,根据实际情况调整 prompt,这个习惯我保持了很长时间,收益很大。

6.3 检索的"最近偏好"偏差

有个隐蔽的坑:如果检索时过度加权"最近访问"或"最近创建",会导致模型只记得最近的事,早期的重要记忆永远召不回。我遇到过用户抱怨"它忘了我最开始说的核心需求",一查发现是最近性权重给太高了。

修正方法是分类型设置最近性权重。偏好类和指令类记忆,最近性权重应该低,因为它们长期有效;事件类记忆,最近性权重可以高。别用一个统一的衰减函数套所有类型。

6.4 并发写入的记忆重复

如果应用是多用户并发的,记忆写入要做幂等。我遇到过同一句话因为重试机制被写入两次的情况,导致记忆库出现重复。解决办法是用 content 哈希做唯一键,写入前先查重,存在就跳过或更新。这个细节在单机测试时发现不了,一上并发就暴露。

6.5 记忆的隐私边界

最后说个非技术但极其重要的点:记忆层是隐私敏感区。用户的偏好、事实、事件,很多是私人信息。设计记忆层时,删除能力必须是一等公民——用户要能查看自己的记忆、删除单条记忆、清空全部记忆。而且删除要彻底,不能只是标记失效,向量库、关键词索引、缓存都要同步清理。这一点在项目设计初期就要考虑,后期补很痛苦。

7. 如果要自己动手:一个最小可行实现路径

如果你看完想自己搭一个类似claude-mem的记忆层,我给一条最小可行路径,避免一上来就过度设计。

第一步,先做存储和检索,不做抽取。手动往记忆库里塞几条记忆,把向量检索跑通,验证检索质量。这一步能帮你快速确认 embedding 模型选得对不对。

第二步,加抽取,但先用规则。用关键词规则抽取明显值得记的内容(比如包含"我喜欢""我要""记住"的句子),先跑起来,观察效果。规则抽取准确率不高但可控,适合验证流程。

第三步,把规则换成模型抽取。用前面给的 prompt 结构,接上模型做抽取,和规则抽取的结果对比,逐步替换。

第四步,加混合检索和衰减。向量检索跑顺了,再加关键词检索和规则加权,加时间衰减。每加一个模块都做 A/B 对比,确认有正向收益再加下一个。

第五步,加遗忘和冲突处理。这是最后一步,也是最复杂的一步。前面都稳了再动这里。

这条路径的核心思想是:每一步都可独立验证,不要一次性把所有模块堆上去。我见过太多项目一上来就设计一个庞大的记忆架构,结果每个模块都没调好,整体效果一塌糊涂。记忆层是个精细活,慢就是快。

关于技术选型,向量库用轻量的就够,记忆量级通常不大,几万到几十万条,很多嵌入式向量库完全能扛。别一上来就上分布式向量数据库,运维成本远大于收益。存储用关系库加向量扩展也行,简单直接。

我在实际做记忆层的过程中最大的体会是:记忆系统的质量,八成取决于抽取和检索这两个环节的调优,而不是架构有多花哨。把抽取 prompt 打磨好,把检索的加权逻辑调对,比换任何 fancy 的框架都管用。这个领域没有银弹,就是一遍遍实测、对比、调整。