news 2026/10/8 7:05:57

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
给Claude外挂记忆系统:从RAG到长期记忆的完整落地指南

1. 先搞清楚一个核心问题:大模型是真的需要记忆层

如果你做过几个基于 Claude 或同类大模型的应用,大概率会碰上同一个尴尬场景:用户上周刚在系统里交代过项目背景、个人偏好和待办事项,这周再打开对话,模型全忘了,又得从头解释一遍。

很多人第一反应是“把聊天记录全量塞进上下文”。这在早期原型阶段确实最省事,但上下文窗口再大也扛不住长期对话:一次会话塞几万字的历史记录,token 成本先不谈,模型在大量无关内容干扰下,指令跟随效果会肉眼可见地变差。我自己在接供应商客服机器人时实测过,把两天对话全量回放,回答质量反而比不带历史时更低。

claude-mem 这个方向解决的就是这类问题:给 Claude 外挂一个独立于模型权重之外的记忆系统。它会判断“哪些信息值得记住”“存到哪儿”“在需要时怎么捞回来”,让模型在每一轮对话中只看到跟当前问题高度相关的记忆片段。

说白了,记忆增强是 Retrieval-Augmented Generation(RAG)的一种特殊形态,只不过检索的对象从外部文档库换成了对话历史和用户画像。这个领域可玩的东西比想象中多,从向量检索到记忆冲突消解都有深入空间。

这篇文章会从方案选型、核心链路、完整实操代码到踩坑实录,把你把整条技术路线走通。适合正在做 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 └── .env

requirements.txt 里核心依赖如下:

anthropic>=0.28.0 chromadb>=0.5.0 openai>=1.35.0 sentence-transformers>=3.0.0 python-dotenv>=1.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( model="claude-3-5-sonnet-latest", max_tokens=1000, temperature=0.2, messages=[{"role": "user", "content": EXTRACTION_PROMPT.format(conversation_text=conversation_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_path="memories.db", collection_name="claude_mem"): self.conn = sqlite3.connect(db_path, check_same_thread=False) 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( name=collection_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_id

Chroma 的 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_exact=5, top_k_vector=20): 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_results=top_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(), key=lambda x: (x["importance"], x["source"] == "exact"), reverse=True) 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_thread=False我已经在代码里写了;并发写入场景建议把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 终于开始了解我了”的感觉。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/8 7:05:47

国产蓝牙耳机什么牌子质量好?2026年公认十大蓝牙耳机品牌排行榜

现在的蓝牙耳机市场卷得厉害&#xff0c;整体销量没涨反跌&#xff0c;开放式耳机却逆着大盘越卖越好&#xff0c;耳夹式更是少数还在正增长的品类。正因如此&#xff0c;拿它当日常主力的人越来越多——不用塞进耳道&#xff0c;戴一整天耳朵也不胀&#xff0c;周围动静照常能…

作者头像 李华
网站建设 2026/10/8 7:05:25

每天一个故事--姜文的电影风格(部分)

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录前言一、上期接续二、我的分析三、姜文总结前言 之前一直在寻找现在是什么&#xff0c;ai给我推荐了一些鲁迅的作品 一、上期接续 鲁迅的《故乡》和《风波》着重讲…

作者头像 李华
网站建设 2026/10/8 7:04:55

2027 届校招|金融产品经理面试,技术底子薄弱是否会被刷

一、核心结论 普通金融产品经理校招&#xff0c;技术背景弱不会成为一票否决项&#xff0c;但会影响面试加分&#xff1b;偏底层系统、风控技术、数据类金融产品岗位&#xff0c;技术理解不足会显著降低通过率。 从 2026 年各大企业校招官网、牛客网、应届生求职网发布的金融产…

作者头像 李华
网站建设 2026/10/8 7:04:40

柔性电路板(FPC)补强:材料选型、工艺与设计全解

柔性电路板&#xff08;FPC&#xff09;以轻薄、可弯折、高密度布线的特性&#xff0c;成为消费电子、汽车、医疗、工控与穿戴设备的核心互联载体。但柔性即意味着局部刚性不足&#xff1a;SMT 贴片易翘曲、连接器插拔易变形、反复弯折易断线、装配定位易偏移。FPC 补强正是通过…

作者头像 李华
网站建设 2026/10/8 7:04:06

OpenShell全面指南:Windows 11经典开始菜单替代与效率提升

如果你还在为 Windows 11 那个居中、图标化、点开还要翻一层的开始菜单头疼&#xff0c;那你大概率听说过 OpenShell。它是经典开源项目 Classic Shell 的延续&#xff0c;目的一直没变&#xff1a;把干净、高效、能一次点到二级菜单的经典开始菜单还给你。作为一个从 Win7 时代…

作者头像 李华