本节摘要:模型本身无状态,每轮对话的"记忆"全靠把历史重新拼进提示词。LangChain 的记忆模块提供缓冲、窗口、摘要、实体四类记录本与配套裁剪策略,本节逐个开箱并演示新旧两种挂线方式。
一个常被忽略的事实:模型不记得任何上一轮。你感觉客服"记得"你说过的话,是因为每次调用前,程序把历史消息重新塞进了提示词。所谓记忆模块,干的就是这本工序记录本的活:每轮把历史裁剪到合适长度、拼进工艺单、再把本轮对话追加回去。
先手工模拟一遍这个过程,看记录本要解决什么:
history = [] # 手工记录本 def talk(question: str, llm) -> str: # 每轮手工把全部历史拼进提示词 text = "以下是对话历史:\n" + "\n".join(history) text += f"\n请回答新问题:{question}" answer = llm.invoke(text).content history.append(f"问:{question}") history.append(f"答:{answer}") return answer from langchain_openai import ChatOpenAI llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0) print(talk("我叫小舟,在做袜子生意", llm)) print(talk("我刚才说我叫什么", llm)) # 输出示例:你刚才说你叫小舟。
能跑,但三个隐患立刻浮现:历史无限增长直到撑爆上下文;换一个用户就串味(全局变量共享);裁剪策略改起来要动核心逻辑。记忆模块把这三件事标准化。
from langchain.memory import ConversationBufferMemory # 形态一:全量缓冲——原封不动记下所有对话 memory = ConversationBufferMemory() memory.save_context({"input": "我叫小舟"}, {"output": "你好,小舟!"}) print(memory.load_memory_variables({})) # 输出:{'history': 'Human: 我叫小舟\nAI: 你好,小舟!'}
from langchain.memory import ConversationBufferWindowMemory # 形态二:滑动窗口——只保留最近 k 轮,其余自动丢弃 memory = ConversationBufferWindowMemory(k=1) memory.save_context({"input": "第一轮"}, {"output": "回答一"}) memory.save_context({"input": "第二轮"}, {"output": "回答二"}) print(memory.load_memory_variables({})["history"]) # 输出:Human: 第二轮 # AI: 回答二 (第一轮已被窗口裁掉)
from langchain.memory import ConversationSummaryMemory from langchain_openai import OpenAI # 形态三:摘要——用模型把旧对话压成一段总结,长对话不失重点 memory = ConversationSummaryMemory(llm=OpenAI(temperature=0)) memory.save_context({"input": "我叫小舟,卖彩色袜子"}, {"output": "记住了,小舟做彩色袜子生意"}) memory.save_context({"input": "我们主打手绘图案"}, {"output": "好的,手绘图案是特色"}) print(memory.load_memory_variables({})["history"]) # 输出示例:小舟经营彩色袜子生意,主打手绘图案特色。
from langchain.memory import ConversationTokenBufferMemory # 形态四:按 token 预算裁剪——不超过模型上下文的硬约束 memory = ConversationTokenBufferMemory( llm=OpenAI(), max_token_limit=60) memory.save_context({"input": "很长很长的一段自我介绍"}, {"output": "简短回应"}) print(memory.load_memory_variables({})["history"]) # 超预算的最早轮次被裁掉
四种记录本的取舍一表看清:
| 形态 | 记住多少 | 成本 | 适用 |
|---|---|---|---|
| 全量缓冲 | 全部 | 随轮次线性涨 | 短对话、调试 |
| 滑动窗口 | 最近 k 轮 | 恒定 | 闲聊、上下文弱的场景 |
| 摘要 | 压缩后的要点 | 每轮多一次模型调用 | 长咨询、客服 |
| token 裁剪 | 预算内尽量多 | 恒定 | 生产默认 |
旧式挂法用 ConversationChain,一行参数带记忆的对话链就转起来:
from langchain.chains import ConversationChain from langchain.memory import ConversationBufferMemory from langchain_openai import OpenAI conversation = ConversationChain( llm=OpenAI(temperature=0), memory=ConversationBufferMemory(), verbose=True, ) print(conversation.predict(input="我叫小舟,在做彩色袜子生意")) print(conversation.predict(input="我叫什么,做什么生意")) # 第二问输出示例:你叫小舟,经营彩色袜子生意。
新式挂法用 RunnableWithMessageHistory 包住 LCEL 管道,会话隔离由编号完成——1.1 节客服翻车的"多用户串味"在这里根治:
from langchain_core.runnables import RunnableWithMessageHistory from langchain_core.chat_history import InMemoryChatMessageHistory from langchain_core.prompts import ChatPromptTemplate from langchain_openai import ChatOpenAI store = {} # 会话仓库:生产环境换成 Redis 或数据库 def get_history(session_id: str): if session_id not in store: store[session_id] = InMemoryChatMessageHistory() return store[session_id] prompt = ChatPromptTemplate.from_messages([ ("system", "你是袜子品牌顾问"), ("placeholder", "{chat_history}"), ("human", "{question}"), ]) chat = prompt | ChatOpenAI(temperature=0) line = RunnableWithMessageHistory( chat, get_history, input_messages_key="question", history_messages_key="chat_history", ) cfg = {"configurable": {"session_id": "user-001"}} print(line.invoke({"question": "我叫小舟"}, config=cfg).content) print(line.invoke({"question": "我叫什么"}, config=cfg).content) # 输出示例:你叫小舟。
⚠️ 内存仓库重启即失。生产上把 InMemoryChatMessageHistory 换成 RedisChatMessageHistory,连接参数在构造时传入,其余代码不动——这正是"记录本可替换"的设计意图。
💡 记忆策略的隐藏成本:摘要型每轮多一次模型调用,等于把"记性好"换成了"话费高"。用户对话普遍不超过十轮的产品,窗口型加 token 裁剪就够,别一上来就上摘要。
把窗口型的 k 从 1 调到 5,重跑上面的两问实验,观察第二轮回答的细节变化;再把两个不同 session_id 各聊两句,验证仓库确实隔离。五分钟的实验胜过背十遍概念。下一节进入知识供给线:原料(文档)怎么进车间。