别让大模型“失忆”:LangChain 记忆机制的三种高阶玩法
一、先从一个让人抓狂的对话说起
如果你真正用过API调用大模型,一定碰到过这种尴尬:你上一句刚说完“我叫张三,在阿里做后端”,下一句问“你还记不记得我在哪上班”,它立马一本正经地回你“抱歉,我没有之前对话的记录”。那一刻你肯定觉得它不是笨,是装傻。
真相其实是,大语言模型本身就像一条“金鱼”——它每一次被调用都是独立的、无状态的。它没有内生的内存条,每次处理你的提问时,对之前发生过什么一概不知。可我们人类聊天是连续的,没有人会每说一句话就把自己的履历重新报一遍。这个矛盾不解决,AI应用就永远停留在“一问一答”的玩具阶段,做不了真正的智能助手。
LangChain的Memory模块就是专门来填这个坑的。它模仿了我们大脑里“记住近期细节”和“压缩储存长期信息”的两套系统,提供了好几种不同的记忆策略。这篇文章我不打算把文档抄一遍,而是带你亲手跑一跑三种真正能在生产环境里扛住压力的高阶玩法。我会尽量把每一步为什么这么做讲清楚,代码也完整贴出来,你只要装了Python和对应的库,跟着敲就能看到效果。
二、第一招:只记最近几轮——滑动窗口记忆
最偷懒也最直观的想法,就是把所有聊过的内容一股脑存下来,每次提问都原封不动塞给模型。LangChain确实有个ConversationBufferMemory是这么干的。但只要你跟它聊上二三十轮,账单就会教你做人——Token消耗直线飙升,而且很容易直接把模型的上下文窗口撑爆。
于是有了滑动窗口这个折中方案:干脆只留最近K轮对话,更早的统统扔掉。就像我们短期记忆一样,能清晰记得刚说完的几句话,但十分钟前聊的细节基本就模糊了。
动手写代码之前,先把依赖装好(如果你用的是OpenAI接口,就这么装):
pip install langchain langchain-openai langchain-chroma chromadb
下面这段代码创建了一个只保留最近3轮对话的记忆容器:
from langchain_openai import ChatOpenAI
from langchain.chains import ConversationChain
from langchain.memory import ConversationBufferWindowMemory
llm = ChatOpenAI(
temperature=0.7,
openai_api_key="YOUR_API_KEY_HERE" # 换成你自己的Key
)
memory = ConversationBufferWindowMemory(
k=3, # 这个参数就是窗口大小
return_messages=True
)
conversation = ConversationChain(
llm=llm,
memory=memory,
verbose=True # 打开这个你能看到每次塞给模型的完整内容
)
然后我们模拟几轮对话试试看:
conversation.predict(input="你好,我叫张三,在字节跳动做推荐算法。")
conversation.predict(input="最近我们团队在尝试用大模型做用户画像。")
conversation.predict(input="你还记得我叫什么吗?")
# 这时候肯定记得,因为“我叫张三”还在窗口里
conversation.predict(input="大模型做画像的成本有点高,我们在找优化方案。")
conversation.predict(input="现在再问你一遍,我叫什么?")
# 大概率已经忘了,因为最早那句自我介绍被挤出了窗口
这个方案的脾气你摸清了吧:它的Token消耗是恒定的,不会随着对话变长而涨价,而且原文保留,没有任何信息扭曲。但代价就是窗口外的信息彻底消失,AI不会给你任何提示,只会坦然地承认不知道。它最适合那种轮次有限、不太需要追溯前情的场景,比如售后客服的简单问答、或者临时性的信息查询。要是你的应用需要跨越多轮做长程规划,下面这一招会更靠谱。
三、第二招:让AI自己写“会议纪要”——摘要记忆
滑动窗口忘得太干脆了,像极了考完试就把课本内容全部清空的学生。摘要记忆的思路则聪明得多:它不丢弃历史,而是每过几轮就让LLM自己动手,把之前的对话压缩成一段精简的摘要。这个摘要会越写越长、越写越完整,但始终控制在比较小的Token规模内。
你可以把它理解成一个自动更新的会议纪要——最初的纪要是“用户叫张三,在字节做推荐算法”,聊了几轮之后纪要变成“用户叫张三,在字节做推荐算法,团队正在尝试用大模型做用户画像,目前受困于成本问题”。这样一来,即使聊了一百轮,传给模型的也不是上万字的原始聊天记录,而是一份精炼的背景说明。
代码写法跟前面很像,但注意ConversationSummaryMemory必须传入一个llm实例,因为它内部要靠模型本身来生成摘要:
from langchain_openai import ChatOpenAI
from langchain.chains import ConversationChain
from langchain.memory import ConversationSummaryMemory
llm = ChatOpenAI(
temperature=0.7,
openai_api_key="YOUR_API_KEY_HERE"
)
memory = ConversationSummaryMemory(
llm=llm, # 这个必须传,摘要是模型自己写的
return_messages=True
)
conversation = ConversationChain(
llm=llm,
memory=memory,
verbose=True
)
conversation.predict(input="你好,我是张三,字节跳动的推荐算法工程师。")
conversation.predict(input="我们团队最近在尝试用大模型做用户兴趣画像。")
conversation.predict(input="最大的难点是每天几百万活跃用户的实时推理成本太高。")
conversation.predict(input="我正在调研LangChain,看能不能用它的记忆机制做缓存优化。")
# 即使过了很多轮,问它核心痛点,它依然能答出来
conversation.predict(input="根据我们前面的对话,我现在最头疼的问题是什么?")
# 它会回答“实时推理成本太高”,因为摘要里牢牢记着这条
LangChain还贴心地提供了一个混血版本ConversationSummaryBufferMemory,它把滑动窗口和摘要揉在了一起:最近几轮的对话原文保留不动,只有超出窗口的旧内容才被压缩成摘要。你可以通过max_token_limit来控制什么时候触发摘要动作,比纯摘要更灵活,细节保留得也更好。
from langchain.memory import ConversationSummaryBufferMemory
memory = ConversationSummaryBufferMemory(
llm=llm,
max_token_limit=2000, # 对话内容超过2000个token时,自动把最早的部分做成摘要
return_messages=True
)
这种方案的代价也很直接:每次更新摘要都要额外调用一次LLM,相当于多花了一份算力。而且摘要本身就是“有损压缩”,一些具体的数字、日期或者细枝末节的表述可能会在压缩中丢失。它特别适合长篇写作辅助、多步骤任务规划这类需要长时间保持主线清晰的场景。如果你既想要长记忆,又不想丢掉任何细节,那向量数据库那一套可能才是你的最终归宿。
四、第三招:给AI装一个“外挂海马体”——向量数据库记忆
前面两种方案本质上都在做一件事——压缩或者截断。但仔细想想,人类真正记住事情的方式并不是把所有经历压缩成一段话,也不是只记得最近的事。我们大脑里的海马体干的是另一件事:把重要的经历编码成特征向量,存起来,等到需要的时候再根据语义关联把它们“回想”起来。
向量数据库记忆方案模拟的就是这套机制。它的运行逻辑拆开看就两步,但每一步都很关键:存储时,每轮对话结束后,Embedding模型会把文本转成浮点数向量,然后存进向量数据库里;检索时,当用户提出新问题,系统先把问题转成向量,再去数据库里做语义相似度搜索,找出最相关的几条历史记录,最后把这些记录塞进Prompt里当作背景信息。
也就是说,AI不需要记住每一句话,也不需要把整本日记从头读到尾。它只需要在每次回答问题的时候,翻一翻“记忆库”,找出跟当前问题最相关的那几段往事就够了。
代码实现稍微长一点,但逻辑其实很清晰,我拆成五步来写,每一步都加了注释:
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_chroma import Chroma
from langchain.memory import VectorStoreRetrieverMemory
from langchain.chains import ConversationChain
from langchain.prompts import PromptTemplate
llm = ChatOpenAI(
temperature=0.7,
openai_api_key="YOUR_API_KEY_HERE"
)
# 1. 准备Embedding模型和向量数据库(数据会持久化到本地磁盘)
embeddings = OpenAIEmbeddings(openai_api_key="YOUR_API_KEY_HERE")
vectorstore = Chroma(
collection_name="agent_memory",
embedding_function=embeddings,
persist_directory="./chroma_db" # 程序重启后记忆依然在
)
# 2. 创建一个检索器,每次检索最相关的2条记忆
retriever = vectorstore.as_retriever(search_kwargs={"k": 2})
# 3. 把检索器包装成LangChain标准的Memory对象
memory = VectorStoreRetrieverMemory(
retriever=retriever,
memory_key="chat_history",
input_key="input"
)
# 4. 写一个Prompt模板,把检索到的历史记录插进去
template = """
你是一个有用的AI助手。以下是和当前问题相关的历史对话记录:
{chat_history}
人类的新问题:{input}
你的回答:"""
prompt = PromptTemplate(input_variables=["chat_history", "input"], template=template)
# 5. 组装成对话链
conversation = ConversationChain(
llm=llm,
memory=memory,
prompt=prompt,
verbose=True
)
然后我们来模拟一个话题来回跳跃的真实对话:
conversation.predict(input="你好,我叫张三,字节跳动的推荐算法工程师。")
conversation.predict(input="我们团队在做用户兴趣画像,用的是双塔模型。")
conversation.predict(input="我周末喜欢去郊野公园爬山,尤其爱走野路。")
conversation.predict(input="上周我们上线了新版本,CTR提升了3个百分点。")
conversation.predict(input="我平时做饭也挺多的,拿手菜是红烧肉。")
# 问一个工作相关的问题,它会精准召回工作那条记忆
conversation.predict(input="我们团队上线的那个新版本,效果怎么样?")
# 回答里会提到“CTR提升了3个百分点”
# 再问一个生活相关的问题,它不会混淆,会去召回爬山和做饭的记忆
conversation.predict(input="我周末有什么爱好?")
# 它会准确说出“爬山”和“做饭”,而不是瞎编
看到没,它不会一股脑把所有历史都塞进去,也不会自作主张地概括丢掉细节。它像个训练有素的档案管理员,你问什么,它就翻出对应的档案给你。
这套方案的天花板很高,特别适合超长对话、跨会话的长期记忆、以及结合外部知识库的问答系统。代价嘛,就是你需要多维护一个向量数据库,而且每次提问都多了一次Embedding计算和检索的耗时。但如果你的应用目标是做一个真正“认识用户”的AI伙伴,这笔开销是值得的。
五、选哪个?别纠结,它们不是对手,是队友
写到这儿你可能有点选择困难。其实不用把它们看成三选一的单选题。滑动窗口简单粗暴,适合短平快的交互;摘要记忆中庸稳健,适合需要长程主线但预算有限的场景;向量数据库虽然复杂,但给了你近乎无限的记忆容量和语义检索能力。
在实际落地的项目里,这三者经常是混着用的。比如拿滑动窗口处理最近三分钟的闲聊细节,拿摘要记忆沉淀整个会话的核心脉络,再拿向量数据库做跨会话的用户画像持久化——各司其职,相互配合。LangChain官方文档里也隐含着这个思路:短期记忆管好上下文连贯,长期记忆管好用户身份和偏好,两手都要抓。
代码全部贴在上面了,别光看,找个周末的下午亲手敲一遍跑一跑。把verbose=True打开,仔细观察每次调用时Prompt里到底塞进去了什么,你会对这三种记忆机制的理解比只看文档深刻得多。踩坑是学习的一部分,跑不通的时候多看看报错信息,调整一下参数,这个过程本身就是最好的老师。
更多推荐
所有评论(0)