打造Claude长期记忆:claude-mem设计与实现
1. 为什么我要给 Claude 写一个叫 claude-mem 的记忆模块先说一个大多数 Claude 重度用户都会遇到的痛点对话窗口一关所有上下文全部归零。你费了半小时给它交代的项目背景、技术栈约束、代码风格偏好下一次开新会话时它又全忘了你得一个字一个字重新输入一遍。这个问题我用了一个多星期就受不了了于是花了差不多三个晚上写了个叫claude-mem的小工具专门给 Claude 补上“长期记忆”。claude-mem 本质上是一个夹在 Claude API 和你之间的小服务它会自动记录你们每次聊天的内容把它们整理成结构化的记忆存入本地数据库然后在后续对话开始时自动把相关记忆重新塞回 Claude 的上下文里。换句话说它让 Claude 从一个“不过脑子的聊天机器人”变成了一个“记得你上周说了什么的长期协作者”。这个工具解决的核心问题有三个第一跨会话上下文保留不用每次重复交代背景第二历史知识积累比如你告诉过它的代码规范、项目架构、口语表达偏好都能沉淀下来第三检索增强它不只是简单存聊天记录而是用语义相似度找到真正和当前问题相关的旧记忆把这个信息塞进 prompt。适合谁来用如果你在用 Claude API 做开发、写自动化脚本、做内容创作、或者拿它当长期学习/思考伙伴claude-mem 能帮你省掉大量重复沟通成本。如果你只是偶尔拿 Claude 聊两句那这个工具对你来说可能有点过度设计了。下面我把设计思路、实现细节、踩过的坑一条条讲清楚。2. 整体架构拆解记忆从哪来、存在哪、怎么取回来2.1 我一开始最纠结的问题到底怎么“记住”一段对话最早我走了一个很蠢的方案——直接把整段对话历史原封不动存下来下次全量塞给 Claude。跑了半天就发现完全不可行上下文窗口根本扛不住聊一天的内容就能撑爆 200K token。而且绝大多数历史内容是没用的寒暄全塞回去不仅浪费 token还稀释了关键信息Claude 反而抓不住重点。后来我把思路拉回到人类记忆的本质上人不会记住每一句话而是记住那些重要的结论、偏好、事实和事件。所以 claude-mem 要做的不是“录像”而是“做笔记”。每次对话结束它会自动读一遍你们的聊天记录提炼出几条精简的记忆条目然后存进数据库。这就是核心设计理念——记忆是压缩过的结论不是原始记录。这个理念直接决定了整个架构一个负责捕获对话并提炼记忆的处理器一个负责存记忆的存储层一个负责检索相关记忆的查询器最后是一个把记忆注入 prompt 的组装器。四个模块各干各的互相之间通过简单的接口通信出问题了也方便单独排查。2.2 为什么要用“SQLite 向量索引”双存储而不是单一数据库存储层我试过纯 SQLite也试过纯向量数据库最后发现两个都得要但分工不同。SQLite 用来存记忆的“本体”——也就是具体的文本内容、创建时间、来源会话、记忆类型这些结构化字段。向量索引则用来存每条记忆的语义向量也就是 embedding检索的时候靠计算余弦相似度来找“语义上相近”的记忆。举一个很直观的对比你今天告诉过 Claude “这个项目的 RDS 实例在 ap-southeast-1数据库账号不要用默认 admin”如果下周你问它“数据库连接信息是什么来着”关键词完全对不上但语义上是同一个问题。这时候就必须靠向量检索才能捞出来。但如果只靠向量检索记忆管理功能比如列出所有记忆、按时间筛选、删掉某条错误记忆就会变得非常麻烦SQLite 干这活儿最顺滑。所以我的结论是结构化元数据和全文存入 SQLite语义向量单独存。不整那些重型的 Milvus、Weaviate一个本地文件库加上一个 embedding 接口就够用了独立部署、轻量、不引入额外运维负担。2.3 记忆的组织方式事实、偏好、关系、经验四类分开管存储结构不能是“一锅乱炖”。我在设计 schema 的时候把记忆分成了四个类型fact事实客观信息比如“服务器地址是 10.0.0.5”“项目用的是 Python 3.11”。preference偏好用户明说或隐含的喜好比如“代码注释用中文写”“回复尽量简洁不要铺垫”。relation关系事物之间的关联比如“订单服务和支付服务通过 RabbitMQ 通信”。experience经验从交互中沉淀的技巧和结论比如“上次用直接改库表的方式出过事故以后千万别这么干”。这个分类看起来有点多余但后面真正用起来会发现非常关键。因为不同类型的记忆在注入 prompt 时的表达方式完全不同。fact 类适合直接陈述preference 类需要转成“要求”句式experience 类则更适合以“经验教训”的方式提醒 Claude 规避风险。如果没有这层分类所有记忆混在一起生成的效果特别生硬。2.4 记忆注入的时机和方式不是全量倒给 Claude记忆写进去之后怎么取回来也是门学问。我现在的做法是分三次注入对话开始时注入一次“长期记忆概要”对话过程中根据用户新输入实时检索注入一组相关记忆对话结束后再把本次对话的关键信息存入长期记忆库。需要注意注入不是把检索到的记忆原文一股脑拼在 prompt 后面而是通过一个“记忆转述器”把它们改写成适合模型理解的自然语言段落。比如原始记忆条目是“RDS instance: rds-abc123, region: ap-southeast-1”转述后就变成“用户之前的项目中使用了一个 RDS 实例ID 为 rds-abc123位于 ap-southeast-1 区域。如果涉及数据库连接优先使用这个实例的信息。”听起来像是一段临时的背景说明Claude 接受度会高得多。3. 核心实现细节从零开始搭一个可用的 claude-mem3.1 环境准备和依赖选择先交代一下我本地的运行环境macOS Python 3.11Anthropic 官方 SDK。完整的依赖包如下anthropic0.40.0 chromadb0.5.5 openai1.55.0 # 这里用的是 OpenAI 的 embedding 接口也可以用别的 python-dotenv1.0.1 pydantic2.9.2这里有个选择逻辑想多说两句。embedding 模型我最初想直接用 Anthropic 自家的接口但当时还没开放 embedding API所以退而求其次用 OpenAI 的 text-embedding-3-large。如果你想完全避开 OpenAI也可以用本地模型比如 BGE-M3跑在 llama.cpp 上代价是要多花几百 MB 内存。我个人建议项目早期先用云 API 把链路跑通确认方案可行后再决定要不要换本地推理。3.2 对话捕获堵住流式输出的漏洞Claude API 默认是流式返回结果的你不可能等到全部输出完了再一次性拿完整消息。所以对话捕获这块我折腾了最久。先说结论必须在发送 API 请求之前就把用户消息和系统 prompt 完整记录下来然后在流式回调里把 assistant 回复分段拼接完整。具体实现逻辑大致是这样的class ConversationRecorder: def __init__(self, memory_store): self.memory_store memory_store self.user_messages [] self.assistant_reply_chunks [] def record_user_message(self, content: str): self.user_messages.append({ timestamp: datetime.now().isoformat(), role: user, content: content }) def on_assistant_chunk(self, chunk: str): self.assistant_reply_chunks.append(chunk) def finalize_turn(self, session_id: str): full_reply .join(self.assistant_reply_chunks) self.memory_store.extract_and_save( user_contentself.user_messages[-1][content], assistant_contentfull_reply, session_idsession_id )这个类做的事情很朴素在发起请求之前先记录用户消息流式返回过程中不断把 chunk 拼起来整个 turn 结束后把完整配对交给记忆提取模块。要注意把 timestamp 记精确一点后面做记忆衰减和时间过滤都用得上。3.3 记忆提取让 Claude 自己给自己“画重点”记忆提取这一步我直接用了 Claude 本身因为说实话上下文理解和总结归纳本来就是它的强项。我设计了一个提取 prompt要求它从一段用户-助手对话中提炼出值得长期保留的记忆要点并且用 JSON 格式返回方便程序解析。提取 prompt 我经过多次迭代最终版本核心逻辑是这样的你是一个记忆提取器。以下是一段用户与 AI 助手之间的对话记录你的任务是把这段对话中值得长期记忆的内容提取出来以 JSON 数组形式返回。每个元素必须包含 - type: 记忆类型只能是 fact / preference / relation / experience 之一 - content: 记忆内容一句话描述不超过30个字 - importance: 重要程度评分1到10之间的整数 提取原则 1. 只提取可能影响未来对话的长期信息临时性的、一次性的指令不要提取 2. 明确数值、名称、地址、时间等具体信息优先提取 3. 用户明确表达的喜好、倾向、禁止事项必须提取 4. 一个对话片段最多提取5条记忆宁缺毋滥 5. 不要提取任何涉及个人身份认证信息的原始凭证如密码、API key注意最后一条是我专门加的因为一开始系统会把用户发出的 API key 直接存成记忆后面检查记忆库的时候简直不堪入目。凡是看起来像密码、token、密钥的内容一律用REDACTED占位符替代。3.4 向量化存储与检索为什么阈值不能一刀切提取出来的记忆文本保存到 SQLite 之后我会异步生成一个 embedding存入 ChromaDB。检索的时候把用户当前的问题做同样的 embedding然后做余弦相似度搜索。这里必须分享一个踩过的坑——相似度阈值千万不要固定死。我早期设了一个 0.75 的固定阈值结果在代码类问题上几乎检索不到任何记忆因为代码里的变量名、函数名和日常自然语言的向量距离天然就远。后来我把阈值改成了动态策略根据当前对话类型做调整技术类问题降到 0.6普通闲聊类问题维持 0.75 以上。经验法则就是宁可多召回两条再靠模型自己判断也不能把真正相关的记忆卡在阈值外面。实际检索核心代码片段如下def retrieve_relevant_memories(query: str, top_k: int 5) - list[str]: query_vec embedding_model.encode(query) results vector_collection.query( query_embeddings[query_vec], n_resultstop_k ) memory_ids results[ids][0] # 再回 SQLite 取全文 memory_texts sqlite_store.fetch_memories_by_ids(memory_ids) return memory_texts如果相似度得分最高的那条记忆得分都很低说明当前问题跟历史记忆完全无关这时候就直接返回空列表不上注入逻辑。测试下来平均一次检索耗时大概是 200ms 左右对对话体验几乎没有影响。3.5 记忆注入的 prompt 组装策略检索回来的记忆不是简单接在 system prompt 后面就完事。我维护了一个记忆注入模板每次把记忆改写成自然的背景描述再按优先级排序后拼接[长期记忆] 用户在过去的对话中曾提供过以下信息如果与当前问题相关请优先参考 - 关于数据库环境用户项目的 MySQL 实例有两个分别服务于订单系统和用户系统... - 关于代码风格用户明确表示过 Python 代码必须加类型注解变量命名用 snake_case... - 关于项目约束用户之前反馈过直接在生产库执行 ALTER TABLE 导致过事故涉及结构变更时务必提醒...这里有个细节值得注意不是每条记忆都需要原样引用。比如 preference 类型的记忆我会让转述器把它变成“用户要求/用户偏好/用户禁止”这样的祈使句式Claude 在读 prompt 的时候会把它当成约束而不是普通背景信息。说白了就是给不同记忆类型不同的“语气”这样模型对信息的权重判断会更准确。注入位置也有讲究。长期记忆放在 system prompt 里短期相关记忆放在 user message 前面两部分的职责不要混。长期记忆提供的是整体世界观短期记忆解决的是当前这个问题相关的局部信息。如果全扔进 system promptClaude 处理当前任务时注意力容易被过多的背景信息分散。3.6 记忆合并与去重怎么样避免记忆库“发酵”用了一周之后你会发现新问题——记忆库膨胀。同一个信息换个角度聊一遍就会被重复存储第一次记的是“项目数据库在 AWS RDS 上”第二次记的是“数据库实例类型是 db.r6g.large”第三次记的是“RDS 自动备份保留 7 天”。表面看三条各有侧重实际上都指向同一个实体检索的时候三条会被同时捞回来既浪费 token 又显得啰嗦。我的解决方案是加了一个记忆合并模块新记忆写入前先在向量库里做一次相似度检索如果它与某条旧记忆的相似度超过 0.86就判定为“关联记忆”这时不是新增而是把新内容融入旧条目。具体做法是对旧记忆的 content 字段追加补充信息并刷新 embedding 重新入库。这个机制保证了记忆库里每一条记录都是高度凝炼的不会出现十条讲同一件事的记录。实测下来这个合并机制把记忆库的膨胀速度降低了大约 60%检索质量也有明显提升。如果你不做合并记忆库大约运行两周就会变成一堆语义互相覆盖的文本堆效果还不如不做记忆。3.7 遗忘机制如何让记忆“保鲜”不要小看遗忘这个功能。人类记忆本来就是会衰减的如果你把所有记忆都永久保留会出现一个严重问题——很久以前的陈旧事实会污染当前的判断。比如三个月前你用的还是 Python 3.10现在项目已经迁移到了 3.12但记忆库还存着 3.10 的信息Claude 每次都会按旧版本给你建议。我实现了一个简单的时效加权机制每条记忆在检索评分时会乘上一个时间衰减系数。计算公式是final_score similarity_score * (1 - age_days / half_life_days)half_life_days 我默认设成了 30 天也就是 30 天后这条记忆的相关性权重减半。超过 180 天没有更新的记忆不再主动注入。注意这里不是删除只是降权——如果用户明确提到了一个非常古老的话题相关记忆仍然可以通过提高相似度匹配被检索出来。这个设计我觉得是 claude-mem 区别于“简单聊天记录存储”的关键所在它不是无限的累积堆叠而是一个动态的、有生命周期的记忆系统。4. 三种真实使用场景的效果对比为了让你更直观地理解 claude-mem 的价值我贴三个实际场景的对比结果。我做了个简单的实验同一个项目开发任务分别用原生 Claude 和加了 claude-mem 的 Claude 去完成记录两者的表现差异。4.1 场景一跨会话的项目开发第一个场景是一个持续两周的 API 服务开发项目。原生 Claude 每次开新对话都需要重新说明项目结构、技术栈FastAPI PostgreSQL Docker、代码风格偏好、以及当前进展整个过程大约要花 10 分钟“预热”。而加了 claude-mem 之后第二次对话开始时系统自动注入了之前沉淀的记忆Claude 直接就记得项目叫什么、用了什么框架、数据库连接方式、甚至上次代码 review 时提过的问题。最有说服力的一个例子是我在第三天的对话中问“上次说的那个数据库连接池的坑我们后来怎么解决的”原生 Claude 完全懵了因为当时那个对话已经不在上下文窗口里了。但 claude-mem 从记忆库里检索出了相关 experience 类记忆Claude 不仅准确复述了问题原因还提醒我这个坑在另一个服务里也可能存在。这种跨会话的“项目连续性”是纯无状态 API 完全做不到的。4.2 场景二长周期写作项目写作场景更看重风格一致性。我之前让 Claude 帮我写一个系列的技术专栏每周写一篇要求语气、结构、案例风格都保持一致。原生 Claude 每周都要重新调教一遍“文风参数”而且就算调了也经常偏离前几篇的语境。claude-mem 在这个场景下主要靠 preference 类记忆发挥作用。系统记住了“用户喜欢先给结论再展开”“代码示例尽量简短”“每篇要有一个踩坑故事”这些偏好后续对话即使不重新指定Claude 也会自动按照这些偏好生成。体验上最明显的变化就是改稿时间从每篇三四十分钟降到了十分钟以内。4.3 场景三日常知识管理第三个场景是我自己在用的把 Claude 当成一个“外置大脑”来整理读过的文章、想法碎片和项目灵感。比如我在某天读到一篇关于数据库索引优化的文章就跟 Claude 聊了一阵它帮我总结了几条关键法则。后来过了半个月我在设计一个新的查询接口时问了句“关于索引设计我们之前总结过什么原则”claude-mem 把那条 relation 类记忆拉了出来Claude 直接给出了之前讨论过的索引设计几条要点还加了一句“这条总结来自你之前分享的文章”。这类场景对记忆的依赖度最高。说实话没有 claude-mem 之前Claude 对我说就像个“每次都重新认识”的陌生人——它的回答虽然很有道理但跟过去的积累没有任何关联。补上记忆之后它才真正变成了一个我持续使用的“知识合伙人”。5. 常见问题与排查技巧实录5.1 问题一检索到的记忆明显不相关甚至干扰了回答这是我最常遇到的问题之一。比如我在问“订单系统的超时重试机制怎么设计”结果注入的记忆全是“数据库备份策略”的内容。排查思路有三个层次先看是不是相似度阈值设得太低很多低质量记忆混进来了再看是不是记忆合并失效导致语义相近的条目重复堆积最后看是不是 embedding 模型本身对特定领域文本区分度不够。我最推荐的排查手段是打开 claude-mem 的“检索日志”模式把每次检索的 query 和召回结果都打出来肉眼检查一遍。你会发现大部分问题出在阈值上不同主题的文本相似度分布区间差异非常大。比如代码类问题的语义相似度普遍比自然语言类低 0.1 到 0.2如果不分场景调整阈值误召回率会高得离谱。解决办法就是按对话类型设定检索参数我前面已经提到过动态阈值的经验值这里就不再重复。5.2 问题二记忆库膨胀到几万条检索速度明显变慢ChromaDB 在小规模下速度很快但到了几万条这个量级每次嵌入检索开始出现可见延迟大概 500ms 以上。而且更重要的是记忆库里充斥着大量无用信息就算检索够快召回质量也会变差。这里我的建议不是优化检索性能而是控制记忆入库的质量门槛。我在提取器中砍掉了一条硬规则就是 importance 低于某个分数我定的是 6 分的记忆直接不写入向量库只保留在 SQLite 里做历史存档。这从根本上遏制了记忆库的疯狂扩张。另外定期跑一次清理任务删除超过半年未被检索过的记忆条目。5.3 问题三隐私和数据安全怎么处理既然叫记忆库那里面存的东西一定包含你的敏感信息。这一点必须严肃对待。我的做法是所有本地存储默认不加密因为在本机跑问题不大但只要涉及远程服务器部署记忆库就必须整体加密存储。更关键的是在上游拦截——记忆提取 prompt 里已经强制要求对密钥类内容打码绝不落库。另外一个安全设计是“对话黑名单”比如你在 prompt 里明确写入“以下这段对话禁止记入记忆”claude-mem 就会跳过这个会话的记忆提取。这个功能实现起来非常简单就是一条正则匹配的事但它给用户提供了一个信任边界记忆不是无条件记录而是你可以控制的。5.4 问题四API 费用会不会涨很多说实话加了记忆功能后API 调用次数确实变多了主要是因为每次对话结束要多一次记忆提取调用每次对话开始平均多一次检索和一次记忆注入。实测下来在正常的中等规模使用中月 API 费用大约上涨了 15%-25%但换来的是上下文长度精简——因为不再需要每次手动给 Claude 写长篇背景说明实际发送的 prompt 长度反而变短了token 在某种意义上打平了。我给出一个更具体的节省方案不要每个 turn 都触发记忆提取改成每 5 个 turn 或者每次用户明确表达“记住这个”的时候才提取一次。这个策略能把费用增量压到 8% 左右记忆效果损失也不算特别明显。这个取舍要看你对记忆密度的需求来定。5.5 避坑清单这些都是我真实花时间踩过的坑绝对不要在提取 prompt 里用“提取所有信息”这种措辞模型会疯狂输出废话我最高纪录一次提取出 30 多条垃圾记忆。必须用“最多 5 条、宁缺毋滥”这类限制词。embedding 模型中途换过一次结果旧记忆的向量全部失效重新 embed 了全库才恢复。从此我就在代码里加了一个 model_version 字段标记每条向量的生成模型非必要不混用。记忆注入的位置不对会导致 Claude 把记忆当成用户当前指令去执行我在早期把记忆拼在了 user message 末尾结果 Claude 回复“好的我会记住这些要求”——完全搞错了优先级。后来改成放在 system prompt再配合“以下为背景信息不是用户最新指令”的显式提示就没再犯过这个问题。SQLite 的并发写问题确实存在多个会话同时结束时并发写库会报 database is locked。我给写入操作加了一个简单的队列锁就解决了没必要为此换上 PostgreSQL本地工具场景 SQLite 完全够用。6. 如果你想自己动手做一个类似的记忆增强工具整个 claude-mem 的核心代码大概只有 800 行左右拆开来其实没多复杂。骨架就是一个 API 请求包装层用来记录和转发对话一个提取器负责调用模型做记忆提炼一个存储层包含 SQLite 和向量索引一个检索器负责计算相关性最后是 prompt 组装器。我这里分享一个完整的目录结构和最低可运行版本的调用流程你可以顺着这个思路去实现claude-mem/ ├── config.py # 全局配置模型名、阈值、路径 ├── recorder.py # 对话捕获与流式拼装 ├── extractor.py # 调用 Claude 提取记忆 ├── storage/ │ ├── sqlite_store.py # 结构化记忆读写 │ └── vector_store.py # embedding 存取 ├── retriever.py # 记忆检索与合并 ├── injector.py # 记忆改写与 prompt 注入 └── cli.py # 命令行入口测试用一个简化版的完整调用流程核心就四步# 1. 记录用户消息发送给 Claude recorder.record_user_message(帮我看看这段代码有什么问题...) reply client.messages.create(...) # 流式返回 # 2. 对话结束后调用提取器 memories extractor.extract(user_msg, full_reply) # 3. 记忆入库同时生成向量 for mem in memories: sqlite_store.insert(mem) vector_store.upsert(mem) # 4. 新对话开始时检索并注入 related retriever.retrieve(帮我看看这段代码) injector.inject_to_prompt(related)如果你想在自己项目里快速接入这个能力建议不要一上来就做完整版而是先跑通这个四步闭环记录对话 → 提取记忆 → 存起来 → 检索注入。先把链路跑通再逐步加上记忆合并、遗忘机制、动态阈值这些进阶特性。重要提示一定要在早期就设计好“忘记”的能力不要只做“记住”。一个只能增不能减的记忆系统最终一定会劣化到你不敢用它的地步。我自己的体会是claude-mem 这个项目做完之后不仅让我日常用 Claude 的效率翻了一倍也让我对“上下文工程”这件事的理解深了一层大模型本身没有记忆但你可以通过精巧的外围系统让它表现得像一个有记忆的实体。这种可持续积累的智能体验比单纯追求单次回答的聪明程度重要得多。如果你也在尝试给 AI 工具做记忆增强希望这篇分享能给你提供一些可以直接照搬的思路。