做了一段时间的 AI 应用之后,我最大的一个感受是:Claude 这类模型确实聪明,但“记忆力”差得离谱。新开一个会话,它就像完全失忆一样,不记得你上周定的方案,不记得用户反复强调的偏好,甚至同一个问题能问三遍。后来我开始折腾 claude-mem,给 Claude 补上一层长期记忆系统,总算把这副“金鱼脑”治得差不多了。这篇文章我会把从设计到落地的完整过程写下来,包括架构取舍、代码实现、踩坑记录和评测方法。不管你是做 AI 助手、知识库问答,还是纯个人效率工具,都可以直接照着抄。
1. 为什么需要 claude-mem:先理解 Claude 的“记忆缺口”
1.1 无状态 API 与对话上下文的本质限制
很多人第一次接触 Claude API 时会忽略一个关键前提:它本质上是无状态的。每次请求,你都要把完整的 messages 数组发送过去,模型只基于当前输入和上下文窗口做推理,服务端不会帮你保存任何“历史记忆”。这意味着多轮对话的记忆责任完全落在客户端应用身上。
上下文窗口再大也有上限,比如 200k token,但长上下文并不等于长期记忆。你把所有历史消息一股脑塞进去,首先 token 成本会快速膨胀,其次无关信息会稀释真正重要的指令,模型容易被海量旧的寒暄带偏。更麻烦的是跨会话场景——用户今天聊完,明天新开一个窗口,历史消息根本没有机会被一起送进去,模型自然什么都不记得。
这就像一个每周换班的临时助理,每次交接只靠一张越写越长的纸条,纸条总有一天写不下、看不清、找不到重点。传统方案就是把纸条无限加长,也就是在每次调用里拼接所有历史消息,但这条路走到最后一定会撞上成本和效果的双重天花板。所以我们需要一种更结构化的方式,让应用层帮模型“记住”该记住的东西。
1.2 从“缓存”到“长期记忆”的思维转变
很多团队一开始会把思路放在 prompt 缓存上,但缓存解决的只是“重复计算的成本”,它不产生任何新记忆。也就是说,缓存能让你便宜地重复发送同一段历史,但跨会话、跨窗口时,那段历史根本不在缓存里。另一派思路是 RAG,把文档切块、向量化、检索后拼进 prompt,这解决的是“外部知识问答”,而不是“用户和项目的持续状态”。
claude-mem 这类方案的核心是把“记忆”当作独立的基础设施。它不再追求让模型自己记住任何事情,而是在模型之外维护一套持续更新的记忆库:从每轮对话中抽取重要信息、按类型组织、做冲突处理,然后在下次对话前自动把最相关的记忆取回来,注入到 prompt 里。
这个转变很关键:你不再依赖模型的隐性记忆能力,而是显式地管理数据资产。只要记忆库存在,哪怕今天用 Claude,明天换成别的模型,记忆本身不会丢。这也是我认为所有认真做 AI 产品的人都需要补上的一课。
2. claude-mem 的设计拆解:记忆层不是简单存聊天记录
2.1 记忆单元:对话切片、实体关系与偏好元数据
大多数初学者做记忆系统,最容易犯的错误是把“记忆”等同于“聊天记录”。但 claude-mem 真正该存的不是流水账,而是从流水账里提炼出来的高价值信息。我的做法是把记忆分成三类,每一类对应不同的用途。
第一类是事实记忆,包括用户的行业背景、当前项目名称、技术栈、团队成员、截止日期等客观信息。第二类是偏好记忆,包括用户喜欢的回答风格、必须避免的表达方式、对某些方案的倾向等。第三类是任务状态记忆,比如进行中的事项、已经拍板的决策、尚未完成的 TODO 列表。
每条记忆中除了正文内容,我还会带上几个关键元数据字段:type 表示记忆类型,timestamp 记录发生时间,source_session_id 记录来源会话,confidence 表示置信度,scope 表示这条记忆属于哪个用户或项目。这个结构看起来简单,但后来排查问题时帮了大忙——没有元数据支撑的记忆库,只是一堆无法追溯的文本碎片。
打个比方,这就像写日记不是把每天几点吃饭、几点出门全部记下来,而是只记“今天认识了谁、决定了什么、有什么待办”。模型也一样,你让它记住所有细节,它反而什么都用不好。
2.2 记忆的写入与更新:提取→结构化→回填
记忆系统的第一个核心环节是写入,也就是把对话内容变成结构化记忆。我不会把原始对话直接存进长期库,而是先调用 Claude 对最近一段对话做一次“记忆提取”。提取 prompt 大概长这样:要求模型从对话中找出用户明确陈述的事实、表达出的偏好、正在推进的任务,并对每条候选记忆分类、去重、标记置信度,最终输出 JSON。
拿到 JSON 后,下一步是冲突检测。先按实体和类型找到现有记忆,比较新旧内容。如果新记忆与旧记忆说的是同一件事,就根据时间戳和置信度决定是覆盖、合并还是保留旧版。如果新记忆和旧记忆直接矛盾,不能盲目覆盖,需要进入人工确认或标记为“待校验”。这一步的意义在于防止记忆库在不知不觉中变成一团互相矛盾的浆糊。
全部处理完成后,才把记忆回填到存储里。我建议把提取过程放在对话结束之后异步执行,而不是每轮都同步阻塞。这样不影响主对话体验,也方便批量条数控制。简单说,写入逻辑不是“转写”,而是“二次加工”。模型产出的记忆质量,直接决定整个系统后续的效果上限。
2.3 记忆的读取与检索:相关性优先于时间顺序
第二个核心环节是读取。用户发起新问题的时候,你不能把整个记忆库都塞给模型,也不能只拿最近几条。正确做法是先做相关性召回,再做精排,最后控制注入量。
具体流程是:先把用户当前 query 向量化,在记忆库中做相似度检索,取回 top-k 候选记忆。这一步用 embedding 模型,成本低、速度也快。然后第二步会做一个小型 rerank,判断候选记忆和当前问题的相关程度。我会额外加一个时间衰减权重,因为对于任务状态类记忆,越新的越重要;但对于用户长期偏好,时间衰减的影响可以忽略。
召回结果按类型分组后,会被拼装成一个<memory>块放到 system prompt 里,再交给 Claude 回答。需要特别控制 token 上限。以 200k 上下文模型为例,我会把注入的长期记忆控制在 1200 token 以内,超过就只保留相关性最高的片段。历史对话太多时,还可以先用摘要压缩再注入,而不是全文堆砌。
3. 从零搭建 claude-mem:实操配置与代码示例
3.1 环境准备与依赖选型
先说依赖选型。整套 claude-mem 实现里,最核心的组件包括 Claude API、一个 embedding 模型、一个向量存储,以及少量胶水代码。
Claude API 用官方 SDK,也就是 anthropic 包。embedding 我建议先选 text-embedding-3-small,理由很简单:效果稳定,维度低,成本几乎可以忽略。如果你有本地化需求,也可以换 bge-small 系列,效果差别不大。向量存储这里需要看数据规模:个人项目和中小团队场景,直接用 Chroma 就足够,零配置、免运维;等到记忆条目超过百万级、需要高并发检索时,再迁移到 Qdrant 这类服务。不要一上手就上重型组件,维护成本会吃掉你的开发精力。
还有一个选择是是否用 LangChain 之类的框架。我个人的建议是,记忆系统这种业务逻辑非常定制化的东西,直接用原生代码反而更清爽,避免被框架的抽象绕来绕去。下面我给出的实现不依赖任何编排框架,只用了最基础的库,你也可以直接照搬。
环境安装命令大概是这样:
pip install anthropic chromadb openai sqlite-utils如果你用本地 embedding 模型,再加一个 sentence-transformers;如果不需要本地模型,保持简单即可。
3.2 核心调用流程实现
直接看代码。我先把记忆存储封装成一个极简的二维接口:写入和召回。写入负责把结构化记忆存进 Chroma,召回负责按 query 找到最相关的记忆。
from chromadb import PersistentClient from openai import OpenAI embed_client = OpenAI() class MemoryStore: def __init__(self, collection_name="claude_mem"): self.chroma = PersistentClient(path="./mem_db") self.collection = self.chroma.get_or_create_collection( name=collection_name, metadata={"hnsw:space": "cosine"}, ) def add_memory(self, content, mem_type, timestamp, session_id, confidence=0.9, scope="default"): self.collection.add( documents=[content], metadatas=[{ "type": mem_type, "timestamp": timestamp, "session_id": session_id, "confidence": confidence, "scope": scope, }], ids=[f"{session_id}-{timestamp}-{hash(content) % 100000}"], ) def recall(self, query, top_k=5, scope="default"): results = self.collection.query( query_texts=[query], n_results=top_k, where={"scope": scope}, ) mems = [] for doc, meta in zip(results["documents"][0], results["metadatas"][0]): mems.append({ "content": doc, "type": meta["type"], "timestamp": meta["timestamp"], "confidence": meta["confidence"], }) return mems这里有几个细节想强调一下。Chroma 的 PersistentClient 会把数据持久化到本地目录,不需要额外启动服务。相似度空间我选 cosine,因为它对文本向量的效果稳定。id 生成时我把 session_id、timestamp 和内容哈希拼在一起,保证同一轮里重复写入不冲突。
接下来是主对话流程。用户发来消息后,先召回记忆,再组装 system prompt,再调用 Claude。对话结束后,异步提取记忆并写回。
from anthropic import Anthropic claude = Anthropic() mem_store = MemoryStore() def chat_with_memory(user_message, scope="default"): mems = mem_store.recall(user_message, top_k=5, scope=scope) memory_block = "\n".join([f"- [{m['type']}] {m['content']}" for m in mems]) system = f"""你是用户的长期协作助手。 以下是从历史对话中召回的相关记忆: <memory> {memory_block} </memory> 如果记忆与当前对话明显冲突,以当前对话为准,并在回复中提醒用户可能发生了变更。 """ response = claude.messages.create( model="claude-sonnet-4-5", max_tokens=1024, system=system, messages=[{"role": "user", "content": user_message}], ) async_extract_and_store(user_message, response.text, scope=scope) return response.text这段代码里的 async_extract_and_store 就是刚才说的提取与回填。你可以把它做成一个后台任务,比如用 FastAPI 的 BackgroundTasks,也可以用一个简单的线程池来跑。这里不展开完整实现,但核心逻辑就是:把最新一轮 user message 和 assistant response 一起交给 Claude,让它输出 JSON 记忆候选,然后通过 mem_store.add_memory 写回。
3.3 记忆注入模板与 system prompt 设计
记忆召回之后,怎么放进 prompt 里很有讲究。我的标准模板长这样:
你是用户的长期协作助手。以下是从历史对话中召回的相关记忆,它们可能是事实、偏好或任务状态: <memory> <item type="fact" timestamp="2025-06-10T10:00:00Z">用户主力开发语言是 Python,团队规模约 10 人</item> <item type="preference" timestamp="2025-06-11T19:30:00Z">用户不喜欢回复里出现客套话,希望直接给结论</item> <item type="task" timestamp="2025-06-12T09:00:00Z">正在推进登录模块重构,计划两周内上线</item> </memory> 请遵守以下规则: 1. 优先相信记忆中的事实; 2. 若当前对话与记忆明显冲突,以当前对话为准,并提醒用户可能发生了信息变化; 3. 不要因为缺少记忆就编造内容,无法确定时可以明确说明。我特意把记忆放到<memory>XML 块里,而不是直接混在 system prompt 的纯文本里。实测下来,Claude 对这类结构化标签的理解非常稳定,既能准确读取,也知道“memory”是一个外部注入的信息源,而不是它自己前世留下的幻觉。多个不同类型条目分组排列,也便于模型在回答时按类型取用。你还可以在模板里加入“如果记忆不足,建议主动追问”的选项,让助手在必要时主动补信息,而不是硬答。
4. 实战中容易踩的坑:记忆污染、冲突与成本失控
4.1 记忆污染:错误信息会像谣言一样扩散
如果只允许我说一个 claude-mem 实战里最危险的坑,那一定是记忆污染。记忆系统的特点是一旦写入错误信息,它会无条件参与后续所有对话的 prompt 组装。也就是说,一条错误的记忆会像谣言一样传遍整个记忆网络,反复影响模型输出。
举个例子:某次对话中模型猜错了用户所在公司的规模,提取模块没有对信息源做甄别,把“公司约 100 人”当成事实写进了长期库。之后用户再问任何与组织相关的问题,模型都会基于这个错误前提回答,而用户往往会困惑:怎么今天说的话和之前完全对不上。
要避免这个问题,我的经验是三层过滤。第一层在提取 prompt 里明确要求:只提取用户明确陈述的信息,模型自己的推断不能作为 fact 保存;第二层在写入前做置信度判断,置信度低于 0.8 的候选记忆写入“待确认区”,需要用户确认后才升为长期记忆;第三层是周期性的记忆审计,比如每周导出记忆库里的高影响 fact,人工或自动检查是否有过时信息。千万别觉得这三步繁琐,初始不做,后面纠错的成本高十倍。
4.2 冲突与过时:多条记忆相互矛盾怎么办
记忆库运行一段时间后,几乎必然出现矛盾。用户上周说“我更喜欢 Python”,这周说“新项目切到 Go 了”;昨天说“上线时间是月底”,今天说“计划推迟到下个月”。这些冲突如果处理不好,模型就会时而按旧记忆答,时而按新记忆答,完全看检索结果心情。
我总结了一套简单的冲突处理规则,直接放进更新流程里判断:
| 场景 | 旧记忆置信度 | 新记忆置信度 | 处理策略 |
|---|---|---|---|
| 同一事实,新时间戳 | 高 | 高 | 新覆盖旧,保留旧版用于审计 |
| 同一事实,新时间戳 | 高 | 低 | 不覆盖,新记忆进入待确认区 |
| 同一事实,新时间戳 | 低 | 高 | 直接覆盖,并记录变更来源 |
| 明显相反偏好 | 任何 | 任何 | 标记为冲突对,优先使用最新,同时请求用户澄清 |
这套规则用代码实现也非常简单。每次写入前先按 scope 和实体名查询现有记忆,如果存在同类型且实体相似度高的条目,就按上表逻辑决定是否覆盖。处理完成后,我会额外给被覆盖的记忆写一个 deprecated 标记,而不是直接删除。这样万一用户回退到旧方案,你还能找回历史依据。
4.3 成本与效果平衡:不是所有内容都值得进长期记忆
记忆系统带来的成本增加是显式的,主要来自三部分:提取调用消耗的 token、embedding 的 API 费用、以及每次对话把记忆注入 prompt 后多消耗的 token。提取调用通常每轮会消耗 500 到 1000 token,embedding 几乎可以忽略,真正需要重视的是注入 token——它会变成每一次请求的固定开销。
我的建议是,不要每轮对话都做完整提取。比较合理的策略是每 3 到 5 轮对话,或者系统空闲时做一次批量提取;也可以设定“只在上一个提取摘要有实质变化时才更新”。另一个思路是分级记忆:短期会话摘要负责保留最近几轮细节,长期记忆库只保存高价值的偏好与决策。这样既能控制成本,也不会丢失关键信息。
做效果评估时,我最看重的指标是“记忆注入命中率”。也就是被注入的那些记忆里,有多少条真正对当前回答起到了正向作用。这个指标没法直接从日志里看,但可以通过对比实验估算。比如随机抽 20 个用户问题,分别用有记忆和无记忆的 prompt,让同一模型回答,人工判断回答质量差异。如果大多数场景下加了记忆和没加记忆差不多,说明召回逻辑或者记忆质量出了问题,需要回头调。
5. 进阶方向:从个人助手到多智能体共享记忆
5.1 多会话、多用户的记忆隔离与共享
当 claude-mem 从个人项目走向团队应用,第一个绕不开的问题就是记忆隔离。你不能让用户 A 的偏好污染用户 B 的回答,也不能让新项目的记忆变成老项目的干扰。我在设计里引入了 scope 概念,可以理解成命名空间,每个记忆条目都挂在一条完整的路径下。
mem://user/123/project/456/fact/xxx mem://team/ops/preference/xxx这个设计的优点是,recall 的时候可以直接按 scope 过滤,存储层天然支持。使用上,个人助手按 user_id 隔离;团队项目里,项目级记忆共享给所有成员,但个人偏好只对自己可见。多智能体场景下,你还可以让不同的 agent 共享同一份项目记忆,但各自维护独立的状态记忆。需要注意的是共享记忆的并发更新,比如两个 agent 同时写同一条事实,可能会互相覆盖。解决办法是给记忆条目加 version 字段,写入时做乐观锁判断,版本不一致就重读再写。
5.2 记忆可视化、导出与可解释性
记忆系统一旦上线,你就不能只把它当黑盒。用户会好奇 AI 到底“记得”了他什么,研发也需要一个直观的工具来调试。我的做法是用一个轻量管理页面,把记忆库里的条目按 scope 和 type 列出来,支持手动删除、修改置信度、合并相似条目。
导出功能也很重要。每周任务会把全部记忆导出成 JSONL 文件,方便离线审计和备份。很多记忆污染问题,单纯靠回答质量很难定位,但看一眼记忆列表就一目了然。比如某天模型突然开始乱答,打开管理页面发现有一条“用户是 CTO”的错误记忆,答案就清楚了。可以说,可解释性是记忆系统生产化的基础,没有可视化的时候,你不知道模型每一句话背后的“记忆依据”是什么。
5.3 评测闭环:怎么判断记忆系统变好了
判断一个记忆系统有没有变好,不能只靠感觉。我建议建立一套小型评测集和三项核心指标。评测集不用太大,30 到 50 个问题即可,但每个问题都要对应一条必须被召回的特定记忆。
这三项指标分别是:召回率,即评测问题中被正确定位到相应记忆的比例;增益率,即有记忆与无记忆情况下回答准确率的提升幅度;修正频率,即用户每次说“不对,我之前说的是 X”这类纠正的次数。前两项可以离线跑,第三项需要在真实使用中记录。
离线评估跑起来也比较简单:把评测问题的 query 单独输入 recall 接口,检查 top-k 结果里是否包含目标记忆;然后把同一批问题用有记忆和无记忆两套 prompt 各跑一遍模型,人工或用一个更强大的模型当裁判打分。我自己的经验是,只要召回率低于 70%,整个记忆系统带来的体验就会非常不稳定;召回率上到 90% 以上,配合明确的注入模板,回答质量的提升才真正可感知。
6. 写在最后:记忆层会是 AI 应用的下一个标配
做完这个 claude-mem 项目之后,我最大的感受是:把记忆做成显式的数据资产,比靠 prompt 让模型自己记住可靠得多。模型的能力更新换代很快,但记忆层一旦建好,是可以跨越模型版本持续积累的资产。它本质上是在给 AI 应用建立一份“用户模型”和“项目状态机”。没有记忆层的 agent 只能活在当下,有了记忆层,才具备了和用户长期协作的基础。
如果你想在自己的项目里落地这套思路,我给新人的建议是:不要一上来就做复杂的知识图谱,也不要想着把所有对话都存下来。先从最基础的三类结构化记忆开始,也就是偏好、事实、任务状态,配合一个简单的向量检索,把闭环跑通。跑通之后再逐步加实体关系、跨会话推理和更多自动化更新逻辑。
最后分享一个我自己常用的排查技巧:在记忆提取的 prompt 里,明确要求模型区分“这是用户亲口说的信息”和“这是模型根据上下文推断的信息”,并分别打上不同标签。只把前者作为高置信度事实入库,后者一律标为低置信度或直接丢弃。这一条规则,能帮你躲掉大半记忆污染带来的头疼问题。