ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

给Claude外挂记忆系统:从RAG到长期记忆的完整落地指南

给Claude外挂记忆系统:从RAG到长期记忆的完整落地指南 1. 先搞清楚一个核心问题大模型是真的需要记忆层如果你做过几个基于 Claude 或同类大模型的应用大概率会碰上同一个尴尬场景用户上周刚在系统里交代过项目背景、个人偏好和待办事项这周再打开对话模型全忘了又得从头解释一遍。很多人第一反应是“把聊天记录全量塞进上下文”。这在早期原型阶段确实最省事但上下文窗口再大也扛不住长期对话一次会话塞几万字的历史记录token 成本先不谈模型在大量无关内容干扰下指令跟随效果会肉眼可见地变差。我自己在接供应商客服机器人时实测过把两天对话全量回放回答质量反而比不带历史时更低。claude-mem 这个方向解决的就是这类问题给 Claude 外挂一个独立于模型权重之外的记忆系统。它会判断“哪些信息值得记住”“存到哪儿”“在需要时怎么捞回来”让模型在每一轮对话中只看到跟当前问题高度相关的记忆片段。说白了记忆增强是 Retrieval-Augmented GenerationRAG的一种特殊形态只不过检索的对象从外部文档库换成了对话历史和用户画像。这个领域可玩的东西比想象中多从向量检索到记忆冲突消解都有深入空间。这篇文章会从方案选型、核心链路、完整实操代码到踩坑实录把你把整条技术路线走通。适合正在做 AI 客服、AI 助手或个性化对话应用的开发者也适合想深入理解 RAG 记忆机制的产品技术同学。2. 记忆子系统到底拆成哪几块2.1 无障碍理解记忆的三层结构给大模型做记忆本质上是在解决一个工程问题数据的生命周期管理。这里有三个层级短期记忆对应当前会话内的上下文一般直接靠模型上下文窗口承载不需要额外存储逻辑。长期记忆则是跨会话保留下来的关键信息比如用户姓名、产品偏好、历史决策记录。工作记忆是夹在中间的一层——每次请求到来时我们从长期记忆里检索出相关片段临时拼装成一份精简版上下文喂给模型。实际上这三层正好对应了 OpenAI 内部提出过的记忆分区思路核心记忆长期不变的事实、工作记忆当前任务相关和会话记忆即时上下文。做记忆系统时给三类数据分配不同的存储和召回策略比一刀切塞进向量库要靠谱得多。2.2 为什么不能只靠向量数据库现在很多团队一上来就搭 Chroma、Pinecone把所有东西都向量化。没有说你错但单位向量库解决不了的问题很多。首先对话历史的记忆跟文档知识库不一样它有很强的时间线和因果关系。用户周一说要 A 方案周三改成了 B 方案如果你只做向量召回很可能把已经被推翻的 A 方案也捞出来造成记忆冲突。其次用户的偏好是动态演化的同一份记忆在不同时间点的置信度不一样单纯按相似度排序很难体现这种衰减。所以成熟的记忆系统通常至少包含两套检索入口向量检索负责语义联想结构化查询负责精确匹配比如按用户 ID 拉取所有偏好配置。必要时还会加一层时间衰减权重让旧记忆的优先级自然下降。2.3 记忆也不是存进去就完事还有一个很多教程不怎么提的点记忆写入前的筛选。对话里 90% 的内容都是一次性寒暄和上下文噪声真正值得长期保存的可能只有几行。如果全盘保存短期看不出问题一到记忆量过万条检索噪声会淹没有效信号召回精度断崖式下跌。所以在我的项目里记忆写入前会过一个轻量筛选让 Claude 自己判断这段对话里有没有“跨会话仍然有价值”的信息有则提取成结构化的记忆条目没有就直接丢弃。这个环节只消耗一次额外的模型调用成本极低但能把后面的检索精度拉高一个档次。3. 核心链路原理从文本到向量再到可检索记忆3.1 嵌入模型怎么选记忆系统的核心链路是文本切片 → 向量化 → 混合检索 → 重排序 → 上下文组装。每一步都有讲究。先说嵌入模型。我实验过 OpenAI 的 text-embedding-3-small、text-embedding-3-large也试过开源的 bge-large-zh、m3e 系列。给我的经验是不要只看 MTEB 榜单一定要拿你自己的记忆语料跑一轮相似度验证。中文对话场景下text-embedding-3-small 在差不多的效果下能把价格和延迟压到很低但如果你完全离线部署bge-m3 也相当能打。嵌入维度和 chunk 大小是配套的如果每条记忆本身是短文本比如一两句话可以直接整条向量化不需要切割但如果是从完整对话里抽取的段落建议按语义边界切成 200 到 500 字的小段理由后面在踩坑部分详细说。3.2 向量入库和相似度检索引擎向量库这块我踩过的坑比较多。Chroma 最轻量适合本地开发和教学Qdrant 和 Milvus 更适合生产环境支持复杂的过滤条件和规模扩展。如果只是个人项目或者企业内部工具Chroma 足够不用一上来就上分布式搜索集群。检索时别有执念只用向量相似度会漏掉很多精确信息。我常用的方式是先做一轮关键词/精确匹配比如用户明确提到“我的生日是 5 月 12 日”再做一轮向量语义匹配最后把两个结果集合并去重。关键词匹配用倒排索引就能完成不需要额外引入 Elasticsearch——在记忆量不超过几十万条的规模下SQLite 的 FTS 模块就够用了。3.3 重排序和上下文组装才是精度关键召回之后直接拼给模型的方案精确率通常不够看。因为嵌入模型的相似度排序跟 GPT/Claude 的语义相关性理解不完全一致top 3 里面可能混着一两条看似向量近但实际没用的旧记忆。我现在的做法是第一轮向量检索召回 20 条候选然后交给我在代码中接入的重排序模型如 bge-reranker或者直接让 Claude 做一次轻量筛选压缩到 3 到 5 条再拼入上下文。多这一步回答质量明显提升尤其当用户问题掺杂了多个记忆点时效果差异是肉眼可见的。上下文组装的格式也有讲究。不要让模型自己去猜哪些是记忆、哪些是即时对话。我习惯在拼装时加上清晰的标记比如memories标签包住记忆片段并在提示词里注明“以下是用户的历史记忆可能与当前问题相关但请以当前对话中的最新信息为准”。这个措辞很重要——它能在引入记忆的同时避免模型被过期记忆带偏。4. 实操用 Python 给 Claude 外挂一个记忆系统4.1 系统配置与目录结构接下来进入实操环节。我会用 Python Anthropic SDK Chroma SQLite 实现一套最小可用的记忆增强 Claude 对话系统完整代码我会拆成模块来讲。整体目录建议这样组织claude-mem/ ├── app.py # 主对话入口 ├── memory/ │ ├── __init__.py │ ├── store.py # 记忆存取向量库 SQLite │ ├── extractor.py # 从对话中抽取可记忆信息 │ ├── retriever.py # 混合检索 重排 │ └── builder.py # 组装带记忆的上下文 ├── requirements.txt └── .envrequirements.txt 里核心依赖如下anthropic0.28.0 chromadb0.5.0 openai1.35.0 sentence-transformers3.0.0 python-dotenv1.0.0这个项目里我同时用了两套嵌入来源默认走 OpenAI 的 text-embedding-3-small也可切换成通义千问的 text-embedding-v3两者接口相似改起来不麻烦。离线场景再用 sentence-transformers 加载本地模型。后面代码里我会把嵌入封装成一个独立函数方便替换。4.2 记忆来源让 Claude 自己判断值得记什么记忆提取是整个系统的灵魂。与其靠规则去解析对话文本不如直接让模型自己判断“这段对话里有没有值得保存的信息”。具体做法是构造一个独立的提取请求不属于主对话流用户无感知# memory/extractor.py import os from anthropic import Anthropic client Anthropic() EXTRACTION_PROMPT 你是一名对话记忆管理员。请阅读下面这段对话提取其中“在未来的对话中仍然有参考价值”的信息。 有价值的信息包括用户明确的身份信息、偏好与禁忌、长期目标、重要决策与原因、关键事实。 不包括一次性的寒暄、临时任务指令完成即失效、无实际意义的闲聊。 请按以下 JSON 结构输出不要输出任何其他文字 {{memories: [{{type: fact|preference|decision|goal, content: 简洁准确的表述, importance: 1-5}}]}} 如果没有任何值得记忆的信息返回 {{memories: []}}。 对话内容 conversation {conversation_text} /conversation def extract_memories(conversation_text: str, user_id: str) - list[dict]: response client.messages.create( modelclaude-3-5-sonnet-latest, max_tokens1000, temperature0.2, messages[{role: user, content: EXTRACTION_PROMPT.format(conversation_textconversation_text)}] ) # 此处应做 JSON 解析和异常处理省略 return parsed.get(memories, [])这里有几个细节值得说温度设成 0.2 而不是 0稍微留一点自由度避免模型反复输出同一套模板importance 参是我后加的1 到 5 分5 最高。它会在后续检索权重里起作用——高 importance 的记忆在排序时天然占优。type 字段也非常有用偏好类记忆和事实类记忆在召回时可以走不同的阈值比如偏好信息要求更精确才注入。对话文本需要我自己做截断通常只取最近的一轮完整“用户提问 助手回复”作为提取输入最多不超过 2000 字。太长反而会导致提取精度下降。4.3 存储层向量库和 SQLite 为什么要同时用存数据时我会同时写入到 SQLite 和 Chroma。SQLite 存结构化字段用户 ID、类型、时间、重要性、原始文本Chroma 存向量索引和 metadata 中的条目 ID。两者通过记忆条目的唯一 ID 关联保证删改同步。store.py 核心逻辑很直白# memory/store.py import sqlite3, uuid, chromadb class MemoryStore: def __init__(self, db_pathmemories.db, collection_nameclaude_mem): self.conn sqlite3.connect(db_path, check_same_threadFalse) self.conn.execute( CREATE TABLE IF NOT EXISTS memories ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL, type TEXT, content TEXT, importance INTEGER, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ) self.chroma_client chromadb.PersistentClient(path./chroma_db) self.collection self.chroma_client.get_or_create_collection( namecollection_name, metadata{hnsw:space: cosine} ) def add_memory(self, user_id: str, mem_type: str, content: str, importance: int, embedding: list): mem_id str(uuid.uuid4()) self.conn.execute( INSERT INTO memories (id, user_id, type, content, importance) VALUES (?,?,?,?,?), (mem_id, user_id, mem_type, content, importance)) self.conn.commit() # chroma 的 metadata 尽量少放内容只放 id 和排序需要的信息 self.collection.add(ids[mem_id], embeddings[embedding], metadatas[{user_id: user_id, importance: importance}]) return mem_idChroma 的 metadata 字段我只放 user_id 和 importance。直接把 content 也塞进去当然可以但 Chroma 的 metadata 是 KV 存储查询过滤时会有额外开销。纯文本内容反正存在 SQLite 里向量检索出 ID 后再回表拿正文即可。4.4 检索层混合策略和水位线检索层的核心函数是 retrieve_memories同时跑精确匹配和向量匹配。这里我要强调一个不算高级但非常实用的坑中技巧给向量检索的结果设置一个相似度水位线。低于这个分数的记忆宁可不注入因为被噪声记忆干扰模型回答的代价远高于“模型没想起这件事”的代价。# memory/retriever.py import numpy as np from typing import Optional SIMILARITY_THRESHOLD 0.32 # 低于该阈值的记忆不注入按向量库实测调整 def retrieve_memories(query_text: str, user_id: str, store, top_k_exact5, top_k_vector20): memories {} # 1. 精确匹配按关键词和历史事实检索 SQLite cur store.conn.execute( SELECT id, type, content, importance FROM memories WHERE user_id? AND (content LIKE ?) ORDER BY importance DESC, created_at DESC LIMIT ?, (user_id, f%{query_text[:6]}%, top_k_exact)) for row in cur.fetchall(): memories[row[0]] {type: row[1], content: row[2], importance: row[3], source: exact} # 2. 向量匹配按语义召回候选 emb get_embedding(query_text) # get_embedding 封装了嵌入模型的调用 results store.collection.query( query_embeddings[emb], n_resultstop_k_vector, where{user_id: user_id}, include[metadatas, distances] ) for idx, mem_id in enumerate(results[ids][0]): dist results[distances][0][idx] if dist SIMILARITY_THRESHOLD: # cosmic distance 注意与相似度的换算 continue meta results[metadatas][0][idx] # 回表 SQLite 拿完整内容 row store.conn.execute(SELECT type, content, importance FROM memories WHERE id?, (mem_id,)).fetchone() if row: memories[mem_id] {type: row[0], content: row[1], importance: row[2], source: vector} # 3. 依据 importance 加权相同 importance 下优先精确匹配 sorted_memes sorted(memories.values(), keylambda x: (x[importance], x[source] exact), reverseTrue) return sorted_memes[:5]注意Chroma 返回的是distances。我用 cosine 空间时Chroma 的 distance 和 cosine similarity 的关系是distance 1 - similarity。所以判断时的阈值这里已经换算过来了。不同版本 API 有差异报错时先把返回字段打印出来看一眼。4.5 组装上下文带记忆的最终对话请求最后就是 builder.py 中组装将检索到的记忆传给 Claude 生成回复# memory/builder.py def build_messages(user_id, user_input, store): memories retrieve_memories(user_input, user_id, store) memory_block 以下是用户的部分历史记忆\n if memories else 暂无相关历史记忆。\n for m in memories: memory_block f- [{m[type]}] {m[content]}\n memory_block 如果记忆与当前问题无关请忽略。当前对话内容始终优先。 system_prompt f你是一位有着良好长期记忆的智能助理。 {memory_block} 请结合记忆和当前对话给出自然、准确的回答。 return [{role: user, content: user_input}], system_prompt实际请求时system 和 user 消息分开传。为什么既要把记忆放 system 又要做成独立段落因为 Claude 的 system prompt 和 user 消息的权重处理方式存在差异过量的记忆塞进 user 消息会稀释当前问题的重要性。这种通过 system 注入、带明确指引的格式从我实验中效果最稳定。整个流程串起来只有几十行代码拿到对话历史 → 提取记忆 → 入库 → 下次请求检索 → 组装 → 让 Claude 回答。这套结构可以无缝嵌进 FastAPI 或 Flask 服务里。5. 跑起来之后你一定会遇到的坑5.1 two type 问题对话撞车和记忆串味我在真实项目里遇到最多的坑是多个用户共用一个向量集合时语义相近的对话会互相污染检索结果。有一次客服机器人上线内测用户 A 和用户 B 都问了“你们能开发小程序吗”结果 A 的记忆里包含了“我公司是做医疗器械的”B 也拿到了这条记忆回答时差点默认 B 也是医疗器械公司。原因就是检索时忘了做 user_id 过滤。解决方案有两种给每个用户建独立的 Chroma collection适合用户量不大但 collection 数量一多管理混乱或者在同一个 collection 中用 where 条件严格过滤 user_id更通用推荐。上面的代码里我已经加了 where 过滤实际开发时务必确认这一点。5.2 上下文被记忆撑爆的问题有的同学会把 top_k 设成 20再把每条记忆不加裁剪地全量拼进上下文长对话场景下立马 token 暴涨、账单吓人。我的策略是检索召回后先做截断。每条记忆在存储时本身就是精简的提取阶段模型已经压缩过正常情况下不会超过 100 字。如果每条记忆超过 200 字写入前我还做了二次摘要。Top_k 在向量召回阶段可以放宽到 20~30保证召回率但在注入阶段经过重排和重要性加权后只保留 3~5 条。这个数字按业务场景调但一般不要超过 8 条。5.3 旧记忆跟新事实冲突记忆冲突是长期运行的对话系统里最微妙的坑。用户上个月说“我在北京工作”这个月说“我搬到上海了”两条记忆如果同时被召回Claude 就会犯迷糊。我用的方案是延迟决策结构存储的每条记忆自带 created_at检索时优先取最新一条同类型记忆如果两条同类型记忆在内容上有明显矛盾旧的那条自动降权。同时在组装提示词里明确写了“以当前对话中的最新信息为准”把最终裁决权交给模型。这个思路不完美但成本低实测能解决大部分冲突场景。升级版方案是记忆合并在提取阶段检测到新旧记忆高度相关时让模型把旧记忆更新为新表述而不是追加新条目。这相当于给记忆系统做了一个原地更新操作能有效阻止冲突累积。5.4 嵌入模型和检索工具的语言冷热不均如果你的对话是中文为主嵌入模型一定要选中文友好的。我最早用一个小型多语言模型嵌入结果对中文短句的区分度极差“我今天想吃酸的”和“我最近胃不舒服”向量距离太近召回全是错的。后来换成 bge-m3 或 text-embedding-3-small短文本的区分度才明显好转。选嵌入模型时拿 50 组短句对做一轮人工评估非常有必要——跑一轮完整标注线可能只要半小时但能帮你避开后续无穷无尽的调参。5.5 SQLite 文件权限与并发锁部署到服务端时有个常见问题SQLite 文件所在的目录没有写权限或者 Flask 多线程下并发写导致database is locked。解决办法SQLite 连接加check_same_threadFalse我已经在代码里写了并发写入场景建议把timeout参数加上或者直接换用 PostgreSQL。对于单机内部工具和小团队产品SQLite 其实足够但别忘记定期备份.db文件它能让你少走很多弯路。6. 再往前一步记忆分层与遗忘机制6.1 给记忆系统引入“遗忘”这个核心概念人类记忆最重要的特性不是容量大而是会忘。一个好的记忆系统也应该懂得遗忘否则沉淀下去的信息会越来越多噪声变大精度下降。我在项目里实现了一个很简单的衰减机制每条记忆在 SQLite 中有一个last_access_at字段每次被成功召回命中就更新一次。后台定时任务每天检查一次对满足下面任一条件的记忆做降权或归档连续 60 天未被召回且 importance 3已被更新的同类型记忆覆盖用户明确表达过“之前的信息不要了”这个机制虽然初级但管理长期对话系统的效果远超预期。它让记忆库保持在一个相对干净、较高的信噪比状态检索精度不会随着运行时间线性下降。6.2 让 Claude 自己管理记忆的优先级分层更进阶的做法是让模型参与记忆管理。我的提取器只做“是否值得记录”的判断另一个定期运行的“记忆整理”流程会让 Claude 对大量低 importance 记忆做汇总和压缩。这个思路是每周把用户一周的零散记忆交给 Claude让它归纳成几条高层次的偏好或画像信息然后合并同类项、删除已经被覆盖的信息。运行一次的花费通常不到几毛钱但对长期记忆系统的质量提升非常显著。6.3 从 claude-mem 到 Agent Memory 的延伸如果你把 claude-mem 的实体切换成 Agent而不是人这套系统同样适用。Agent 在多个任务中积累的经验、偏好和决策记录同样需要分层记忆和可信度评估。比如一个自动写周报的 Agent它可以记住用户对周报格式的偏好“喜欢数字多一点的风格”“每周先列结论再列明细”。这些偏好一旦进入长期记忆Agent 的输出质量是几何级提升的而不是每次回答都像一个刚上岗的新人。我目前打算做的多 Agent 记忆共享体系是 Memory 层的进一步抽象化为不同 Agent 提供统一的 RAG 记忆接口特定 Agent 既能读写自己的私有记忆也能读取团队级的共享经验库。配对实现这对 claude-mem 来说是一个很自然的演进方向。从最开始的全量回放到现在的提取、分层、检索、遗忘这套记忆系统的改动量不算大但带来的体验变化是全方位的。如果你正在做基于 Claude 的对话应用花一个周末把记忆层搭起来应该是我近半年最值得的一次时间投入——Embedding 跑起来的那一刻你就能立即体会到“这个 AI 终于开始了解我了”的感觉。
返回列表