基于LangChain与向量数据库构建AI长期记忆系统
1. 项目概述为什么AI需要“长期记忆”最近在折腾本地大模型发现一个挺普遍的问题每次和AI对话它都像得了“健忘症”。你告诉它你喜欢喝美式咖啡讨厌香菜习惯用Markdown写文档但下一次聊天它又得从头问起。这种“金鱼脑”式的交互体验严重限制了AI作为个人助理或深度伙伴的潜力。我们真正需要的是一个能记住我们习惯、偏好和过往对话上下文的智能体也就是所谓的“长期记忆”。这个项目的核心就是利用Langchain这个强大的框架为本地部署的大模型比如Qwen、Llama等构建一套持久化的记忆系统。它不再是简单的“本次对话历史”而是一个可以跨会话、跨时间存储和检索关键信息的“外部大脑”。想象一下你告诉AI助手“我下周要去北京出差”几天后你问“我之前的行程安排是什么”它能准确回忆起来并基于此给出建议。这才是真正有价值的智能交互。实现这个目标技术上主要解决两个问题“记什么”和“怎么记”。“记什么”涉及记忆的提取与抽象我们不能把每句聊天记录都存下来那太臃肿了。“怎么记”则关乎存储、检索和与模型的集成。这正是Langchain大显身手的地方。它提供了一套标准化的组件让我们能像搭积木一样构建出符合自己需求的记忆模块。无论是简单的对话历史缓存还是基于向量数据库的语义记忆检索都能高效实现。2. 核心思路与架构设计2.1 记忆系统的分类与选型在动手之前得先想清楚我们需要什么样的记忆。Langchain社区和实践中通常将记忆分为几种类型每种适合不同的场景对话缓冲区ConversationBufferMemory最简单直接就是把整个对话历史或最近N轮的原始文本保存下来每次提问时连同历史一起发给模型。优点是信息无损缺点也很明显上下文长度有限受模型Token限制且无关信息会干扰模型成本也高。适合短小、连贯的对话场景。对话摘要缓冲区ConversationSummaryMemory这是对缓冲区的一个智能升级。它不会保存所有原始对话而是定期或按需让大模型对之前的对话内容进行总结只保存这个摘要。下次交互时提供的是摘要而非冗长的历史。这大大节省了Token也能保留核心信息。但缺点是多了一步总结的调用可能丢失细节且总结的质量依赖模型能力。对话知识图谱ConversationKGMemory这是一种更结构化的记忆方式。它利用大模型或专门工具从对话中提取实体人、物、地点和关系喜欢、讨厌、计划构建成一个知识图谱。查询时可以根据实体关系进行检索。这种方式记忆精度高便于推理但实现相对复杂对提取的准确性要求高。向量存储记忆VectorStoreRetrieverMemory这是本项目重点采用也是目前最灵活、强大的长期记忆方案。其核心思想是将对话中产生的“记忆片段”例如用户陈述的事实、偏好、计划等转换成向量Embedding存储到向量数据库如Chroma, FAISS, Pinecone中。当需要回忆时将当前问题也转换成向量在数据库中进行相似性搜索找出最相关的记忆片段作为上下文喂给模型。这种方式支持海量记忆存储且能实现语义级别的相似性检索非常智能。为什么选择向量存储记忆对于“记住用户喜好”这个目标用户的陈述往往是分散在多轮对话中的。比如用户可能在周一提到“我喜欢科幻电影”在周三提到“我最爱的导演是诺兰”。传统的缓冲区很难高效地从大量历史中精准找出这两条信息。而向量存储记忆通过语义检索当你问“推荐一部电影”时它能同时召回“科幻”和“诺兰”这两条相关记忆从而给出“可以看看诺兰的《星际穿越》”这样精准的建议。它实现了记忆的“按需、精准”读取是构建个性化AI的基石。2.2 基于Langchain的系统架构确定了核心记忆方案整个系统的架构就清晰了。下图展示了数据流的核心路径graph TD A[用户输入新问题] -- B[记忆检索器]; C[向量数据库br/存储历史记忆片段] -- B; B -- D[组合当前问题相关记忆]; D -- E[本地大模型]; E -- F[生成回答]; F -- G[记忆生成器]; G -- C;核心流程解读记忆存储写入当用户与AI完成一轮有价值的对话后例如用户表达了明确的偏好或事实系统不会简单丢弃。一个独立的“记忆生成器”模块通常由另一个LLM调用驱动会分析这轮对话提取出需要长期保存的“记忆片段”。例如从“我讨厌下雨天”中提取出(“用户”, “讨厌”, “下雨天”)这样的三元组或一段自然语言描述。接着将这个片段通过文本嵌入模型Embedding Model转化为高维向量最后存入向量数据库。这个过程可以是实时的也可以是定期批量处理。记忆检索读取当用户提出一个新问题时例如“周末天气不好怎么办”系统首先将这个问题同样转化为向量。然后用这个“问题向量”在向量数据库中进行相似度搜索如余弦相似度找出K个最相关的“记忆向量”及其对应的原始文本片段。这些被检索出来的记忆片段就是模型回答问题的“长期记忆上下文”。上下文组合与推理检索到的记忆片段和当前用户问题被共同组合成一个新的提示Prompt发送给本地大模型。Prompt的模板可能类似“以下是关于用户的一些已知信息{记忆片段}。请基于这些信息回答当前问题{用户问题}”。模型在生成回答时就会将这些记忆作为已知事实来参考。闭环与更新模型生成的回答如果其中包含了新的、值得长期记忆的信息可能由模型自己判断或根据规则触发可以再次进入“记忆生成”流程更新数据库。这样就形成了一个不断丰富、演进的个人记忆库。技术栈选择本地大模型Ollama运行Llama 3.2、Qwen等、LM Studio、或直接调用本地部署的模型API如vLLM。选择取决于你的硬件显存/内存和模型偏好。Langchain作为编排框架其Memory、Retriever、Chains模块是本项目的粘合剂。向量数据库轻量级首选ChromaDB纯Python易嵌入追求性能可选FAISSFacebook开源适合本地若考虑云服务可用Pinecone。本项目以本地部署为主推荐Chroma。嵌入模型同样需要本地化。推荐BAAI/bge-small-zh-v1.5或thenlper/gte-small这类轻量级且对中文支持好的开源模型可以用SentenceTransformers库运行。3. 环境准备与核心工具部署3.1 基础Python环境与Langchain安装首先确保有一个干净的Python环境3.8以上避免包冲突。建议使用conda或venv。# 创建并激活虚拟环境 conda create -n ai-memory python3.10 conda activate ai-memory # 安装核心库 pip install langchain langchain-community langchain-core # Langchain是一个模块化框架langchain是核心-community包含很多第三方集成-core是基础接口。注意Langchain版本迭代较快API可能有变动。如果遇到问题可以尝试指定稍早的稳定版本如pip install langchain0.1.0。但建议优先查阅最新官方文档。3.2 本地大模型服务搭建以Ollama为例要让Langchain调用本地模型我们需要一个“模型服务”。Ollama是目前最方便的工具之一它简化了本地大模型的下载、运行和API暴露。安装Ollama前往Ollama官网下载对应操作系统的安装包并安装。拉取并运行模型在终端执行以下命令拉取一个适合你硬件的中小模型。例如对于8GB显存可以尝试qwen2.5:7b或llama3.2:3b。# 拉取模型 ollama pull qwen2.5:7b # 运行模型Ollama会在本地11434端口启动API服务 ollama run qwen2.5:7b验证服务模型运行后打开浏览器访问http://localhost:11434应该能看到Ollama的API界面。更直接的测试是用curlcurl http://localhost:11434/api/generate -d { model: qwen2.5:7b, prompt: Hello }看到返回的JSON流说明模型服务正常。硬件门槛与模型选择心得 标题里提到了“64GB内存32GB显存”这属于高端配置可以流畅运行70B甚至更大参数的模型。但对于“记忆”这个任务模型的理解和指令跟随能力比规模更重要。实测下来一个7B参数的好模型如Qwen2.5-7B、Llama-3.2-3B在清晰的Prompt指导下完全能胜任记忆的提取和利用任务。优先选择在评测中表现好的小模型而不是盲目追求大参数。3.3 向量数据库与嵌入模型部署接下来部署记忆的“仓库”——向量数据库以及把文本变成向量的“编码器”——嵌入模型。安装ChromaDBpip install chromadbChromaDB默认使用all-MiniLM-L6-v2作为嵌入模型但它是英文倾向的。为了更好的中文记忆效果我们需要替换为中文优化的模型。部署中文嵌入模型 我们使用SentenceTransformers来加载本地嵌入模型。pip install sentence-transformers在代码中我们可以这样初始化和使用它from langchain.embeddings import HuggingFaceEmbeddings # 加载中文嵌入模型首次运行会自动下载模型文件 embed_model HuggingFaceEmbeddings( model_nameBAAI/bge-small-zh-v1.5, # 中文小模型效果不错 model_kwargs{device: cpu}, # 如果没有GPU就用cpu。有GPU可改为cuda encode_kwargs{normalize_embeddings: True} # 归一化有利于相似度计算 ) # 测试一下 text 我喜欢编程和咖啡 vector embed_model.embed_query(text) print(f向量维度{len(vector)})为什么选择BGE模型BGEBAAI General Embedding是智源研究院开源的系列嵌入模型其中文训练数据丰富在中文语义相似度任务上表现显著优于同等规模的通用模型如OpenAI的text-embedding-ada-002的平替。bge-small-zh-v1.5只有100多MB在CPU上也能快速推理是本地化项目的理想选择。4. 分步实现长期记忆系统现在我们将把各个组件串联起来构建一个完整的、具有长期记忆的对话链。4.1 步骤一初始化核心组件首先我们创建所有必要的工具实例。import os from langchain.chains import ConversationChain from langchain.memory import VectorStoreRetrieverMemory from langchain_community.vectorstores import Chroma from langchain_community.embeddings import HuggingFaceEmbeddings from langchain_community.llms import Ollama from langchain.prompts import PromptTemplate # 1. 初始化本地大模型通过Ollama # 确保Ollama服务正在运行并且你已拉取对应模型 llm Ollama( base_urlhttp://localhost:11434, # Ollama API地址 modelqwen2.5:7b, # 你本地运行的模型名 temperature0.2, # 温度调低让输出更稳定、更基于记忆 ) # 2. 初始化中文嵌入模型 embeddings HuggingFaceEmbeddings( model_nameBAAI/bge-small-zh-v1.5, model_kwargs{device: cpu}, encode_kwargs{normalize_embeddings: True} ) # 3. 初始化Chroma向量数据库并指定嵌入模型 # persist_directory 指定数据持久化目录这样记忆才能跨会话保存 persist_dir ./chroma_memory_db vectorstore Chroma( collection_nameuser_preference_memory, embedding_functionembeddings, persist_directorypersist_dir ) # 4. 创建检索器从向量库中查找最相关的K条记忆 retriever vectorstore.as_retriever(search_kwargs{k: 3}) # 每次检索3条最相关的记忆 # 5. 创建VectorStoreRetrieverMemory这是Langchain中连接检索器和对话链的桥梁 memory VectorStoreRetrieverMemory( retrieverretriever, memory_keychat_history, # 在Prompt中这些记忆将被放入这个变量 input_keyinput # 指定用户输入对应的变量名 )关键参数解析search_kwargs{k: 3}这个k值至关重要。它决定了每次回忆时给模型提供多少条过去的记忆。太少了可能信息不全太多了可能引入噪声。从3开始调整是个好选择。memory_key这个名称会在后续的Prompt模板中被引用用于放置检索到的记忆文本。persist_directory务必设置。不设置的话Chroma默认使用内存模式程序关闭后所有记忆都会丢失。4.2 步骤二设计智能Prompt模板Prompt是引导模型正确使用记忆的“说明书”。一个糟糕的Prompt会让模型无视你的记忆。# 定义一个包含记忆上下文的Prompt模板 template 你是一个拥有长期记忆的AI助手。以下是从我们过往对话中提取的关于用户的一些信息 {chat_history} 请严格依据以上信息如果相关并结合你的通用知识回答用户的最新问题。 如果以上信息与问题无关你可以忽略它们直接根据你的知识回答。 当前对话 用户{input} 助手 PROMPT PromptTemplate( input_variables[chat_history, input], # 声明模板中的变量 templatetemplate )Prompt设计心得 这个模板做了几件关键事明确指令开头就告诉模型“你拥有长期记忆”并指出下方是过往信息。划定范围“严格依据以上信息如果相关”这句话很重要它鼓励模型优先使用记忆但也给了它“无关则忽略”的灵活性防止记忆错误时强行关联导致胡言乱语。清晰的结构将{chat_history}和{input}分开符合模型处理上下文的习惯。你可以在{chat_history}前加上“### 历史记忆 ###”这样的分隔符效果可能更好。4.3 步骤三组装对话链并进行测试现在把模型、记忆和Prompt组装成一个完整的对话链。# 创建对话链 conversation ConversationChain( llmllm, promptPROMPT, memorymemory, verboseTrue # 设为True可以看到链的详细执行过程调试时非常有用 ) # 进行第一轮对话注入初始记忆 print(--- 第一轮注入喜好 ---) response1 conversation.predict(input嘿记住一下我最喜欢的编程语言是Python最讨厌的食物是香菜。) print(f助手{response1}\n) # 进行第二轮对话测试记忆检索 print(--- 第二轮测试记忆 ---) response2 conversation.predict(input你能推荐一个适合Python初学者的项目吗) print(f助手{response2}\n) # 进行第三轮对话测试跨会话记忆模拟新启动程序 print(\n--- 模拟新会话开始 ---) # 注意因为我们使用了persist_directory向量库是持久化的。 # 重新初始化对话链但使用同一个persist_dir记忆应该还在。 # 在实际应用中你需要重新运行初始化代码但加载已有的vectorstore。 # 这里为了演示我们直接问一个需要结合两段记忆的问题。 response3 conversation.predict(input我最近心情不好有什么能让我开心的建议吗比如吃的或者活动) print(f助手{response3})预期效果与解读第一轮后关于“喜欢Python”和“讨厌香菜”的陈述会被memory对象自动保存到向量数据库中吗不会。这是一个常见的误区。VectorStoreRetrieverMemory默认只在predict时进行检索读操作不会自动执行保存写操作。我们需要手动或通过其他机制来添加记忆。所以上述测试中第二轮和第三轮对话很可能检索不到第一轮的信息。这引出了下一个关键步骤如何向记忆库中写入内容。4.4 步骤四实现记忆的写入与维护我们需要一个独立的流程来“制造”记忆片段并存入向量库。通常这需要另一个LLM调用或一套规则来分析对话判断哪些是值得保存的长期事实。方案一手动添加记忆用于测试和简单规则对于结构清晰的偏好陈述我们可以用规则提取。def add_memory_manually(text): 手动添加一段文本作为记忆 # 直接将要记忆的文本添加到向量库 vectorstore.add_texts(texts[text]) print(f已添加记忆{text}) # 在第一轮对话后手动添加 add_memory_manually(用户最喜欢的编程语言是Python。) add_memory_manually(用户最讨厌的食物是香菜。)方案二使用LLM自动提取记忆推荐更通用的方法是在每轮对话后让另一个LLM可以是同一个模型判断用户输入中是否包含值得长期记忆的“事实”并将其总结成简洁的陈述句。from langchain.schema import Document def extract_and_save_memory(user_input, ai_output, llm_for_extraction): 使用LLM从对话中提取记忆并保存 extraction_prompt f 请从以下对话中提取出关于用户的、值得长期记住的客观事实或明确偏好。 以简洁的陈述句形式输出每条事实占一行。如果没有任何值得记忆的内容输出“无”。 用户说{user_input} 助手说{ai_output} 提取出的记忆 # 调用一个LLM来执行提取任务。这里为了简化使用同一个Ollama实例但可以单独配置。 extracted_text llm_for_extraction.predict(extraction_prompt) if extracted_text.strip() ! 无: memories [line.strip() for line in extracted_text.split(\n) if line.strip()] for memory in memories: # 将提取的记忆封装为Document对象这是向量库的标准输入格式 doc Document(page_contentmemory, metadata{source: auto_extracted}) vectorstore.add_documents(documents[doc]) print(f自动提取并保存记忆{memory}) else: print(未提取到值得记忆的内容。) # 整合到对话流程中 print(--- 带自动记忆提取的对话 ---) user_input 我觉得夏天去海边度假最舒服尤其喜欢青岛。 ai_response conversation.predict(inputuser_input) print(f助手{ai_response}) # 对话后自动提取记忆 extract_and_save_memory(user_input, ai_response, llm)记忆维护的注意事项记忆去重简单的向量检索无法避免存储高度相似的记忆。可以在存入前先用新记忆的向量在库中搜索一下如果相似度超过某个阈值如0.95则选择更新原有记忆而非新增。记忆更新与失效用户的喜好会变。目前没有完美的自动机制。一种策略是为每条记忆添加时间戳和置信度元数据。检索时可以考虑时间权重越近的记忆可能越相关或者设计一个手动“忘记”的指令让用户来触发记忆的删除或修正。元数据利用在Document的metadata字段中存储额外信息非常有用例如{type: food_preference, valence: negative, date: 2023-10-27}。这样在检索时可以结合元数据进行过滤比如“只检索关于食物偏好的负面记忆”。5. 高级优化与实战技巧基础系统搭建完成后我们可以从以下几个方向进行优化提升记忆系统的准确性和实用性。5.1 提升记忆检索的相关性默认的向量检索是“语义相似度”检索。但“用户问题”和“存储的记忆”之间不总是简单的语义相似。问题用户问“我应该学什么编程语言”而记忆是“用户最喜欢的编程语言是Python”。这两句话在字面上相似度可能不高。解决方案使用查询重写Query Rewriting。在检索前先用LLM将用户问题重写成一个更可能匹配记忆的“搜索查询”。rewrite_prompt 将用户的问题改写成一句可能在我们记忆库中找到答案的陈述句。 记忆库主要存储关于用户个人偏好和事实的陈述。 原问题{question} 改写后的搜索查询 def retrieve_with_rewrite(question, llm, retriever): prompt_template PromptTemplate.from_template(rewrite_prompt) chain prompt_template | llm # Langchain新语法组合Prompt和LLM search_query chain.invoke({question: question}).strip() print(f原始问题{question} - 搜索查询{search_query}) # 使用改写后的查询进行检索 return retriever.get_relevant_documents(search_query) docs retrieve_with_rewrite(我应该学什么编程语言, llm, retriever) for d in docs: print(f检索到{d.page_content})这样问题可能被重写为“用户的编程语言偏好”从而更精准地命中“喜欢Python”这条记忆。5.2 实现多轮对话的短期记忆融合长期记忆向量库负责跨会话的“冷知识”而短期记忆对话缓冲区负责维持当前会话的连贯性。两者需要结合。from langchain.memory import ConversationBufferMemory, CombinedMemory # 创建短期记忆缓冲区 short_term_memory ConversationBufferMemory(memory_keyshort_term_history, input_keyinput) # 创建长期记忆向量检索 long_term_memory VectorStoreRetrieverMemory(retrieverretriever, memory_keylong_term_history, input_keyinput) # 组合记忆 combined_memory CombinedMemory(memories[short_term_memory, long_term_memory]) # 更新Prompt模板同时容纳两种记忆 advanced_template 你是一个拥有记忆的AI助手。 以下是本次对话的近期上下文 {short_term_history} 以下是从我们所有过往对话中提取的关于你的一些长期信息 {long_term_history} 请综合以上所有信息回答用户的最新问题。 当前用户输入{input} 助手 ADVANCED_PROMPT PromptTemplate( input_variables[short_term_history, long_term_history, input], templateadvanced_template ) # 使用组合记忆和高级模板创建对话链 advanced_conversation ConversationChain( llmllm, promptADVANCED_PROMPT, memorycombined_memory, verboseFalse )这样模型既能记住当前聊了5句什么短期记忆又能想起你上周说过喜欢Python长期记忆回答的连贯性和个性化程度大大提升。5.3 为记忆添加元数据与过滤通过为记忆片段添加丰富的元数据可以实现更精细的检索控制。from datetime import datetime def add_memory_with_metadata(text, memory_type, tags[]): 添加带元数据的记忆 doc Document( page_contenttext, metadata{ type: memory_type, # 如preference, fact, plan tags: tags, # 如[food, hobby] created_at: datetime.now().isoformat(), confidence: 1.0 # 可来自提取模型的置信度 } ) vectorstore.add_documents(documents[doc]) # 添加记忆 add_memory_with_metadata(用户计划明年去日本旅游。, plan, [travel]) add_memory_with_metadata(用户对芒果过敏。, fact, [health, food]) # 检索时进行过滤例如只检索与“食物”和“健康”相关的记忆 from langchain.vectorstores import Chroma retriever_with_filter vectorstore.as_retriever( search_kwargs{ k: 3, filter: {tags: {$in: [food, health]}} # Chroma的过滤语法 } ) filtered_docs retriever_with_filter.get_relevant_documents(我吃东西要注意什么)6. 常见问题与故障排查在实际部署和运行中你可能会遇到以下典型问题。6.1 记忆检索不准确或召回无关内容症状AI的回答似乎没有用到记忆或者用错了记忆。排查步骤检查记忆是否成功存入直接查询向量数据库。对于Chroma可以用vectorstore.get()查看所有内容或用vectorstore.similarity_search(“你的测试查询”)看返回结果。检查嵌入模型确保使用的是中文优化的模型。用embeddings.embed_query(“测试句子”)看向量是否正常生成应为一串浮点数。调整检索参数search_kwargs{“k”: 3}中的k值可能太小漏掉关键记忆或太大引入噪声。尝试调整为2, 4, 5。score_threshold参数可以设置相似度阈值低于阈值的结果不返回。优化Prompt可能是Prompt没有清晰指示模型使用{chat_history}。尝试在Prompt中更加强调例如“你必须优先参考以下历史信息{chat_history}”。检查记忆文本质量自动提取的记忆可能过于冗长或模糊。优化你的记忆提取Prompt要求输出格式为“用户[动词][对象]”如“用户喜欢Python”、“用户讨厌香菜”。6.2 本地大模型响应慢或报错症状Ollama服务无响应或Langchain调用超时。排查步骤确认Ollama服务状态在终端执行ollama list看模型是否在列。执行curl http://localhost:11434/api/tags看API是否正常。检查模型是否加载运行ollama run命令的终端不能关闭它承载了模型进程。也可以使用ollama serve在后台运行服务。调整Langchain超时设置在初始化Ollama对象时增加超时参数。llm Ollama( base_urlhttp://localhost:11434, modelqwen2.5:7b, temperature0.2, timeout120.0 # 将超时时间设为120秒 )降低模型负载7B模型在CPU上推理确实慢。如果使用CPU考虑换用更小的模型如3B或尝试使用llama.cppgguf量化格式的模型速度会有显著提升。如果拥有GPU确保Ollama能识别到CUDA通常需要NVIDIA显卡和正确安装的驱动。6.3 向量数据库数据丢失症状程序重启后之前存储的记忆不见了。原因与解决未设置持久化目录初始化Chroma时必须提供persist_directory参数且每次都要使用同一个目录路径。未调用持久化方法在程序结束时或定期调用vectorstore.persist()是一个好习惯确保数据从内存写入磁盘。不过Chroma客户端通常会在操作后自动持久化但显式调用更安全。目录权限问题确保程序对persist_directory指定的目录有读写权限。6.4 记忆冲突与过时信息症状用户说“我现在开始喜欢香菜了”但系统里还存着“用户讨厌香菜”。解决方案目前没有银弹。可以采取混合策略时间加权检索在记忆的metadata中存储时间戳。检索时不仅考虑相似度也给较新的记忆更高的权重。手动修正接口提供简单的命令如“/forget 香菜”或“/update 我喜欢香菜”来触发对特定记忆的删除或更新操作。这需要你解析用户指令并调用向量库的删除或更新APIChroma支持按ID或元数据过滤删除。置信度衰减为每条记忆设置一个置信度分数每次被成功使用即检索到并帮助生成了合理回答时加分每次可能与新信息冲突时减分。低于某个阈值时自动归档或删除。构建一个真正智能的长期记忆系统远不止是技术拼接。它涉及到对记忆本质的理解——记忆不是数据的堆砌而是可检索、可推理、有时效的知识网络。通过Langchain我们搭建起了这个网络的骨架。而如何让记忆的提取更精准、检索更智能、维护更灵活将是持续迭代的过程。从我自己的实践来看Prompt工程的质量和记忆提取的粒度是影响体验最直接的两个因素。花时间精心设计让LLM总结记忆的Prompt比盲目增加向量数据库的容量更有效。另外从一个小的、明确的记忆类型比如“食物偏好”开始实验成功后再扩展到更复杂的领域会让整个开发过程顺畅得多。