基于LangChain为DeepSeek构建三层记忆系统:从对话连贯到知识增强
1. 从“健忘”到“健谈”:为什么大模型需要记忆
如果你用过像 DeepSeek 这样的主流大模型 API,一定有过这样的体验:你问它“我上一句话说了什么?”,它大概率会一脸茫然地告诉你“抱歉,我无法访问之前的对话历史”。或者,在一个多轮对话中,你费尽心思地介绍了项目背景、技术栈和需求,到了第五轮,你问“那么,基于我们刚才讨论的架构,数据库选型你有什么建议?”,它可能会给你一个非常通用、完全忽略了你前面提到的“高并发写入”和“地理空间查询”需求的答案。
这就是典型的“无状态”困局。每一次 API 调用,对于大模型来说,都是一次全新的、孤立的“邂逅”。它不记得几秒前你说了什么,更不用说几分钟、几小时甚至几天前的对话上下文了。这种设计在技术上有其合理性,比如保证了请求的独立性和可扩展性,但对于构建连贯、智能、能真正理解用户意图和背景的对话应用来说,这无疑是一个巨大的障碍。
我们真正想要的,不是一个每次都要从头开始解释的“金鱼脑”助手,而是一个能记住关键信息、理解对话脉络、甚至能基于长期互动形成个性化认知的“伙伴”。这就是为 DeepSeek 这类大模型注入“记忆”能力的核心诉求。它不仅仅是把历史对话文本拼接起来再喂给模型那么简单,而是涉及对信息的筛选、存储、检索和有效利用的一整套工程体系。
最近,围绕 LangChain、LangGraph 等框架的讨论非常火热,大家的核心痛点都指向了如何让 AI 应用变得更“聪明”、更“持久”。无论是想构建一个能记住用户偏好的客服机器人,还是一个能持续跟进项目进度的编程助手,抑或是一个能进行深度、多轮分析的数据洞察工具,“记忆”都是不可或缺的基石。本文将抛开那些空洞的概念,直接切入实战,手把手带你用 LangChain 这套目前最流行的工具链,为 DeepSeek 大模型搭建起从短期对话记忆到长期知识记忆的完整能力,破解无状态困局。
2. 记忆系统的核心架构:不止是“记住聊天记录”
在开始敲代码之前,我们必须先理清“记忆”到底包含什么。一个健壮的记忆系统,远非一个简单的历史消息列表。根据 LangChain 的实践和社区共识,我们可以将其粗略分为几个层次,这比简单地讨论“短期”和“长期”更有操作性。
2.1 对话记忆:让聊天连贯起来
这是最基础、最直接的需求。目标就是让模型能“看到”当前对话中已经发生过的内容。LangChain 提供了几种开箱即用的方案:
ConversationBufferMemory:最简单粗暴,把整个对话历史(包括用户输入和AI输出)都存成一个长字符串,每次调用时全部附加上去。优点是信息完整,缺点是上下文长度爆炸(Token 费用和模型处理能力都有限制),而且无关信息会干扰当前回答。ConversationBufferWindowMemory:只保留最近 K 轮对话。这解决了上下文长度问题,但“健忘”得更快,可能丢失掉对话早期的重要前提。ConversationSummaryMemory:一个非常巧妙的思路。它并不保存原始对话,而是随着对话进行,动态地生成并更新一个“摘要”。每次新的交互都基于之前的摘要和最新对话来生成新的摘要。这样,无论对话多长,传递给模型的始终是一个精炼的、包含核心信息的摘要,极大地节省了 Token。这是处理长对话的利器。
在实际项目中,我通常不会直接使用最基础的BufferMemory。对于大多数客服或问答场景,BufferWindowMemory(比如只保留最近5轮)结合一个清晰的系统提示词(如“请基于最近几轮的对话内容回答”),就已经能解决80%的连贯性问题。而对于需要回顾大量背景信息的深度分析或创作场景,SummaryMemory是更优的选择。
2.2 实体记忆:记住关于“你”的关键信息
对话记忆是关于“我们说了什么”,而实体记忆则是关于“你是谁”。想象一个场景,用户在第一轮说:“我叫张三,是一名后端工程师,主要用 Go 语言。” 在第十轮,用户问:“那我刚才说的那种技术方案,用我熟悉的语言实现有什么坑吗?” 一个仅有对话记忆的系统,可能需要在上下文中保留长达十轮的记录才能关联上“Go语言”。而实体记忆系统,则能主动提取并存储“用户职业:后端工程师”、“擅长语言:Go”这样的结构化信息。
LangChain 的ConversationEntityMemory就是干这个的。它利用一个小型语言模型(或规则)从对话中提取实体(人、地点、事物、属性)及其关系,存储在一个类似知识图谱的结构中。当新的查询到来时,它会先检索相关的实体信息,并将其作为补充上下文注入。这相当于为模型配备了一个便签本,专门记录关于用户或讨论对象的关键事实。
2.3 长期记忆与向量检索:构建专属知识库
当我们需要模型记住超出单次会话范围的信息时,比如公司内部文档、产品手册、历史会议纪要,就需要引入长期记忆。其核心是RAG(检索增强生成)架构。
- 知识入库:将你的文档(PDF、Word、网页、Notion页面等)进行切分,转换成文本片段。
- 向量化:使用嵌入模型(如 OpenAI 的
text-embedding-3-small,或开源的BGE、M3E等)将这些文本片段转换为高维向量(一串数字),并存入向量数据库(如Chroma,Weaviate,Qdrant,Milvus)。 - 检索增强:当用户提问时,先将问题本身也向量化,然后在向量数据库中搜索与之最相似的文本片段(即“记忆”)。
- 生成回答:将检索到的相关文本片段作为上下文,连同用户问题一起发送给 DeepSeek 大模型,让它基于这些“记忆”生成答案。
这才是真正意义上的“注入记忆”。模型本身并没有被修改或训练,但它获得了一个可以实时查询的、海量的、专属的外部大脑。这也是目前让大模型落地于私有知识场景最主流、最有效的方法。LangChain 在文档加载、切分、向量化、检索等每个环节都提供了丰富的组件和极简的接口,让搭建一个 RAG 系统变得像搭积木一样简单。
3. 实战:为 DeepSeek API 搭建三层记忆系统
理论说再多不如一行代码。下面,我将以一个“技术顾问助手”为例,展示如何结合 LangChain 为 DeepSeek 构建一个包含对话记忆、实体记忆和长期知识记忆的完整系统。假设我们已有一个 DeepSeek 的 API Key,并且端点兼容 OpenAI SDK 格式(这是目前最通用的方式)。
注意:以下示例基于 LangChain 的主流版本,确保你的
langchain和langchain-community等包已更新。DeepSeek 的 API 调用方式可能与 OpenAI 有细微差别,主要在于base_url和model参数的设置。
3.1 环境准备与基础连接
首先,安装必要的库,并设置好 DeepSeek 的 LLM 对象。这里我们使用langchain_openai中的ChatOpenAI,因为它兼容 OpenAI 协议。
pip install langchain langchain-community langchain-openai chromadb tiktokenimport os from langchain_openai import ChatOpenAI from langchain.memory import ConversationBufferWindowMemory, ConversationEntityMemory from langchain.memory.entity import SQLiteEntityStore from langchain.chains import ConversationChain from langchain.prompts import PromptTemplate # 设置你的 DeepSeek API Key 和 Base URL os.environ["DEEPSEEK_API_KEY"] = "your-deepseek-api-key-here" # 初始化 DeepSeek LLM # 注意:model 名称请根据 DeepSeek 官方文档填写,例如 "deepseek-chat" # base_url 指向 DeepSeek 的 API 端点 llm = ChatOpenAI( model="deepseek-chat", openai_api_key=os.environ["DEEPSEEK_API_KEY"], base_url="https://api.deepseek.com/v1", # 请以官方文档为准 temperature=0.7, max_tokens=2000 )3.2 实现对话记忆与实体记忆的融合
我们将结合ConversationBufferWindowMemory和ConversationEntityMemory,创建一个既能记住近期对话,又能捕捉关键实体的记忆体。EntityMemory需要一个地方来存储实体信息,这里我们用轻量级的SQLiteEntityStore。
# 初始化一个 SQLite 数据库来存储实体(会在当前目录生成 .sqlite 文件) entity_store = SQLiteEntityStore() # 创建融合记忆 memory = ConversationBufferWindowMemory( memory_key="chat_history", # 对话历史在 prompt 中的变量名 k=3, # 只保留最近3轮对话 return_messages=True # 返回 Message 对象列表,而不是字符串 ) entity_memory = ConversationEntityMemory( llm=llm, # 实体记忆需要一个 LLM 来提取实体,这里可以用同一个 DeepSeek 实例,但为节省成本,实践中常用小模型 entity_store=entity_store, k=5 # 在提示词中注入最多5个相关实体信息 ) # 在实际的 Chain 中,我们需要自定义一个逻辑来合并两种记忆。 # 这里为了演示清晰,我们先分别展示它们的作用。让我们先测试一下对话记忆:
# 创建一个简单的对话链,使用对话记忆 conversation_with_memory = ConversationChain( llm=llm, memory=memory, verbose=True # 打印详细日志,方便理解 ) print("--- 测试对话记忆 ---") response1 = conversation_with_memory.predict(input="你好,我叫李雷,是一名 DevOps 工程师。") print(f"AI: {response1}\n") response2 = conversation_with_memory.predict(input="我最近在关注 Kubernetes 的自动伸缩。") print(f"AI: {response2}\n") # 此时,记忆里应该有前两轮对话。 # 第三轮问题,依赖之前的上下文。 response3 = conversation_with_memory.predict(input="对于我这种角色,你有什么学习建议吗?") print(f"AI: {response3}") # 观察输出,AI 应该能关联到“DevOps 工程师”和“Kubernetes”。接下来,我们单独测试实体记忆是如何工作的:
# 使用实体记忆进行对话 prompt_with_entity = PromptTemplate( input_variables=["entities", "input"], template="以下是当前已知的相关信息:\n{entities}\n\n用户的最新问题:{input}\n请根据已知信息回答:" ) # 模拟一个多轮过程,手动展示实体记忆的存储和检索 entity_memory.save_context( {"input": "我叫韩梅梅,是公司的产品经理。"}, {"output": "你好韩梅梅,产品经理需要很好的沟通能力。"} ) entity_memory.save_context( {"input": "我们团队主要使用 Figma 和 Jira。"}, {"output": "Figma 和 Jira 是产品设计和项目管理的常用工具。"} ) # 在回答新问题前,检索实体 entities = entity_memory.load_memory_variables({"input": "我该如何提高原型设计效率?"})["entities"] print("--- 检索到的实体信息 ---") print(entities) print("--- 基于实体信息的回答 ---") # 这里简化处理,实际应将 entities 和 input 填入 prompt 再调用 llm # 可以看到,实体记忆里存储了“韩梅梅”、“产品经理”、“Figma”、“Jira”等实体及其关系。在实际的复杂应用中,你需要设计更精巧的 Chain 或使用LangGraph来编排对话流,决定在每一步如何使用和更新不同的记忆组件。一个常见的模式是:用户输入 -> 用实体记忆检索相关事实 -> 结合对话历史和检索到的事实 -> 生成提示词 -> 调用 LLM 生成回答 -> 将本轮交互更新到对话记忆和实体记忆中。
3.3 集成长期记忆:构建 RAG 知识库
假设我们想为助手注入公司内部的“技术栈选型指南”作为长期记忆。
步骤一:准备知识文档并向量化
我们使用Chroma作为本地向量数据库,它简单易用。
from langchain_community.document_loaders import TextLoader from langchain.text_splitter import RecursiveCharacterTextSplitter from langchain_openai import OpenAIEmbeddings from langchain_community.vectorstores import Chroma # 1. 加载文档(这里用文本文件示例) loader = TextLoader("./tech_stack_guide.txt", encoding="utf-8") documents = loader.load() # 2. 分割文档 text_splitter = RecursiveCharacterTextSplitter( chunk_size=500, # 每个片段大小 chunk_overlap=50 # 片段间重叠部分,避免割裂语义 ) texts = text_splitter.split_documents(documents) # 3. 初始化嵌入模型。DeepSeek 可能也提供嵌入模型,这里以 OpenAI 为例,你需要替换为可用的嵌入模型。 # 如果 DeepSeek 未提供,可以使用开源的 embedding 模型,如 from langchain.embeddings import HuggingFaceEmbeddings embeddings = OpenAIEmbeddings( openai_api_key=os.environ.get("OPENAI_API_KEY"), # 注意:这里可能需要另一个 API Key model="text-embedding-3-small" ) # 4. 创建向量数据库 vectorstore = Chroma.from_documents( documents=texts, embedding=embeddings, persist_directory="./chroma_db" # 持久化到本地目录 ) # 之后加载可以直接用 Chroma(persist_directory="./chroma_db", embedding_function=embeddings)步骤二:创建检索链,与对话结合
现在,我们将检索能力加入到对话流程中。这里使用RetrievalQA链,它封装了检索和问答。
from langchain.chains import RetrievalQA # 创建检索问答链 qa_chain = RetrievalQA.from_chain_type( llm=llm, chain_type="stuff", # 将检索到的文档“塞”进提示词 retriever=vectorstore.as_retriever(search_kwargs={"k": 3}), # 检索最相关的3个片段 return_source_documents=True # 返回源文档,用于解释 ) # 测试长期记忆 print("--- 测试长期记忆(RAG)---") result = qa_chain.invoke({"query": "我们公司对于微服务通信推荐使用什么协议?"}) print(f"回答:{result['result']}") print("\n来源文档:") for doc in result['source_documents'][:2]: # 打印前两个来源 print(f"- {doc.page_content[:200]}...")步骤三:融合短期、实体与长期记忆
这是最复杂的一步,需要设计一个决策逻辑:用户的问题是关于私有知识(长期记忆),还是关于当前对话或个人信息(短期/实体记忆)?或者是混合?一个简单的路由方案如下:
from langchain.agents import Tool, AgentExecutor, create_react_agent from langchain import hub # 定义工具 tools = [ Tool( name="Company_Knowledge_Base", func=qa_chain.run, # 调用 RAG 链 description="当问题涉及公司内部技术规范、指南、文档时使用此工具。" ), # 理论上,对话和实体记忆也可以封装为工具,但这里我们将其作为 Agent 的默认记忆。 ] # 从 LangChain Hub 拉取一个 ReAct 风格的提示词(这是一个智能代理框架) prompt = hub.pull("hwchase17/react") # 创建代理 agent = create_react_agent(llm, tools, prompt) # 创建代理执行器,并传入我们之前定义的对话记忆 agent_executor = AgentExecutor( agent=agent, tools=tools, memory=memory, # 代理可以拥有对话记忆 verbose=True, handle_parsing_errors=True ) # 现在,这个代理可以智能地决定何时查询知识库,何时仅基于对话历史回答。 # 例如: # 问题“Kubernetes 的 HPA 怎么配置?” -> 可能触发知识库工具。 # 问题“我上一句说了什么?” -> 直接利用对话记忆回答。 # 问题“基于我 DevOps 工程师的身份,刚才说的方案可行吗?” -> 结合对话记忆和实体记忆(如果配置了)回答。这个代理只是一个起点。在真实的高阶应用中,你会使用LangGraph来绘制更精细、可循环、带状态的工作流图,精确控制记忆的读取、更新和工具调用的顺序,从而构建出真正强大、稳定、可控的 AI 应用。LangGraph允许你定义复杂的循环、分支和状态管理,是构建具备复杂记忆和推理能力 Agent 的终极武器。
4. 避坑指南:记忆系统实战中的常见陷阱
在实际部署中,为 DeepSeek 或其他模型添加记忆绝非一帆风顺。下面是我在多个项目中总结出的关键陷阱和应对策略。
4.1 上下文窗口与 Token 消耗的平衡
DeepSeek 和其他模型一样,有上下文长度限制(例如 32K、128K Token)。ConversationBufferMemory会无限制增长,很快会触顶,导致最开始的对话被“遗忘”(实际上是被截断),或者触发 API 的 Token 超限错误。
- 解决方案:
- 优先使用
ConversationSummaryMemory:对于长对话,这是首选。但要注意,摘要可能丢失细节。 - 设定合理的
BufferWindow:根据场景调整k值。快速问答场景k=3-5足够;深度讨论可能需要k=10。 - 主动管理上下文:在对话关键节点(如话题切换时),通过编程方式清理或总结旧记忆。例如,当用户说“我们聊点别的吧”,你可以调用 LLM 对之前的对话生成一个最终摘要并存入长期记忆(如果重要),然后清空短期对话缓冲区。
- Token 计数与预警:使用
tiktoken库计算每次请求的 Token 数,设置阈值报警,并在 UI 上给用户提示“对话历史过长,正在优化记忆”。
- 优先使用
4.2 实体记忆的噪声与幻觉
ConversationEntityMemory依赖一个小型 LLM 来提取实体。这个提取过程可能出错,提取出无关实体,或者建立错误的关系(如将“苹果”错误地关联到“公司”而不是“水果”)。更糟糕的是,如果用于提取实体的 LLM 本身有幻觉,它会向知识库中注入虚假信息,污染后续所有回答。
- 解决方案:
- 使用更可靠的提取模型:如果成本允许,使用 GPT-4 等更强模型进行实体提取,比使用小模型或同一个大模型(在节省模式下)更准确。
- 设置置信度过滤:对于提取出的实体和关系,可以设计一个置信度评分,过滤掉低置信度的条目。
- 提供人工审核或修正接口:对于关键应用,记录下 AI 记忆的“事实”,并提供给用户确认或修正的机会。例如,“您提到您是‘后端工程师’,对吗?”
- 定期清理:为实体存储设置 TTL(生存时间),或者定期扫描并清理长时间未被引用的孤立实体。
4.3 RAG 长期记忆的检索质量瓶颈
RAG 的效果严重依赖于检索质量。“垃圾进,垃圾出”,如果检索不到相关文档,或者检索到不相关的文档,大模型再强也无力回天。
- 解决方案:
- 文档预处理是重中之重:
- 智能分块:不要简单按字符或段落分。尝试按语义分块(使用
SemanticChunker),或按标题结构分块。 - 添加元数据:为每个文本块添加来源、标题、页码、章节等元数据。检索时可以利用元数据过滤。
- 优化索引:为关键段落生成摘要或问题,并将其与原始文本一起索引,这能提升检索的召回率。
- 智能分块:不要简单按字符或段落分。尝试按语义分块(使用
- 检索策略优化:
- 混合搜索:结合向量搜索(语义相似)和关键词搜索(如 BM25)。
Chroma和Weaviate都支持混合检索。语义搜索找“意思像的”,关键词搜索找“字面有的”,互补性强。 - 重排序:先用向量数据库召回 Top K(比如20个)文档,再用一个更精细的交叉编码器模型对它们进行重排序,选出 Top N(比如3个)最相关的。这能显著提升精度。
- 查询转换/扩展:在检索前,用 LLM 对用户原始查询进行改写、扩展或生成假设性答案。例如,将“怎么配置?”扩展为“配置步骤、配置方法、配置教程”。
- 混合搜索:结合向量搜索(语义相似)和关键词搜索(如 BM25)。
- 针对 DeepSeek 的适配:确保你的嵌入模型与 DeepSeek 的理解能力在语义空间上对齐。如果可能,使用 DeepSeek 官方推荐的或同系列的嵌入模型,或者用 DeepSeek 生成的数据对开源嵌入模型进行微调。
- 文档预处理是重中之重:
4.4 记忆的隔离与冲突
在多用户、多会话场景下,记忆不能“串台”。用户 A 的对话历史和实体信息绝对不能泄露给用户 B。同时,同一个用户在不同主题的对话中,可能希望记忆有所隔离(比如工作模式和闲聊模式)。
- 解决方案:
- 会话级隔离:这是最基本的。为每个对话会话(Session)创建独立的记忆对象(
memory实例)和向量数据库索引(或至少使用不同的命名空间namespace)。Web 应用中,通常将会话 ID 作为隔离键。 - 使用
LangGraph的状态管理:LangGraph的StateGraph天然支持多线程、带状态的工作流。你可以将每个用户会话的状态(包括所有记忆)封装在一个状态对象中,由LangGraph的检查点机制来持久化和恢复,完美解决隔离和持久化问题。 - 命名空间:在使用如
Chroma或Weaviate时,为不同用户或不同主题的知识库使用不同的collection_name或namespace。
- 会话级隔离:这是最基本的。为每个对话会话(Session)创建独立的记忆对象(
5. 进阶思考:从记忆到“认知架构”
当我们解决了基础的记忆问题后,下一个挑战是如何让记忆“活”起来,形成更高层次的认知。这不仅仅是存储和检索,而是关于信息的整合、推理和规划。
5.1 记忆的主动管理与摘要
高级的 AI 助手不应该被动地记录一切,而应该像人类一样,主动总结、提炼和遗忘。我们可以设计这样的逻辑:
- 周期性摘要:每对话 N 轮后,自动触发一个摘要任务,将详细的对话记忆压缩成一段精炼的要点,并存入一个“长期摘要”存储区。原始的详细记录则可以部分清空。
- 重要性打分:为对话中的陈述或实体赋予重要性分数。例如,用户明确说“这一点非常重要”的语句,其得分应该更高,在记忆检索和摘要中占据更大权重。
- 遗忘机制:模仿人脑的遗忘曲线,为记忆条目设置衰减权重。长时间未被提及或使用的信息,其检索优先级逐渐降低。
5.2 基于记忆的规划与反思
这是LangGraph等框架大显身手的地方。一个具备规划能力的 Agent,其工作流可能是:
- 接收目标:用户说“帮我制定一个学习 Go 微服务的三个月计划”。
- 检索记忆:从长期记忆中检索用户已有的技能(“熟悉 Python”、“了解 Docker”),从对话记忆中检索用户的偏好(“希望以项目驱动”)。
- 制定计划:LLM 基于目标和记忆,生成一个初步的计划步骤列表。
- 执行与反思:Agent 开始执行计划的第一步,比如“推荐第一周的学习资源”。执行后,将结果和用户反馈存入记忆。然后,Agent 可以进入一个“反思”节点,评估第一步的效果,并决定是继续第二步,还是调整计划。
- 持续迭代:在整个多轮交互中,记忆在不断更新,计划也在动态调整。最终形成的计划,是高度个性化的。
5.3 与外部系统的记忆同步
真正的企业级应用,AI 助手的记忆不能只存在于对话中。它需要与外部系统打通:
- 同步到 CRM:如果用户说“我对企业版套餐感兴趣”,这条“销售线索”记忆应该能自动创建或更新 CRM 中的客户记录。
- 从项目管理系统读取:当用户问“我当前的 Bug 修复进度如何?”,Agent 应该能通过 API 去查询 Jira 或 Asana,并将查询结果作为“临时记忆”用于生成回答。
- 记忆的导出与审计:出于合规和调试目的,需要能够导出和查看 AI 在某个会话中形成的所有“记忆”(对话、实体、检索记录),了解其决策依据。
为 DeepSeek 这类大模型注入记忆,从技术上看是组合使用LangChain的各种Memory类和RAG链。但从产品角度看,这是一个将通用 AI 转化为专用、个性化、可持续交互的智能体的过程。核心在于理解不同记忆组件的适用场景和局限,并在架构设计上做好隔离、优化和扩展。