LangChain的组件------Memory组件
Memory组件介绍
LangChain的Memory组件是构建有状态对话应用(如聊天机器人)的核心模块。让LLM能够记住之前的交互内容,从而实现多轮对话。之前也提到过LLM本身是无状态的,每次调用独立,不会保留历史消息,在之前的示例中为了实现多轮对话,我们是不断将所有历史消息+大模型的回答+新的用户问题一起给大模型输入,才能实现记忆功能。
而Memory组件本质上也是通过这种方式,只是将这些功能封装起来,按照既定规则调用即可。Memory组件负责存储、管理、注入历史对话信息,使得模型能够理解上下文。
Memory的核心工作流程
- 存储:每次用户输入和模型输出,都会存入指定介质。
- 加载:调用LLM前,Memory将历史消息格式化成合适的字符串或消息列表,拼接到prompt中。
- 清理:长对话可以通过压缩摘要,或者只保留最近N轮,防止token超限。
常见的Memory类型
按照内容保留策略划分
| 类型 | 存储方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 全量记忆 | 保留全部历史对话 | 信息零丢失,简单可靠 | token消耗线性增长 | 短对话 |
| 窗口记忆 | 只保留最近K轮对话 | token可控 | 超出窗口的历史信息永久丢失 | 中长对话,但能接受早期信息丢失 |
| 摘要记忆 | 定期调用LLM把历史信息压缩成摘要 | token消耗低 | 压缩时额外的LLM成本,可能丢失细节 | 超长时间对话,但能接受细节丢失 |
全量记忆
实现一个带完整历史记忆的对话机器人。它能够记住同一个会话(由 session_id 标识)中的所有历史消息,并在后续对话中将这些历史自动注入到提示词中,让 LLM 基于完整上下文回答。历史记录暂时存储在内存中(适合测试/开发),生产环境可替换为数据库。
python
llm = ChatOpenAI(api_key=API_KEY, base_url=BASE_URL, model=MODEL_NAME, temperature=0.3)
# 1. 定义提示词模板(包含历史消息占位符)
full_memory_prompt = ChatPromptTemplate.from_messages([
("system", "你是友好的对话助手,需基于完整的历史对话回答用户问题。"),
MessagesPlaceholder(variable_name="chat_history"), # 历史消息占位符
("human", "{user_input}") # 用户当前输入
])
# 2. 构建基础链(提示词 + LLM)
base_chain = full_memory_prompt | llm
# 3. 会话历史存储,键为 session_id,值为 BaseChatMessageHistory 对象(内存模式,生产环境可替换为数据库存储)
full_memory_store = {}
# 4. 定义会话历史获取函数(核心:返回完整历史)
def get_full_memory_history(session_id: str) -> BaseChatMessageHistory:
"""根据session_id获取会话历史,不存在则创建新的历史记录"""
if session_id not in full_memory_store:
full_memory_store[session_id] = InMemoryChatMessageHistory()
return full_memory_store[session_id]
# 5. 构建带全量记忆的对话链
full_memory_chain = RunnableWithMessageHistory(
runnable=base_chain,
get_session_history=get_full_memory_history,
input_messages_key="user_input", # 输入中用户问题的键名
history_messages_key="chat_history" # 传入提示词的历史消息键名
)
# 测试多轮对话(指定session_id=user_001,隔离不同用户)
config = {"configurable": {"session_id": "user_001"}}
# 第一轮对话
response1 = full_memory_chain.invoke({"user_input": "我叫小明,喜欢编程"}, config=config)
print("助手回复1:", response1.content)
# 输出示例:你好小明!编程是一项很有创造力的技能,你平时常用什么编程语言呢?
# 第二轮对话(验证记忆:询问历史信息)
response2 = full_memory_chain.invoke({"user_input": "我刚才说我喜欢什么?"}, config=config)
print("助手回复2:", response2.content)
# 输出示例:你刚才说你喜欢编程呀~
# 查看完整历史记录
print("\n全量记忆的对话历史:")
for msg in get_full_memory_history("user_001").messages:
print(f"{msg.type}: {msg.content}")
代码解释
提示词模板定义:
ChatPromptTemplate.from_messages是LangChain中构建多角色、结构化聊天提示模板的和新方法,它可以通过显示定义消息角色与内容,精准控制对话上下文,常用于开发聊天机器人、多轮对话应用。
特点:
- 它专为聊天模型设计,原生支持5种角色:system、human/user、ai/assistant,其中human和user等价,ai和assitant等价。
- 自动变量解析:模板内 {变量名} 会被自动识别为输入参数,无需手动声明。
- 返回标准对象:生成 ChatPromptTemplate 实例。
ChatPromptTemplate.from_messages()的参数支持4种类型,分别为: - 元组,通过元组定义角色、模板字符串。上面代码中所使用的就是元组
- 纯字符串,会自动转为HumanMessagePromptTempate,也就是如果方法中参数为
"Hello,LangChain",实际上等价于("human", "Hello,LangChain") - 消息模板对象SystemMessagePromptTemplate / HumanMessagePromptTemplate
- 消息实例SystemMessage / HumanMessage
MessagesPlaceholder
- MessagesPlaceholder:是一个消息占位符,运行时会被替换为一组消息,它是构建包含对话历史、多轮上下文或可变长度消息列表的提示词模板的核心工具。
- 在上面代码中的作用是:告诉模板这里要动态插入一段历史消息列表,variable_name="chat_history"指定了后续传入该组消息时使用的参数名。ChatPromptTemplate.from_messages最终生成的提示词结构为:system + 历史消息列表 + 最新用户问题。
- MessagesPlaceholder常与RunnableWithMessageHistory配合使用。
回想一下之前文章中的多轮对话代码,我们是有一个history的消息列表,其中有固定提示词,每次将新的回复与问题都拼接到列表之后,再让大模型回答,而有了MessagesPlaceholder,我们不需要手动拼接。
在上一篇文章中提到了ChatPromptTemplate也是其中一种提示词模板类型,那PromptTemplate和ChatPromptTemplate什么区别呢?
| 对比维度 | PromptTemplate | ChatPromptTemplate |
|---|---|---|
| 作用 | 为文本补全模型 生成单一字符串提示 | 为聊天模型 生成结构化消息列表(带角色和内容) |
| 输出格式 | 纯字符串 | 消息列表,比如SystemMessage,HumanMessage,AIMessage |
| 构造方式 | 主要使用from_template方法,传入字符串模板,自动解析变量 | 可使用from_messages、from_template(转为单条human消息) 等 |
| 角色区分 | 无角色概念,输出是纯文本字符串 | 支持system/human/ai等角色,符合聊天模型API规范 |
| 适用场景 | 简单,适合单一输入输出任务 | 更适合多轮对话、系统指令等场景 |
构建基础链
使用 LangChain 的管道运算符 |,将提示词模板和 LLM 组合成一个 Runnable 链。
输入:包含用户输入 user_input 和聊天历史 chat_history 的字典(管道的第一环节ChatPromptTemplate要求输入字典来填充模板中的所有变量,即普通占位符和MessagesPlaceholder,通过字典的键用来区分不同变量)。
输出:LLM 的响应(AIMessage 对象)。
会话历史获取函数
该函数是构建对话链时 RunnableWithMessageHistory 要求提供的回调,根据 session_id 返回对应的历史消息管理器。
InMemoryChatMessageHistory:LangChain 内置的内存历史类,为对话提供一个临时的"聊天记录本"。它为无状态的 LLM 提供了对话所需的短期记忆。提供 add_user_message(), add_ai_message(), messages 列表等方法。
InMemoryChatMessageHistory的 核心作用:对话的临时"记录本"
在对话中,LLM 模型本身是无状态的,无法记住上下文。InMemoryChatMessageHistory 解决了这个问题。它就像一个临时的"记录本":
- 存储与读取:它能将用户和 AI 的每一轮对话都记录在内存中,当需要时,可以快速读取所有历史记录。
- 保持连续性:通过这种方式,AI 能够"记得"刚才聊过的内容,从而让多轮对话变得连贯,不再"失忆"。
InMemoryChatMessageHistory更适合本地开发与单元测试场景,不适用于生产环境。
在 LangChain 的标准流程中,InMemoryChatMessageHistory 通常与 RunnableWithMessageHistory 配合工作。
全量带记忆的对话链
RunnableWithMessageHistory 是一个包装器,负责自动管理消息历史,会自动保存每一轮的用户输入和 AI 回复,无需手动调用 add_message:
- 每次调用 invoke 时,根据 config 中的 session_id 调用 get_session_history 创建或获取一个专属的 InMemoryChatMessageHistory 历史对象实例。
- 读取记忆并注入上下文:从历史对象中取出所有历史消息chat_history,连同用户最新的问题,一起发送给base_chain 。
- 更新记录:base_chain生成回答后,包装器会自动将刚刚的问答(用户问题 + LLM回答)添加到"记录本"里,为下一轮对话做准备。
对话标识
LangChain的 标准配置格式,configurable 可放置任意键值对。RunnableWithMessageHistory 会读取其中的 session_id 来区分不同会话。
多轮对话的第一轮
调用 invoke,传入用户输入和配置。
内部流程:
根据 session_id = "user_001" 获取历史对象(此时为空)。
自动将用户消息 "我叫小明,喜欢编程" 添加到历史对象。
构建提示词:系统消息 + 空历史 + 当前用户输入。
LLM 生成回答(例如询问喜欢什么编程语言)。
将 AI 回答自动添加到历史对象。
输出 response1.content 提取 AI 消息的文本内容。
第二轮
同一 session_id,因此历史对象已包含第一轮的用户消息和 AI 消息。
自动将新用户输入追加到历史。
提示词中包含完整的两轮对话历史,LLM 能正确回答"你喜欢编程"。
窗口记忆
以下代码中与全量记忆代码唯一区别就是会话历史获取函数的定义,代码注释比较全,就不再详细解释。
python
import os
from dotenv import load_dotenv
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.chat_history import BaseChatMessageHistory, InMemoryChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_openai import ChatOpenAI
# 加载环境变量(确保.env文件中配置了API_KEY)
load_dotenv()
API_KEY = os.getenv("API_KEY")
BASE_URL = os.getenv("BASE_URL")
MODEL_NAME = os.getenv("MODEL_NAME")
print("API_KEY=",API_KEY,'\nBASE_URL=',BASE_URL,'\nMODEL_NAME=',MODEL_NAME)
# 初始化LLM模型
llm = ChatOpenAI(api_key=API_KEY, base_url=BASE_URL, model=MODEL_NAME, temperature=0.3)
# 定义提示词模板(与全量记忆通用)
window_memory_prompt = ChatPromptTemplate.from_messages([
("system", "你是友好的对话助手,需要基于最近的对话历史回答用户问题"),
MessagesPlaceholder(variable_name="chat_history"),
("human", "{user_input}")
])
# 构建基础链
window_base_chain = window_memory_prompt | llm
# 会话历史存储
window_memory_store = {}
WINDOW_SIZE = 2
# 4. 定义带窗口限制的会话历史获取函数
def get_window_memory_history(session_id: str) -> BaseChatMessageHistory:
"""获取会话历史,仅保留最近WINDOW_SIZE轮对话"""
if session_id not in window_memory_store:
window_memory_store[session_id] = InMemoryChatMessageHistory()
# 获取完整历史,截取最近WINDOW_SIZE轮(每轮2条消息)
history = window_memory_store[session_id]
if len(history.messages) > 2 * WINDOW_SIZE:
# 截取后WINDOW_SIZE轮消息(保留最新的)
history.messages = history.messages[-2 * WINDOW_SIZE:]
return history
# 5. 构建带窗口记忆的对话链
window_memory_chain = RunnableWithMessageHistory(
runnable=window_base_chain,
get_session_history=get_window_memory_history,
input_messages_key="user_input",
history_messages_key="chat_history"
)
# 测试多轮对话(session_id=user_002,与全量记忆会话隔离)
config = {"configurable": {"session_id": "user_002"}}
# 模拟5轮对话,验证窗口记忆的截断效果
inputs = [
"我叫小红",
"我喜欢画画",
"我来自上海",
"我是一名学生",
"我刚才说我来自哪里?", # 第5轮:询问第3轮的信息,验证窗口截断
"我叫什么名字?" # 第6轮:询问第1轮的信息,验证窗口记忆
]
for i, user_input in enumerate(inputs, 1):
response = window_memory_chain.invoke({"user_input": user_input}, config=config)
print(f"\n第{i}轮 - 助手回复:", response.content)
# 查看窗口记忆的最终历史(仅保留最近2轮)
print("\n窗口记忆的最终对话历史(最近2轮):")
for msg in get_window_memory_history("user_002").messages:
print(f"{msg.type}: {msg.content}")
摘要记忆
实现一个带有对话摘要功能的记忆链。完整会把完整的聊天历史全部传给LLM,当对话轮次增多时,历史文本会变得很长,导致:Token 消耗巨大(成本高)、可能超过 LLM 的上下文窗口长度、无关信息干扰模型回答质量。
摘要记忆不再传递原始历史,而是定期(此处为每一轮)生成一段极简摘要,将摘要注入系统提示,让 LLM 基于摘要 + 当前用户输入来回答。同时,系统仍然保存完整历史,用于下一次生成更准确的摘要。
核心代码解释
定义摘要提示词
python
summary_prompt = ChatPromptTemplate.from_messages([
("system", "你是对话摘要助手,需简洁总结以下对话的核心信息(包含用户身份、偏好、关键问题等),不超过50字。"),
("human", "对话历史:{chat_history_text}\n请生成摘要:")
])
作用:构造一个提示模板,用于让 LLM 把完整对话历史压缩成一句 50 字以内的摘要。
字段:{chat_history_text} 会被填入格式化的历史文本。
摘要生成链
详细信息可以参考LangChain核心组件------Chains
作用:将提示模板与LLM模型连接成一条链(LCEL语法)。
输入:一个包含 chat_history_text 的字典。
输出:LLM生成的摘要(AIMessage 对象,包含 .content 属性)。
定义对话记忆提示词
python
summary_memory_prompt = ChatPromptTemplate.from_messages([
("system", "你是友好的对话助手,需基于对话摘要回答用户问题,摘要包含核心上下文信息。"),
("system", "对话摘要:{chat_summary}"),
("human", "{user_input}")
])
作用:这是最终回答用户时使用的提示模板。它不包含原始历史,只包含摘要和当前用户输入。
两个 system 消息会被合并成一条系统消息,告诉 LLM 要基于摘要来回答问题。
变量 {chat_summary} 和 {user_input} 将在运行时填充。
构建基础对话链
方式一
python
summary_base_chain = (
RunnablePassthrough.assign(
chat_summary=lambda x: summary_chain.invoke(
{
"chat_history_text": "\n".join(
[f"{msg.type}: {msg.content}" for msg in x["chat_history"]]
)
}
).content
)
| summary_memory_prompt
| llm
)
- 输入:x 是一个字典,必须包含两个键
(1)"chat_history":历史消息列表(List[BaseMessage]),由 RunnableWithMessageHistory 自动注入。
(2)"user_input":当前用户输入。 - RunnablePassthrough.assign(...)
在保留原始输入所有字段的基础上,新增一个 chat_summary 字段,该字段的值通过调用 summary_chain 获得: - 消息格式化
将 x["chat_history"] 中的每条消息格式化为 "type: content" 的字符串(例如 human: 我叫小李,ai: 你好)。
用"\n"连接将列表转换为字符串,并将{"chat_history_text":"历史消息内容"} 传入摘要链summary_memory_prompt。 - 获取摘要文本
取出摘要链返回结果的 content 字段(即摘要文本)。 - langchain链构成
用管道操作符将新增了 chat_summary 的字典传递给summary_memory_prompt,格式化得到最终提示词,再传给 LLM 生成回复。
注意点:这个链每轮对话都会基于当前完整的 chat_history 重新生成一次摘要。虽然摘要本身很短,但每次都要将全部历史文本发送给 LLM 来生成摘要,随着对话轮次增加,计算成本会线性增长。
方式二
可能是不习惯看这么长的代码,总觉得有些难理解,所以将这个链给拆分重新写了下:
python
# 4.构建基础对话链------第二种方式
# 这里的chat_history_list是第一种方式中的x["chat_history"]
def generate_summary(chat_history_list):
history_text = "\n".join([f"{msg.type}:{msg.content}" for msg in chat_history_list])
return summary_chain.invoke({"chat_history_text": history_text}).content
# 最终要输入给summary_memory_prompt的是一个字典并且字典中有两个键:chat_summary和user_input
def prepare_summary_context(inputs):
summary = generate_summary(inputs["chat_history"])
return{
"user_input": inputs["user_input"],
"chat_summary": summary
}
summary_base_chain = (RunnableLambda(prepare_summary_context)
| summary_memory_prompt
| llm)
这里面需要解释一下是RunnableLambda
RunnableLamda
-
什么是RunnableLamda
RunnableLambda 是 LangChain 中最核心、最常用的 "适配器"------可以把任意普通 Python 函数(def /lambda)包装成 LangChain 标准的 Runnable 组件,从而能直接放进 LCEL 链里和模型、提示词、解析器等一起用。(方式二其实就可以看你出来,prepare_summary_context原本是个普通函数,返回的是字典,想要和Runnable组件一起组成LCEL链,就需要RunnableLamda包裹起来)
-
关键规则
(1)函数必须是单参数输入,Runnable 链路只能传一个对象。需要多参数的话可以用字典打包。
(2)支持:同步 .invoke() / 异步 .ainvoke() / 生成器(流式).stream() / 批量 .batch()
(3)RunnableLambda 包裹函数时,不用手动传参,参数是通过调用.invoke() .ainvoke() ...的时候传递的,RunnableLambda 只是把函数变成一个 "管道节点",
数据流过管道时,自动把数据当参数喂给函数。
在方式二中,prepare_summary_context的传参是summary_base_chain.invoke(some_input)时传入的some_input,而后面代码中没有显示调用summary_base_chain,而是被包装在RunnableWithMessageHistory 中,当执行下面的代码时
python
response = summary_memory_chain.invoke({"user_input": user_input}, config=config)
RunnableWithMessageHistory 会做以下工作:
- 根据 session_id 从存储器中取出历史消息列表。
- 构造一个新的输入字典,形如:
python
{
"user_input": "当前用户输入的内容",
"chat_history": [HumanMessage(...), AIMessage(...), ...] # 历史消息列表
}
将这个字典作为 some_input,调用 summary_base_chain.invoke(some_input)。
因此,RunnableLambda(prepare_summary_context) 收到的 some_input 就是这个包含 user_input 和 chat_history 的字典,并自动传给repare_summary_context 函数。
会话历史存储与获取函数
python
summary_memory_store = {}
def get_summary_memory_history(session_id: str) -> BaseChatMessageHistory:
if session_id not in summary_memory_store:
summary_memory_store[session_id] = InMemoryChatMessageHistory()
return summary_memory_store[session_id]
summary_memory_store:字典,键为 session_id,值为 InMemoryChatMessageHistory 对象(可存储消息列表)。
get_summary_memory_history:根据 session_id 返回对应的历史存储器,若不存在则新建一个。
构建带摘要记忆的对话链
python
summary_memory_chain = RunnableWithMessageHistory(
runnable=summary_base_chain,
get_session_history=get_summary_memory_history,
input_messages_key="user_input",
history_messages_key="chat_history"
)
- RunnableWithMessageHistory是一个包装器,负责:
- 从 get_session_history 获取当前会话的历史消息列表。
- 将历史消息以 history_messages_key 指定的键(这里是 "chat_history")合并到调用 runnable
的输入字典中。 - 调用 runnable(即我们的 summary_base_chain)并得到输出。
- 将用户输入和模型输出追加到历史存储器中。
- 参数说明
input_messages_key="user_input":指明输入字典中哪个键对应当前用户消息。
history_messages_key="chat_history":指明历史消息列表在传给Runnable的字典中使用什么键名,这里使用"chat_history"。
- 因此,当我们调用 summary_memory_chain.invoke({"user_input": "..."}, config) 时,RunnableWithMessageHistory 会自动:
(1) 取出 session_id(从 config 中获取)。
(2) 获取该会话的历史消息列表,放入 {"chat_history": [...], "user_input": "..."} 中。
(3) 调用 summary_base_chain。
将用户消息和 LLM 回复追加到历史中。
本文代码主要来源于:datawhale