2.5 工序记录本:记忆模块


2.5 工序记录本:记忆模块

本节摘要:模型本身无状态,每轮对话的"记忆"全靠把历史重新拼进提示词。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 各聊两句,验证仓库确实隔离。五分钟的实验胜过背十遍概念。下一节进入知识供给线:原料(文档)怎么进车间。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U