第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:

    • 结合了 ConversationBufferMemoryConversationSummaryMemory
    • 存储最近的对话消息(按 token 限制),并为更早的消息维护一个摘要。
    • 优点: 平衡了完整性和上下文大小。
    • 参数: llm, max_token_limit
  • VectorStoreRetrieverMemory:

    • 将历史对话存储在向量数据库中。
    • 在需要时,根据当前输入检索最相关的历史对话片段作为上下文。
    • 优点: 可以处理非常长的对话历史,只检索最相关的信息。
    • 缺点: 实现相对复杂,依赖向量存储和检索。

3. 如何在 Chains/Agents 中集成 Memory

将 Memory 集成到链中通常有两种方式:

方式一:使用 LCEL (推荐)

在 LCEL 中,Memory 通常不直接用 | 连接,而是其状态需要在调用链时手动管理(加载和保存)。这通常涉及到:

  1. 初始化 Memory 对象。
  2. 构建链 (不包含 Memory): 使用 ChatPromptTemplate,并确保包含 Memory 使用的输入变量(如 history)。
  3. 调用链: 在调用 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 的作用是非常方便的。