十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

LangChain长期记忆系统:从向量化存储到会话隔离的完整实现

LangChain长期记忆系统:从向量化存储到会话隔离的完整实现 1. 项目概述为什么我们需要“长期记忆”在构建基于大语言模型LLM的应用时我们常常会遇到一个尴尬的局面每次对话都像是初次见面。你花了几分钟向一个智能客服详细描述了你的订单问题它给出了不错的建议但当你刷新页面或第二天再次打开时它又回到了那个礼貌但空洞的初始状态对你的历史一问三不知。这种“金鱼式”的七秒记忆极大地限制了应用的实用性和用户体验。这就是“长期记忆”Long-term Memory要解决的核心痛点。它不是一个单一的技术而是一套机制旨在让LLM应用能够跨越不同的会话Session记住与用户交互的历史、上下文信息以及从数据中学习到的知识从而实现连续、个性化且具备深度的对话与服务。LangChain 1.x 中的Store概念正是实现这种长期记忆的基石。你可以把它理解为一个智能的、可持久化的“记忆仓库”。它不仅仅是将聊天记录简单存到数据库里更重要的是它能将这些非结构化的文本对话通过向量化Embedding技术转换成计算机可以理解和快速检索的数学形式向量并与高效的向量数据库结合。这样当用户提出一个新问题时应用可以快速地从海量历史记忆中搜索出最相关的内容作为上下文注入给LLM从而让LLM的回复基于“记忆”而非凭空生成。简单来说这个项目的核心价值在于将一次性的、孤立的LLM调用升级为具备连续记忆和知识回溯能力的智能体Agent或应用。无论是构建一个能记住用户偏好的个人助理一个能积累领域知识的专家系统还是一个能进行多轮复杂任务规划的自动化流程长期记忆都是不可或缺的一环。接下来我们就深入拆解如何利用LangChain来实现它。2. 长期记忆的整体架构与核心组件要实现一个健壮的长期记忆系统我们需要理解其背后的数据流和核心组件。整个流程可以概括为“存、管、取”三个环节LangChain提供了相应的模块化工具来支持每一个环节。2.1 核心数据流从文本到记忆的旅程当一个对话回合或一段需要记忆的文本产生时它会经历以下旅程文本生成与分割首先我们需要决定“记住什么”。可能是一整段用户提问和AI回答的对话对也可能是其中关键的摘要或提取出的实体如产品名、日期、用户需求。对于长文本还需要用文本分割器Text Splitter切成大小合适的片段以便后续处理。向量化Embedding这是将文本转化为“记忆”的关键一步。我们使用一个嵌入模型Embedding Model将每个文本片段转换成一个高维度的向量例如1536维。这个向量在数学空间中的位置语义相近的文本其向量距离也相近。存储与索引生成的向量连同原始的文本片段作为元数据被存入一个向量数据库Vector Store。向量数据库如Chroma, Pinecone, Weaviate的核心能力是支持近似最近邻搜索能够快速从数百万个向量中找出与查询向量最相似的几个。检索Recall当新的用户查询到来时系统首先用同样的嵌入模型将查询文本也转化为向量。然后将这个查询向量发送到向量数据库中进行相似性搜索找出k个例如4个最相关的历史文本片段。上下文构建与注入检索到的文本片段被组合成一个提示Prompt的上下文部分与新查询一起发送给LLM。LLM基于这个包含了“记忆”的上下文来生成回答从而实现有记忆的对话。2.2 LangChain中的关键抽象Document, VectorStore, RetrieverLangChain 通过几个核心抽象让上述流程变得可配置和易用Document这是LangChain中表示一段文本及其元数据的基本单位。一个Document对象包含page_content文本内容和metadata如来源、日期等字典信息。所有需要被记忆的文本最终都会被封装成Document对象。VectorStore这是一个接口类定义了向量数据库的通用操作如add_documents,similarity_search。LangChain集成了众多流行的向量数据库Chroma, FAISS, Pinecone等你只需更换不同的VectorStore实现而无需重写核心逻辑。Retriever检索器。它是对VectorStore检索功能的进一步封装提供了一个统一的get_relevant_documents(query)方法。Retriever的优势在于它不仅可以做简单的向量相似性搜索还可以支持更复杂的检索策略例如自查询Self-query从用户自然语言查询中自动提取过滤器如“上个月关于项目A的文档”。上下文压缩Contextual Compression先检索出较多文档再用一个LLM来压缩、去重只保留最相关的部分节省上下文窗口。多向量检索Multi-Vector为同一份文档存储摘要、问题、关键句等多种向量表示提升检索质量。注意选择Retriever而不仅仅是VectorStore进行检索是构建生产级应用的好习惯。它为未来优化检索逻辑如增加重排序、混合搜索提供了更大的灵活性。2.3 会话Session的隔离与管理“跨会话持久化”意味着不同用户、甚至同一用户的不同对话线程之间的记忆需要隔离。这通常通过以下两种方式实现命名空间Namespace许多向量数据库支持命名空间概念。你可以为每个用户或每个会话创建一个独立的命名空间。所有该用户的Document在存储时都指定这个命名空间检索时也限定在该命名空间内。这是实现隔离最直接的方式。元数据过滤Metadata Filtering更通用和灵活的方式是利用Document的metadata。在存储时为每个Document添加如session_id: “user123_session1”或user_id: “user123”的元数据。在检索时通过Retriever的过滤功能只检索匹配特定元数据的文档。例如Chroma和Weaviate都支持基于元数据的精确过滤和范围过滤。在实际项目中推荐使用“用户ID 时间窗口/会话ID”作为元数据过滤的核心策略。这样既能保证隐私隔离又能灵活地控制记忆的范围例如可以选择检索该用户所有的历史或者仅限本次会话。3. 实操一步步构建带长期记忆的聊天机器人理论讲完了我们动手搭建一个最简单的、具备跨会话记忆的聊天机器人。我们将使用本地运行的ChromaDB作为向量数据库OpenAI的Embedding模型以及LangChain的对话链。3.1 环境准备与依赖安装首先确保你的Python环境建议3.8以上并安装必要的包pip install langchain langchain-openai langchain-chroma tiktoken这里我们安装了langchain核心库、OpenAI的官方集成包langchain-openai、ChromaDB的集成包langchain-chroma以及用于计算Token的tiktoken。接下来在代码中设置你的OpenAI API密钥如果你使用其他模型如本地部署的或Azure OpenAI配置方式类似但略有不同import os from getpass import getpass # 安全地设置API密钥避免硬编码在代码中 os.environ[OPENAI_API_KEY] getpass(请输入你的OpenAI API Key: )3.2 初始化核心组件嵌入模型与向量库我们选择text-embedding-ada-002作为嵌入模型它在效果、速度和成本之间取得了很好的平衡。from langchain_openai import OpenAIEmbeddings from langchain_chroma import Chroma # 初始化嵌入模型 embeddings OpenAIEmbeddings(modeltext-embedding-ada-002) # 指定一个持久化目录这样数据才会保存在本地实现“持久化” persist_directory ./chroma_db_langchain_memory # 初始化Chroma向量数据库并指定持久化路径。 # 如果目录已存在它会加载已有数据否则会新建。 # collection_name 可以理解为数据库中的一张表这里我们命名为“chat_memory”。 vectorstore Chroma( collection_namechat_memory, embedding_functionembeddings, persist_directorypersist_directory )关键参数解析persist_directory这是实现“持久化”的关键。不设置此参数Chroma默认在内存中运行程序退出数据即丢失。设置后所有向量和元数据会保存到该目录下的SQLite和文件系统中。collection_name集合名称。一个Chroma实例可以管理多个集合类似于数据库中的表。用不同的集合来隔离不同类型的记忆如“聊天记录”、“知识库文档”是一个好习惯。3.3 创建具备记忆能力的对话链LangChain提供了ConversationChain但为了更清晰地展示记忆机制我们使用更底层的RetrievalQA链与历史管理结合的方式。首先我们需要一个工具来管理对话历史。我们将历史记录也存入向量库但每次只检索最近且最相关的几条。from langchain.memory import VectorStoreRetrieverMemory from langchain.chains import ConversationalRetrieverChain from langchain_openai import ChatOpenAI # 1. 基于我们刚才创建的vectorstore创建一个Retriever。 # search_kwargs 中的 k 表示每次检索返回的文档数量。这里设为4。 retriever vectorstore.as_retriever(search_kwargs{k: 4}) # 2. 使用 VectorStoreRetrieverMemory 将Retriever包装成一个“记忆”组件。 # 这个记忆组件会自动将对话历史保存到vectorstore并在需要时从中检索。 memory VectorStoreRetrieverMemory(retrieverretriever) # 3. 初始化大语言模型。这里使用GPT-3.5-turbo性价比高。 llm ChatOpenAI(modelgpt-3.5-turbo, temperature0.7) # 4. 创建对话链。这里使用ConversationalRetrieverChain它专为“基于检索的对话”设计。 # 它将自动处理将用户当前问题历史记忆 - 检索相关文档 - 组合成最终提示 - 调用LLM。 conversation_chain ConversationalRetrieverChain.from_llm( llmllm, retrieverretriever, # 用于检索知识库如果有的话和记忆 memorymemory, # 用于管理对话历史记忆 verboseFalse, # 设为True可以看到链的详细执行过程调试时有用 )代码解读VectorStoreRetrieverMemory这是连接对话和向量存储的桥梁。每次对话后它会将对话内容通常是“Human: ...\nAI: ...”格式作为一个Document存入vectorstore。下次对话前它会根据当前输入从vectorstore中检索出相关的历史对话片段。ConversationalRetrieverChain这是一个功能强大的链。它内部会先调用memory加载相关历史然后将历史上下文和当前问题组合发送给retriever去搜索可能相关的知识文档如果你额外加载了知识库最后把所有内容整合成一个完整的提示给LLM。在这个例子中我们的retriever和memory用的是同一个向量库所以它同时负责了历史记忆和“知识”的检索。3.4 实现跨会话的持久化与隔离上面的代码已经实现了记忆的持久化通过persist_directory但还没有隔离。我们需要为不同会话添加元数据。修改VectorStoreRetrieverMemory的创建方式使其在保存记忆时自动添加会话IDfrom langchain.schema import Document from uuid import uuid4 class SessionAwareMemory(VectorStoreRetrieverMemory): 一个支持会话隔离的记忆类 def __init__(self, session_id, *args, **kwargs): super().__init__(*args, **kwargs) self.session_id session_id def save_context(self, inputs: Dict[str, Any], outputs: Dict[str, str]) - None: 重写保存上下文的方法在存入向量库时添加session_id元数据 # 调用父类方法生成要保存的文本 super().save_context(inputs, outputs) # 但我们需要更精细的控制所以直接操作vectorstore # 实际上更优雅的方式是自定义一个Retriever这里为演示清晰采用直接操作 pass # 更实用的方法在创建Retriever时直接配置元数据过滤器 def get_session_retriever(vectorstore, session_id): 创建一个只检索特定会话内容的Retriever # Chroma的as_retriever支持filter参数 retriever vectorstore.as_retriever( search_kwargs{ k: 4, filter: {session_id: session_id} # 关键元数据过滤 } ) return retriever # 使用示例 session_id user_001_chat_01 # 可以从用户登录信息或前端传入 session_retriever get_session_retriever(vectorstore, session_id) session_memory VectorStoreRetrieverMemory(retrieversession_retriever) # 现在当有新的对话需要保存时我们需要手动将其存入向量库并附上session_id def save_conversation_turn(vectorstore, session_id, human_input, ai_output): 保存一轮对话到向量库并打上会话标签 text fHuman: {human_input}\nAI: {ai_output} doc Document( page_contenttext, metadata{session_id: session_id, turn: datetime.now().isoformat()} ) vectorstore.add_documents([doc]) vectorstore.persist() # 确保写入磁盘实操心得会话ID生成在实际应用中session_id可以是f”{user_id}_{thread_id}”的组合。对于Web应用一个简单的实现是使用用户的唯一标识和聊天窗口的标识来生成。元数据设计除了session_id强烈建议在metadata中添加时间戳如timestamp和对话轮次如turn_index。这样你可以在检索时实现更复杂的逻辑例如“检索该用户最近24小时内最相关的5条记录”这可以通过元数据过滤timestamp 某个值和相似性搜索结合来实现。持久化调用注意vectorstore.add_documents()之后需要调用vectorstore.persist()才能确保数据写入磁盘。Chroma有自动持久化的机制但在关键操作后手动调用一次更保险。3.5 完整的对话循环示例下面是一个模拟两个独立会话的完整示例import datetime def chat_session(session_name, user_id): print(f\n 开始会话: {session_name} ) # 为每个会话创建独立的检索器和记忆 session_id f{user_id}_{session_name} retriever get_session_retriever(vectorstore, session_id) memory VectorStoreRetrieverMemory(retrieverretriever) # 创建会话链 chain ConversationalRetrieverChain.from_llm( llmllm, retrieverretriever, memorymemory, verboseFalse ) # 模拟对话 queries [ 我叫张三我喜欢打篮球和编程。, 我上次说我喜欢什么运动来着, 我的爱好是什么请总结一下。 ] for query in queries: print(f\n[用户]: {query}) # 注意ConversationalRetrieverChain期望的输入格式是 {question: query} result chain.invoke({question: query}) response result[answer] print(f[AI]: {response}) # 手动保存本轮对话到向量库以便后续会话能检索到 save_conversation_turn(vectorstore, session_id, query, response) print(f 会话 {session_name} 结束 \n) # 模拟用户A的两次不同会话 chat_session(运动咨询, user_A) chat_session(技术讨论, user_A) # 模拟用户B的一次会话 chat_session(日常聊天, user_B) # 最后验证隔离性尝试从全局检索“篮球” print( 测试全局检索‘篮球’相关记忆 ) all_docs vectorstore.similarity_search(篮球, k5) for i, doc in enumerate(all_docs): print(f结果{i1} [来自会话: {doc.metadata.get(session_id, N/A)}]: {doc.page_content[:100]}...)运行这段代码你将观察到在“运动咨询”会话中AI能记住用户名叫张三喜欢篮球。在“技术讨论”会话中AI不会知道用户喜欢篮球除非你故意问一个跨会话的全局问题我们在最后测试了。用户B的会话完全独立不会看到用户A的任何信息。程序退出后重新运行只要persist_directory指向同一个目录所有记忆依然存在。4. 高级技巧与性能优化基础功能实现后我们来看看如何提升长期记忆系统的质量和效率。4.1 记忆的“修剪”与摘要化无限制地存储每一轮对话会导致向量库膨胀检索速度变慢且可能引入噪声很久以前的不相关记忆。常见的优化策略有基于时间的窗口在检索时通过元数据过滤器只检索最近N天或最近M条记录。这需要在保存时记录时间戳。摘要式记忆不存储原始对话而是定期如每10轮对话用LLM对近期对话历史生成一个摘要然后将摘要存入向量库。这样既保留了关键信息又大幅压缩了存储。LangChain的ConversationSummaryBufferMemory结合VectorStoreRetrieverMemory可以实现此功能。重要性评分在保存记忆时可以用一个简单的规则或另一个LLM调用为这段记忆打一个“重要性”分数存入元数据。检索时可以按相似度和重要性分数进行加权排序。4.2 检索策略的优化超越简单相似度搜索混合搜索Hybrid Search除了向量相似度还可以结合关键词匹配如BM25。这对于精确匹配名称、代号等场景效果更好。一些向量数据库如Weaviate, Qdrant原生支持混合搜索。重排序Re-ranking先使用向量搜索召回100个相关文档再用一个更精细但更耗时的重排序模型如Cohere的rerank模型或BGE-reranker对前100个结果进行精排返回最精准的Top-K个。这能显著提升最终上下文的质量。多跳检索Multi-hop Retrieval对于复杂问题可能需要多次检索。例如先检索到“某公司发布了新产品A”再以“产品A的技术规格”为查询进行第二次检索。这可以通过RetrievalQA链的迭代或使用Agent来实现。4.3 元数据过滤的灵活运用元数据是管理记忆的强大工具。你可以设计丰富的元数据模式metadata { “session_id”: “user_001_chat_01”, “user_id”: “user_001”, “timestamp”: “2023-10-27T14:30:00Z”, “entity”: [“篮球”, “Python”], # 从对话中提取的关键实体 “topic”: “hobbies”, # 对话主题分类 “importance”: 0.8, # 人工或自动标注的重要性 “source”: “direct_chat” # 记忆来源区分是聊天记录还是上传的文档 }在检索时你可以构建复杂的过滤器{“user_id”: “user_001”, “topic”: “work”}获取用户001所有关于工作的记忆。{“timestamp”: {“$gte”: “2023-10-01T00:00:00Z”}}获取十月以后的所有记忆。{“importance”: {“$gte”: 0.7}}只检索高重要性的记忆。5. 常见问题、排查与实战心得在实际部署中你肯定会遇到各种问题。这里分享一些踩坑经验和解决方案。5.1 记忆检索不准确或“遗忘”症状AI似乎不记得刚才说过的话或者检索到的记忆风马牛不相及。排查与解决检查向量化模型确保保存和检索时使用的是同一个嵌入模型。不同模型生成的向量空间不同无法直接比较。这是最常见的问题。检查元数据过滤确认你的session_id或user_id在保存和检索时完全一致包括大小写和格式。调试时可以临时关闭过滤器看全局检索是否正常。查看原始存储内容直接查询向量数据库看看里面到底存了什么。# 对于Chroma可以这样查看所有数据谨慎使用数据量大时慢 all_docs vectorstore.get() print(all_docs[‘documents’][:3]) # 查看前3个文档内容 print(all_docs[‘metadatas’][:3]) # 查看前3个文档的元数据调整检索参数kk值太小可能漏掉相关记忆太大可能引入噪声。通常从4开始尝试根据场景调整。文本分割问题如果存储的Document文本过长或过短都可能影响检索效果。确保使用合适的TextSplitter如RecursiveCharacterTextSplitter并调整chunk_size和chunk_overlap。5.2 性能瓶颈与成本考量向量数据库选择本地/轻量级Chroma本项目所用简单易用适合原型和中小规模。FAISSFacebook开发性能极高纯内存操作但需要自己处理持久化。云端/生产级Pinecone、Weaviate、Qdrant是托管服务提供高可用、可扩展和高级功能如混合搜索、多租户适合生产环境但有成本。嵌入模型成本如果使用OpenAI等付费API大量文本的向量化会产生成本。对策对记忆进行摘要减少存储文本量。考虑使用开源嵌入模型如BGE、text2vec系列通过HuggingFaceEmbeddings本地部署零调用成本。检索延迟向量搜索是计算密集型操作。当记忆库很大时10万条延迟可能显著。使用支持索引的向量数据库如HNSW in FAISS/Qdrant。实施记忆“修剪”策略保持活跃记忆库的大小可控。对于实时性要求高的对话可以考虑在内存中缓存最近N轮对话使用ConversationBufferWindowMemory结合向量库用于长期记忆回溯。5.3 隐私与数据安全长期记忆存储了所有对话历史这是高度敏感的数据。加密存储确保向量数据库文件如Chroma的SQLite文件存储在加密卷或经过加密处理。访问控制严格实现基于user_id的元数据过滤确保任何API接口或检索操作都不会越权访问他人数据。在服务端进行权限校验不要信任客户端传入的session_id。数据清理提供用户数据删除接口。当用户请求删除时需要能根据user_id从向量库中物理删除所有相关Document。这需要向量数据库支持按元数据删除Chroma支持delete操作配合where过滤器。5.4 一个实战中的经典“坑”记忆的无限膨胀与上下文污染即使有向量搜索如果每次都将所有历史记忆作为上下文喂给LLM很快就会超过模型的上下文窗口限制如GPT-4的128K也会用完。ConversationalRetrieverChain的聪明之处在于它通过检索只选取最相关的少量记忆而非全部历史从而避免了这个问题。但是这引入了另一个问题“记忆碎片化”。如果用户的问题很泛如“我之前都说过什么”检索到的几个片段可能无法代表完整的记忆全景。解决方案采用分层记忆系统。短期记忆使用ConversationBufferWindowMemory或ConversationSummaryMemory在内存中保留最近几轮对话或一个摘要。这保证了对话的连贯性。长期记忆使用本文所述的VectorStoreRetrieverMemory存储所有历史用于回溯特定事实或主题。在链中组合你可以创建一个自定义链先查询短期记忆如果不够再查询长期记忆向量库将两者结果融合后作为上下文。LangChain的CombinedMemory类可以辅助管理多个记忆源。构建一个真正智能、可靠且高效的长期记忆系统远不止调用一个API那么简单。它需要你在数据模型设计、检索策略、性能成本和隐私安全之间反复权衡。从本文介绍的基础架构出发结合你的具体应用场景不断迭代和优化你就能打造出真正让用户感到“被记住”的AI应用体验。
返回列表