第05节:记忆
默认情况下,使用LangChain与大模型交互是无状态的。每次独立调用,程序不会记得之前的交互。代码如下所示:
prompt = "我是晓舟,我喜欢看电影。"
response = llm.invoke(prompt) #invoke方法发送消息
prompt = "我是谁"
response = llm.invoke(prompt) #invoke方法发送消息
print(response.content)
#因为调用大模型是无状态的,所以大模型并不知道我是谁。
这对于一次性问答或文本生成任务可能没问题,但对于构建需要连续对话的应用程序(如聊天机器人)来说,这是一个巨大的限制。
记忆 (Memory) 组件就是 LangChain 用来解决这个问题的方案。它允许链或智能体在多次交互之间保持状态,记住之前的对话内容或信息。
1. Memory 的基本概念与重要性
- 状态保持: Memory 的核心作用是存储和管理对话历史或其他相关状态信息。
- 上下文注入: 在每次调用链或模型之前,Memory 会加载存储的状态,并将其作为上下文信息注入到提示 (Prompt) 中。
- 状态更新: 在链或模型处理完当前交互后,Memory 会更新其存储的状态,将新的输入和输出添加进去。
为什么需要 Memory?
- 连贯对话: 使聊天机器人能够理解上下文,进行有意义的多轮对话。
- 个性化体验: 记住用户的偏好、历史信息,提供更个性化的服务。
- 复杂任务: 在需要逐步积累信息或状态的任务中保持进度。
2. 常用的 Memory 类型
LangChain 提供了多种 Memory 实现,以适应不同的需求和场景:
ConversationBufferMemory
:- 最简单常用的 Memory 类型。
- 将所有历史对话消息原封不动地存储在内存中,并在每次调用时将它们全部添加到提示中。
- 优点: 完整保留对话历史,理解上下文最直接。
- 缺点: 当对话很长时,存储的对话历史会变得非常大,可能超出模型的上下文窗口限制,并且增加 API 调用成本和延迟。
- 适用场景: 短对话、需要完整上下文的场景。
ConversationBufferWindowMemory
:ConversationBufferMemory
的变种。- 只保留最近的
k
轮对话消息。 - 优点: 控制了上下文的大小,避免超出限制。
- 缺点: 可能丢失较早的重要信息。
- 参数:
k
(要保留的对话轮数)。 - 适用场景: 大多数标准聊天机器人场景。
ConversationTokenBufferMemory
:- 根据消息的 token 数量 来限制存储的历史大小。
- 需要指定一个
LLM
实例来计算 token。 - 优点: 更精确地控制上下文大小,以适应模型的 token 限制。
- 缺点: 每次交互都需要计算 token,略有开销。
- 参数:
llm
,max_token_limit
。
ConversationSummaryMemory
:- 随着对话进行,使用一个 LLM 动态地生成并提炼 迄今为止的对话摘要。
- 将这个摘要作为上下文注入提示。
- 优点: 即使对话很长,也能将关键信息压缩进较小的上下文中。
- 缺点: 每次交互都需要额外的 LLM 调用来更新摘要,成本和延迟增加;摘要过程可能丢失细节。
- 参数:
llm
。
ConversationSummaryBufferMemory
:- 结合了
ConversationBufferMemory
和ConversationSummaryMemory
。 - 存储最近的对话消息(按 token 限制),并为更早的消息维护一个摘要。
- 优点: 平衡了完整性和上下文大小。
- 参数:
llm
,max_token_limit
。
- 结合了
VectorStoreRetrieverMemory
:- 将历史对话存储在向量数据库中。
- 在需要时,根据当前输入检索最相关的历史对话片段作为上下文。
- 优点: 可以处理非常长的对话历史,只检索最相关的信息。
- 缺点: 实现相对复杂,依赖向量存储和检索。
3. 如何在 Chains/Agents 中集成 Memory
将 Memory 集成到链中通常有两种方式:
方式一:使用 LCEL (推荐)
在 LCEL 中,Memory 通常不直接用 |
连接,而是其状态需要在调用链时手动管理(加载和保存)。这通常涉及到:
- 初始化 Memory 对象。
- 构建链 (不包含 Memory): 使用
ChatPromptTemplate
,并确保包含 Memory 使用的输入变量(如history
)。 - 调用链: 在调用
chain.invoke()
或chain.stream()
等方法时:- 使用
memory.load_memory_variables({})
加载历史记录,并将其与其他输入变量一起传递给链。 - 链执行完毕后,使用
memory.save_context(inputs, {"output": result})
保存当前的输入和输出到 Memory。
- 使用
虽然这种方式更灵活,但也更手动。LangChain 正在不断改进 LCEL 与 Memory 的集成方式。
方式二:使用旧版 ConversationChain
(或特定支持 Memory 的链)
一些旧版的链(如 ConversationChain
)直接支持在初始化时传入 memory
对象。链内部会自动处理 Memory 的加载和保存。
import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
## Memory 组件通常在 langchain.memory 或 langchain_community.memory 中
from langchain.memory import ConversationBufferMemory
## ConversationChain 是一个经典的、内置 Memory 支持的链
from langchain.chains import ConversationChain
## 加载环境变量
load_dotenv()
print("环境变量已加载。")
## 初始化 LLM (建议使用 Chat Model 以获得更好的对话效果)
llm = ChatOpenAI(
model="deepseek-chat",
openai_api_key=os.getenv("DEEPSEEK_API_KEY"),
openai_api_base=os.getenv("DEEPSEEK_API_BASE")
)
## --- 1. 使用 ConversationBufferMemory ---
print("\n--- 1. 使用 ConversationBufferMemory (存储所有历史) --- ")
## 初始化 Buffer Memory
memory_buffer = ConversationBufferMemory()
print("ConversationBufferMemory 初始化成功。")
## 创建 ConversationChain,传入 LLM 和 Memory
## verbose=True 会打印链的详细执行过程,包括发送给 LLM 的完整提示
conversation_buffer = ConversationChain(
llm=llm,
memory=memory_buffer,
verbose=True
)
#实现命令行中的多轮对话,可以看到大模型可以记住之前的交互内容。
while True:
inp = input("请输入:")
response = conversation_buffer.run(inp) # 仅返回响应文本
print(f"用户: {inp}")
print(f"AI: {response}")
这种方式更简单直接,尤其适合快速构建标准的聊天机器人。在学习阶段,使用 ConversationChain
来理解 Memory 的作用是非常方便的。