引言:如果你的AI客服和用户聊了20轮后突然问"您贵姓",这篇文章就是写给你看的。
一、那个让人尴尬的线上事故
去年夏天,我们上线了一个AI客服机器人。内测时一切正常,回答流畅、逻辑清晰,产品经理很满意。
正式上线第三天,客服主管在群里发了一张截图,我看完脸都红了。
截图里,用户和机器人已经聊了18轮。用户从咨询"运费怎么算",聊到"我去年买的耳机坏了能不能保修",再聊到"你们和XX品牌哪个好"。机器人一直应对自如,直到用户突然问了一句:
"对了,我一开始问你的问题,你还记得吗?"
机器人回复:"您好,请问您想了解哪方面的问题呢?"
18轮对话,一键清零。
用户当场在对话框里打了一串省略号,然后点了"转人工"。
我连夜排查,发现问题出得极其低级:我们的代码根本没有实现对话记忆。每一轮请求都是独立的,模型看到的永远是"当前这一轮",前面的对话历史根本没有传给它。
那一刻我才真正理解:大模型本身没有记忆。它的"记忆",全靠我们把历史对话塞进Prompt里。
但怎么塞、塞多少、塞什么,这是一门学问。塞少了,AI失忆;塞多了,Token爆炸、成本飙升、响应变慢。LangChain的Memory模块,就是用来解决这个问题的。但Memory本身也在进化------2024年之后,LangChain官方推荐了一套全新的范式,旧版的ConversationChain正在被淘汰。
这篇文章,我会用一个完整的客服对话场景,带你搞懂LangChain四种Memory的优劣,以及为什么你应该拥抱新范式。
二、大模型为什么没有记忆?
在深入AI记忆机制之前,先澄清一个常见的误解。
很多人以为,和GPT多轮对话时,它"记得"之前聊过什么。其实不是。每一次API调用,模型接收的都是完整的消息列表------包括系统提示、历史对话、当前输入。模型处理完,返回结果,然后这次调用的上下文就被销毁了。
所谓的"记忆",完全是客户端的责任。
你的程序需要在每次请求前,把之前的历史对话拼进Prompt里,发给模型。模型根据这些历史消息"假装"自己有记忆。历史消息越多,模型看到的上下文越长,Token消耗越大,响应越慢。
这就带来了一个核心矛盾:记忆完整度 vs Token成本控制。LangChain的四种Memory类型,本质上就是在这两个维度上做不同的trade-off。
三、四种Memory实战对比:一个客服对话场景
为了直观展示四种Memory的差异,我设计了一个真实的客服对话场景,模拟用户和AI客服的6轮对话:
| 轮次 | 用户 | AI客服 |
|---|---|---|
| 1 | 你好,我想买一台笔记本电脑,预算8000左右 | 您好!8000预算可以看看我们的轻薄本系列,推荐小新Pro 16,性价比很高。 |
| 2 | 我平时主要用来写代码和偶尔剪视频 | 了解,那推荐您选32G内存+1T固态的版本,编程和剪辑都够用。 |
| 3 | 好的,我姓王,帮我备注一下 | 好的王先生,已为您备注。 |
| 4 | 对了,你们支持分期付款吗? | 支持,我们提供3/6/12期免息分期,您可以在结算页选择。 |
| 5 | 我之前在你们店买过一台显示器,质量挺好的 | 感谢您的认可!老用户下单可额外享受50元优惠券,我帮您自动领取。 |
| 6 | 好的,那我还是选刚才那台电脑吧,帮我加急发货 | ... |
第6轮的关键在于:AI需要同时记住"用户姓王"、"推荐的是小新Pro 16 32G+1T版本""用户是老客户有50元优惠券""用户要求加急发货"这些信息。如果Memory策略选择不当,要么Token爆炸,要么关键信息丢失。
下面我们逐一测试四种Memory在这个场景下的表现。
第一种:ConversationBufferMemory------全量记录的"老实人"
ConversationBufferMemory是最简单、最直观的Memory。它的策略就一个:全部记下来,原封不动地传给模型。
python
from langchain.memory import ConversationBufferMemory
from langchain.chains import ConversationChain
memory = ConversationBufferMemory(return_messages=True)
conversation = ConversationChain(llm=llm, memory=memory)
在第6轮时,它会把前面5轮共10条消息(用户+AI各5条)全部塞进Prompt。好处是信息零丢失,模型能看到完整的对话上下文,"王先生""小新Pro 16""50元优惠券"这些细节一个不落。
但代价是Token消耗随轮次线性增长。
假设每轮对话平均消耗200个Token,到第6轮时,仅历史消息就占了1000个Token,加上系统提示和当前输入,单次请求可能突破1500 Token。如果对话进行到20轮、50轮呢?Token成本会翻倍,响应延迟明显上升,而且很多模型有上下文长度限制(比如4K、8K、128K),一旦超限,直接报错或截断。
适用场景:短对话(5轮以内)、对上下文完整性要求极高的场景(如法律咨询、医疗问诊)。
致命缺陷:长对话下的Token爆炸。
第二种:ConversationBufferWindowMemory------滑动窗口的"金鱼记忆"
ConversationBufferWindowMemory的策略是:只保留最近k轮对话,之前的全部扔掉。
python
from langchain.memory import ConversationBufferWindowMemory
memory = ConversationBufferWindowMemory(k=2, return_messages=True)
conversation = ConversationChain(llm=llm, memory=memory)
如果设置k=2,到第6轮时,模型只能看到第4、5、6轮的内容。第1轮推荐的"小新Pro 16"、第3轮提到的"姓王",全都被遗忘了。
AI的回复可能是:"好的,请问您需要哪款电脑呢?"------用户当场崩溃。
这就是典型的"失忆"问题。 滑动窗口虽然有效控制了Token消耗(始终只保留固定轮数),但代价是早期关键信息的丢失。如果重要信息出现在窗口之外,AI就会"断片"。
适用场景:对近期上下文敏感、但历史信息不太重要的场景(如闲聊机器人、简单问答)。
致命缺陷:长跨度信息丢失,用户体验断崖式下跌。
第三种:ConversationSummaryMemory------自动摘要的"速记员"
ConversationSummaryMemory的策略更聪明:让AI自己把历史对话总结成一段摘要,然后用摘要替代原始对话。
python
from langchain.memory import ConversationSummaryMemory
memory = ConversationSummaryMemory(llm=llm, return_messages=True)
conversation = ConversationChain(llm=llm, memory=memory)
在第6轮时,它不会传递10条原始消息,而是传递一段类似这样的摘要:
"用户王先生预算8000元购买笔记本电脑,主要用于编程和视频剪辑。推荐了小新Pro 16(32G+1T)。用户是老客户,享受50元优惠券。用户询问过分期付款,支持3/6/12期免息。"
这段摘要可能只有100个Token,远低于原始对话的1000个Token。Token消耗被大幅压缩,同时保留了关键信息。
但摘要机制也有代价:
第一,细节丢失。 "偶尔剪视频"被压缩成"视频剪辑","加急发货"可能在摘要中被忽略。如果用户问"我上次说的加急发货还记得吗?",AI可能一脸懵。
第二,初期Token反而更多。 因为摘要本身需要调用一次LLM来生成,前几轮的Token消耗可能比BufferMemory还高。但随着对话增长,Summary的Token曲线会逐渐平缓,而BufferMemory持续线性上升。
第三,依赖摘要模型的质量。 如果摘要模型抽风,把关键信息漏掉,后面的对话质量会连锁下降。
适用场景:长对话(10轮以上)、对细节不敏感但对主线连贯性要求高的场景(如客服、教育辅导)。
第四种:VectorStoreRetrieverMemory------向量检索的"智能档案柜"
前三种Memory都是按时间维度管理历史:要么全保留,要么留最近,要么全摘要。但真实对话中,用户提到的信息重要性并不均等。
比如用户第1轮说"预算8000",第3轮说"姓王",第5轮说"买过显示器"。到第6轮时,"姓王"和"买过显示器"可能和当前问题相关,但"预算8000"可能已经不再重要了。
VectorStoreRetrieverMemory的策略是:把每轮对话转成向量存进向量数据库,用户提问时,按语义相似度检索最相关的历史片段,只把相关的片段传给模型。
python
from langchain.memory import VectorStoreRetrieverMemory
from langchain_community.vectorstores import Chroma
from langchain_openai import OpenAIEmbeddings
vectorstore = Chroma(embedding_function=OpenAIEmbeddings())
retriever = vectorstore.as_retriever(search_kwargs={"k": 2})
memory = VectorStoreRetrieverMemory(retriever=retriever)
当用户第6轮说"帮我加急发货"时,检索器会从历史中找到语义最相关的片段------可能是第5轮"买过显示器"(老客户身份)和第3轮"姓王"(收件人信息),而第1轮"预算8000"因为语义不相关,被自动过滤掉了。
这种策略的精妙之处在于:它不按时间取舍,而按相关性取舍。 理论上,即使100轮前的对话,只要和当前问题相关,也能被召回。
但代价也很明显:
第一,实现复杂。 需要向量数据库、Embedding模型、检索策略,架构重量远超前三种。
第二,检索质量不稳定。 如果Embedding模型对中文理解不够好,或者用户的表述和历史的表述差异很大,可能召回错误的内容。
第三,无法保证时间线连贯。 它召回的是"相关片段",不是"连续对话"。如果用户问"我们刚才聊到哪了?",这种Memory可能给不出完整的上下文。
适用场景:超长对话(50轮以上)、知识密度高、需要跨轮次信息关联的场景(如技术支持、复杂咨询)。
四、Trade-off全景图:一张表选对Memory
把这四种Memory放在同一个坐标系里对比,选择就变得清晰了:
| Memory类型 | Token消耗 | 信息完整度 | 实现复杂度 | 最佳适用场景 |
|---|---|---|---|---|
| ConversationBufferMemory | 线性增长,高 | 100%保留 | 极低 | 短对话(<5轮)、高完整度要求 |
| ConversationBufferWindowMemory | 固定,低 | 仅保留k轮 | 极低 | 闲聊、简单问答、对历史不敏感 |
| ConversationSummaryMemory | 初期高,后期低 | 主线保留,细节可能丢失 | 低 | 中长对话(10-30轮)、客服、教育 |
| VectorStoreRetrieverMemory | 取决于检索条数,可控 | 相关性高的保留,其余丢失 | 高 | 超长对话、知识密集型咨询 |
核心结论:没有最好的Memory,只有最适合当前场景的Memory。
如果你的对话平均3-5轮结束,直接用BufferMemory,简单可靠。如果你的对话可能拖到20轮以上,SummaryMemory是性价比最高的选择。如果你在做的是一个需要"长期记忆"的助手(比如陪伴型AI),VectorStoreRetrieverMemory值得投入。
五、新范式:为什么旧版Memory正在被淘汰
讲到这里,你可能会问:上面这些代码都是ConversationChain的写法,但网上很多教程说这种写法已经过时了。怎么回事?
没错,旧版的ConversationChain + ConversationBufferMemory这套组合,LangChain官方已经不再推荐用于新项目了。
2024年之后,LangChain推出了基于LCEL(LangChain Expression Language)的新范式,核心变化就一句话:记忆不再是链的"黑盒属性",而是显式注入Prompt的"独立组件"。
旧范式的问题
旧版代码长这样:
python
from langchain.chains import ConversationChain
from langchain.memory import ConversationBufferMemory
memory = ConversationBufferMemory()
chain = ConversationChain(llm=llm, memory=memory)
response = chain.predict(input="你好")
这段代码的问题在于:记忆的管理是隐式的 。你根本不知道历史消息是怎么被塞进Prompt的,也不知道它在哪一步被截断了。出了问题,你只能打印memory.buffer猜。而且ConversationChain不支持LCEL的管道符语法,无法和现代的prompt | llm | parser流水线混用。
新范式:RunnableWithMessageHistory
新范式的核心是两个组件:BaseChatMessageHistory(存储)+ RunnableWithMessageHistory(注入)[93]。
python
from langchain_core.chat_history import InMemoryChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_openai import ChatOpenAI
# 1. 定义提示词模板,显式声明历史消息的位置
prompt = ChatPromptTemplate.from_messages([
("system", "你是一位电商客服助手。"),
MessagesPlaceholder(variable_name="chat_history"), # 历史消息显式占位
("human", "{input}"),
])
# 2. 用LCEL组装链
chain = prompt | ChatOpenAI(model="gpt-4o")
# 3. 定义会话历史存储(可用Redis/PostgreSQL替换)
store = {}
def get_session_history(session_id: str):
if session_id not in store:
store[session_id] = InMemoryChatMessageHistory()
return store[session_id]
# 4. 包装成带记忆的链
chain_with_history = RunnableWithMessageHistory(
chain,
get_session_history,
input_messages_key="input",
history_messages_key="chat_history",
)
# 5. 调用时指定session_id
response = chain_with_history.invoke(
{"input": "你好,我想买一台笔记本"},
config={"configurable": {"session_id": "user_123"}}
)
新范式的优势非常明显:
第一,记忆是显式的。 你在Prompt模板里用MessagesPlaceholder明确标出了历史消息的位置,一目了然。
第二,存储和链解耦。 get_session_history可以返回内存存储、Redis、PostgreSQL、MongoDB------近50种持久化方案任选,切换存储后端不需要改链的代码。
第三,天然支持多会话隔离。 通过session_id区分不同用户,就像微信不同聊天窗口的记录互不干扰。
第四,兼容LCEL生态。 可以和prompt | llm | parser流水线无缝衔接,支持流式输出、异步调用、批量处理等高级特性。
第五,你可以自己实现Memory策略。 因为历史消息的存取完全由你控制,你可以在上面实现滑动窗口、自动摘要、向量检索------任何策略。LangChain只提供了基础设施,策略由你决定。
新范式下如何实现四种Memory机制
在新范式下,四种Memory的区别不再是"选哪个类",而是"get_session_history函数怎么写":
- BufferMemory :直接返回
InMemoryChatMessageHistory(),不做任何处理; - WindowMemory :在返回前截取
history.messages[-2*k:]; - SummaryMemory:在返回前调用LLM生成摘要,替换原始消息;
- VectorStoreMemory:把消息向量化存储,返回时按相似度检索。
核心思想从"选一个Memory类"变成了"写一个历史消息处理函数"。 控制权完全在你手里。
六、结语:记忆管理的三个段位
做AI应用这一年多,我对记忆管理的理解也经历了三个阶段:
-
青铜段位:没有记忆。 每轮请求都是独立的,AI像金鱼一样转身就忘。用户聊到第3轮就开始重复自我介绍。
-
白银段位:全量记忆。 用
ConversationBufferMemory把历史全塞进去,简单暴力。短对话没问题,长对话Token爆炸,成本账单让人心疼。 -
黄金段位:策略化记忆。 根据业务场景选择Memory策略,短对话用Buffer,长对话用Summary,超长对话用VectorStore。并且拥抱LCEL新范式,把记忆的存取和注入显式化、可配置化、可持久化。
记忆管理是AI应用开发中最容易被低估的环节。 很多人把精力花在Prompt调优和模型选型上,却忽略了"AI记不记得住"这个基础问题。一个记忆策略选错的客服机器人,比模型选错的客服机器人更让用户崩溃------因为前者会在第10轮突然问你"您贵姓",而后者只是回答得不够完美。
LangChain的新范式给了我们足够的灵活性,但灵活性也意味着责任。你需要理解每种策略的trade-off,需要根据业务场景做选择,需要在Token成本和用户体验之间找到平衡点。
毕竟,给用户最好的体验,不是让AI记住一切,而是让AI记住该记住的,忘记该忘记的。