2026/10/26 22:47:22

claude-mem:为Claude构建可检索、可累积的长期记忆系统

claude-mem:为Claude构建可检索、可累积的长期记忆系统 1. 项目缘起与核心定位第一次看到claude-mem这个名字我的直觉是这大概率是一个围绕 Claude 生态做“记忆层”的项目。事实也确实如此。它要解决的是一个所有长期使用大模型的人都会遇到的痛点——模型没有持久记忆。每次开新会话之前聊过的项目背景、代码约定、个人偏好、踩过的坑全部归零你得重新交代一遍。短对话无所谓但当你把 Claude 当成日常开发助手、写作搭档或者知识管理工具时这种“失忆”会让人抓狂。claude-mem的核心价值就是给 Claude 这类对话式 AI 补上一层可检索、可累积、可复用的长期记忆。它不是一个模型也不是一个插件市场里的花哨玩具而是一套偏工程化的记忆管理方案把对话中产生的关键信息抽取出来结构化存储然后在后续会话中按需召回拼进上下文。说白了它让 AI 从“每次重新认识你”变成“记得你上次说过什么”。这篇文章适合几类人看一是重度使用 Claude 做开发或内容创作的从业者想摆脱重复交代背景的低效二是对 RAG、向量检索、上下文工程有一定了解想动手搭一套个人记忆系统的工程师三是产品经理或独立开发者想理解“AI 记忆”这件事在工程上到底怎么落地。我会从设计思路、核心细节、实操流程到问题排查完整拆一遍尽量让你看完能自己复现一套。需要先说明一点claude-mem这类项目的具体实现会随版本演进下面涉及的结构、参数、流程一部分来自我对这类记忆系统的通用工程实践总结属于“一个合格从业者在此情境下最可能采用的合理方案”你在实际落地时按自己环境调整即可。2. 整体设计与思路拆解2.1 为什么“记忆”不能只靠加长上下文很多人第一反应是现在上下文窗口都到 200K 甚至更大直接把历史对话全塞进去不就行了我早期也这么干过结论是——能塞但不好用而且贵。第一成本问题。上下文越长每次请求的 token 消耗越大长期高频使用账单会很难看。第二注意力稀释。上下文里塞了大量无关历史模型对当前任务的聚焦能力反而下降容易出现“答非所问”。第三也是最关键的历史对话里 90% 是废话——寒暄、试错、重复确认真正有价值的决策和结论可能只占 10%。把整段历史当记忆等于把垃圾和金子一起存。所以claude-mem的设计思路必然是抽取 结构化 按需召回而不是简单堆历史。这跟 RAG 的思路一脉相承但更聚焦于“对话记忆”这个场景。2.2 记忆分层短期、长期、工作记忆我在设计这类系统时习惯把记忆分成三层claude-mem的合理架构也大致如此工作记忆Working Memory当前会话的上下文随对话实时更新会话结束即丢弃或压缩。长期记忆Long-term Memory跨会话持久化的关键信息比如项目约定、用户偏好、重要结论存在数据库里。检索记忆Retrieval Memory长期记忆的索引层通常是向量库 关键词索引负责在需要时快速召回相关片段。这样分层的好处是职责清晰工作记忆管“当下”长期记忆管“沉淀”检索记忆管“找回来”。三者配合才能在有限上下文里塞进最相关的信息。2.3 存储选型向量库 关系库的组合拳纯向量库能解决语义召回但解决不了精确查询。比如你想找“上周三关于数据库选型的结论”向量检索可能给你一堆语义相近但时间不对的片段。所以我的经验是向量库 关系库组合存储类型承担职责常见选型选择理由向量库语义相似召回Chroma、Qdrant、pgvector支持模糊语义匹配适合“意思相近”的检索关系库元数据、时间、标签过滤SQLite、PostgreSQL精确条件查询支持时间范围和分类筛选全文索引关键词精确匹配SQLite FTS5、Elasticsearch补足向量检索对专有名词不敏感的短板选 SQLite 作为起步是很多个人项目的务实选择——零运维、单文件、够用。等数据量上来了再迁 PostgreSQL 或独立向量服务。这个取舍逻辑是先跑通再优化别一上来就上分布式那是给自己找麻烦。2.4 召回策略混合检索才是正解单一检索方式都有短板。纯向量检索对“项目代号 X”这种专有名词不敏感纯关键词检索又抓不住语义。所以claude-mem合理的召回策略是混合检索 重排序向量检索取 Top-K 语义相关片段关键词检索取 Top-K 精确匹配片段合并去重后用一个轻量重排序模型或简单的加权打分排序取最终 Top-N 拼进上下文。这个流程听起来复杂但每一步都有现成工具落地难度没想象中大。关键是别追求一步到位先做向量检索跑通再逐步加关键词和重排。3. 核心细节解析与实操要点3.1 记忆抽取从对话里挑出“值得记”的东西这是整个系统里最考验设计的一环。对话是流水账记忆是精华抽取质量直接决定系统好不好用。我的做法是用一次轻量 LLM 调用做抽取而不是写一堆正则规则。抽取的 prompt 大致长这样这是我在实际项目中反复调过的版本你是一个记忆抽取器。请从以下对话片段中提取值得长期记住的信息。 只提取以下类型 1. 用户的偏好和习惯如喜欢用 TypeScript 2. 项目相关的决策和约定如数据库用 PostgreSQL 3. 重要的结论和事实如接口超时阈值设为 3 秒 4. 待办和未完成事项 忽略寒暄、试错过程、重复确认、临时性内容。 输出 JSON 数组每项包含 content、type、importance(1-5)。 对话片段 {conversation}这里有几个实操要点。第一importance 字段很关键它决定了后续召回的优先级别省。第二抽取要异步做别阻塞主对话流程否则用户体验会很差。第三抽取粒度要控制太细会碎片化太粗会丢信息我一般按“一个独立事实一条记录”来切。注意抽取用的模型不必是最强的小模型够用因为任务相对简单。用强模型做抽取是浪费成本会翻好几倍。3.2 去重与合并别让记忆库变成垃圾场抽取出来的记忆如果不做去重很快就会出现“用户喜欢 TypeScript”存了二十遍的情况。我的处理方式是两阶段去重写入时去重新记忆入库前先做一次向量相似度查询如果和已有记忆相似度超过阈值我一般设 0.92就不新增而是更新已有记录的“最后确认时间”和“确认次数”。定期合并每周跑一次批处理把语义高度重叠的记忆合并成一条更完整的记录。确认次数这个字段很有用——它天然反映了某条记忆的重要性。被反复确认的记忆召回时应该加权。3.3 上下文注入怎么把记忆“喂”给模型召回只是第一步怎么把召回的记忆拼进 prompt 也有讲究。我的经验是分区块注入并明确标注[长期记忆 - 用户偏好] - 用户偏好使用 TypeScript 而非 JavaScript - 用户习惯用函数式风格写代码 [长期记忆 - 项目约定] - 项目数据库使用 PostgreSQL 15 - API 超时阈值统一设为 3 秒 [当前对话] ...这样标注的好处是模型能清楚区分“这是背景知识”和“这是当前任务”减少混淆。别把记忆和当前对话混在一起那会让模型分不清主次。另外注入的记忆条数要控制。我一般限制在 5-10 条总 token 不超过 1500。多了反而干扰。宁可少而精不要多而杂。3.4 记忆衰减让旧信息自然退场不是所有记忆都值得永久保留。三个月前的一个临时决定现在可能已经失效了。所以我给记忆加了衰减机制每条记忆有last_accessed和access_count长期未被召回且确认次数低的记忆降低召回权重超过一定时间如 180 天且从未被再次确认的记忆归档而非删除。这个机制的核心逻辑是记忆的价值随时间衰减但被反复使用的记忆价值反而上升。这跟人脑的记忆规律其实很像。4. 实操过程与核心环节实现4.1 环境准备与依赖安装先把基础环境搭起来。我用 Python 做示例因为生态最全。python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install anthropic chromadb sqlite-utils sentence-transformers这里解释下选型anthropic是官方 SDKchromadb做向量存储起步够用且零配置sqlite-utils简化 SQLite 操作sentence-transformers提供本地 embedding 模型省得调外部 API。提示embedding 模型我推荐all-MiniLM-L6-v2体积小、速度快、效果够用。别一上来就用大模型做 embedding本地跑不动还慢。4.2 数据库结构设计SQLite 建表结构如下CREATE TABLE memories ( id INTEGER PRIMARY KEY AUTOINCREMENT, content TEXT NOT NULL, type TEXT NOT NULL, importance INTEGER DEFAULT 3, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, last_accessed TIMESTAMP, access_count INTEGER DEFAULT 0, confirm_count INTEGER DEFAULT 1, archived INTEGER DEFAULT 0 ); CREATE VIRTUAL TABLE memories_fts USING fts5( content, contentmemories, content_rowidid );memories存主数据memories_fts是全文索引表用于关键词检索。向量则存在 Chroma 里用id关联。这个设计的好处是关系数据、全文索引、向量索引各司其职查询时灵活组合。4.3 记忆写入流程实现写入是核心链路我把它拆成四步抽取、去重、入库、索引。import anthropic import chromadb import json client anthropic.Anthropic() chroma chromadb.PersistentClient(path./memory_db) collection chroma.get_or_create_collection(memories) def extract_memories(conversation: str) - list: prompt f从以下对话提取值得长期记住的信息输出 JSON 数组。 每项包含 content、type、importance(1-5)。 忽略寒暄和试错过程。 对话 {conversation} resp client.messages.create( modelclaude-3-5-haiku-20241022, max_tokens1024, messages[{role: user, content: prompt}] ) text resp.content[0].text # 容错处理防止模型输出带 markdown 代码块 text text.strip().removeprefix(json).removesuffix().strip() return json.loads(text) def is_duplicate(content: str, threshold: float 0.92) - bool: results collection.query(query_texts[content], n_results1) if not results[distances] or not results[distances][0]: return False # Chroma 返回的是距离越小越相似转成相似度 similarity 1 - results[distances][0][0] return similarity threshold def save_memory(mem: dict): if is_duplicate(mem[content]): # 更新确认次数而非新增 update_confirm_count(mem[content]) return # 写入 SQLite db[memories].insert({ content: mem[content], type: mem[type], importance: mem[importance] }) mem_id db[memories].last_pk # 写入向量库 collection.add( ids[str(mem_id)], documents[mem[content]] )这段代码有几个细节值得说。第一抽取模型用 Haiku 而非 Opus因为抽取任务简单用强模型是浪费。第二JSON 解析要做容错模型有时会输出带 markdown 包裹的内容得清理。第三去重阈值 0.92 是我调出来的经验值太低会误判不同记忆为重复太高会漏掉真正的重复。4.4 记忆召回流程实现召回是另一个核心链路我采用混合检索def recall_memories(query: str, top_k: int 8) - list: # 向量检索 vec_results collection.query(query_texts[query], n_resultstop_k * 2) vec_ids [int(i) for i in vec_results[ids][0]] # 关键词检索 fts_rows db[memories_fts].search(query, limittop_k * 2) fts_ids [r[rowid] for r in fts_rows] # 合并去重 candidate_ids list(set(vec_ids fts_ids)) # 按 importance 和 access_count 加权排序 rows db[memories].rows_where( id IN ({}).format(,.join(? * len(candidate_ids))), candidate_ids ) scored sorted( rows, keylambda r: r[importance] * 2 r[access_count] * 0.5, reverseTrue ) return scored[:top_k]这里的排序公式importance * 2 access_count * 0.5是我拍的经验权重。importance 权重更高因为它代表内容本身的价值access_count 权重低一些作为辅助信号。你可以根据自己的使用习惯调这两个系数。召回后更新last_accessed和access_countdef mark_accessed(mem_id: int): db[memories].update(mem_id, { last_accessed: datetime.now().isoformat(), access_count: db[memories].get(mem_id)[access_count] 1 })4.5 与 Claude 对话的集成最后把记忆层接到对话流程上。核心逻辑是对话前召回记忆注入对话后异步抽取新记忆。def chat_with_memory(user_input: str, session_history: list) - str: # 1. 召回相关记忆 memories recall_memories(user_input) memory_block format_memories(memories) # 2. 构造带记忆的 prompt system_prompt f你是一个有长期记忆的助手。 以下是关于用户和项目的长期记忆请在回答时参考 {memory_block} # 3. 调用 Claude resp client.messages.create( modelclaude-3-5-sonnet-20241022, max_tokens4096, systemsystem_prompt, messagessession_history [{role: user, content: user_input}] ) answer resp.content[0].text # 4. 异步抽取新记忆不阻塞返回 threading.Thread( targetextract_and_save, args(user_input \n answer,) ).start() return answer异步抽取这一步很关键。如果同步做用户每次都要多等一两秒体验很差。用线程或任务队列异步处理用户无感知。5. 常见问题与排查技巧实录5.1 记忆召回不准怎么办这是最常见的问题。表现是明明存过相关信息但对话时没被召回。排查思路按这个顺序走排查项检查方法常见原因解决方向是否真的存进去了直接查数据库抽取环节漏了调抽取 prompt向量是否正常手动 query 测试embedding 模型不匹配统一 embedding 模型相似度阈值打印距离值阈值设太高降到 0.85 左右试排序权重看召回列表importance 权重过高调低 importance 系数我踩过最坑的一次是写入和查询用了不同的 embedding 模型导致向量空间不一致召回全是乱的。这个错误很隐蔽因为代码不报错只是结果不对。所以务必保证写入和查询用同一个模型。5.2 记忆库膨胀太快用一段时间后发现记忆条数暴涨检索变慢。原因通常是抽取太激进把鸡毛蒜皮都存了。解决办法提高抽取的 importance 门槛只存 importance 3 的加强去重把相似度阈值从 0.92 降到 0.88定期跑归档任务把 180 天未访问且 confirm_count 1 的记忆归档。提示归档不是删除是标记 archived 1检索时默认过滤掉。这样万一需要还能找回来。5.3 记忆之间互相矛盾比如早期存了“数据库用 MySQL”后来改成 PostgreSQL但旧记忆还在召回时两条都出来模型就懵了。我的处理方式是引入 supersedes 字段ALTER TABLE memories ADD COLUMN supersedes INTEGER;当新记忆和旧记忆冲突时把旧记忆标记为被取代检索时过滤掉。判断冲突可以靠 LLM 做一次比对也可以靠人工确认。这个机制不完美但能解决大部分问题。5.4 隐私与数据安全记忆库里会沉淀大量个人信息和项目细节安全不能忽视。我的做法数据库文件加密存储敏感信息如密钥、密码在抽取阶段就过滤掉prompt 里明确要求“不提取任何凭证类信息”定期备份但备份文件同样加密。这一点很多人会忽略等出事就晚了。记忆系统本质上是你的第二大脑它的安全性应该和你的密码管理器一个级别。5.5 性能优化什么时候该升级架构SQLite Chroma 的组合在个人使用场景下能撑很久。我实测下来记忆条数到 5 万条以内召回延迟都在可接受范围200ms 内。超过这个量级或者多人共享使用就该考虑升级向量库换 Qdrant 或 pgvector支持更好的并发关系库换 PostgreSQL支持更复杂的查询加一层缓存Redis缓存高频召回结果。但别过早优化。我见过太多人一上来就搭分布式结果个人项目根本用不上纯属给自己加运维负担。6. 几个我踩过的坑和实用心得先说抽取 prompt 的调优。我一开始写的 prompt 太宽松模型把“用户说你好”都当成记忆存了。后来加了明确的类型限制和忽略规则质量才上来。prompt 里的“忽略什么”比“提取什么”更重要这是反直觉但很关键的一点。再说 embedding 模型的选择。我试过用 OpenAI 的 embedding API效果确实好但成本和延迟都上去了。后来换成本地的all-MiniLM-L6-v2效果差一点点但速度快十倍成本为零。对于个人记忆系统本地模型是更务实的选择。还有一个细节记忆注入的位置。我试过放在 system prompt 里、放在对话开头、放在对话末尾实测下来放在 system prompt 里效果最稳。因为 system prompt 是模型的“底层设定”记忆作为背景知识放这里模型会更自然地把它当成已知事实而不是当前任务的一部分。最后分享一个扩展思路claude-mem这套架构其实不限于 Claude换成任何对话式模型都能用。核心是“抽取-存储-召回”这三板斧。你甚至可以把记忆层做成一个独立服务多个 AI 工具共享同一份记忆库那样你的所有 AI 助手就都能“认识你”了。这个方向我觉得挺有意思后续可以再展开聊聊。