第5节:记忆

默认情况下,使用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 的集成方式。

from langchain.memory import ConversationBufferMemory
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain.schema.runnable import RunnablePassthrough,RunnableLambda
from langchain.prompts import ChatPromptTemplate,MessagesPlaceholder
from langchain_core.output_parsers import StrOutputParser
import os;

load_dotenv()
llm = ChatOpenAI(
    model=os.getenv("DEEPSEEK_MODEL"),
    api_key=os.getenv("DEEPSEEK_API_KEY"),
    base_url=os.getenv("DEEPSEEK_API_BASE")
)

memory = ConversationBufferMemory(return_messages=True)

prompt = ChatPromptTemplate.from_messages([
    ("system", "你是一个乐于助人的AI助手。"),
	MessagesPlaceholder(variable_name="history"),
    #user和human是一样的,在提示词中都是HumanMessage
    ("human", "{input}")
])

output_parser = StrOutputParser();

def debug(x):
    print("~~~~~prompt~~~~~")
    print(x)
    print("~~~~~prompt~~~~~")
    return x;

chain = (
    RunnablePassthrough.assign(
        history = lambda x: memory.load_memory_variables({"input": x})["history"]
    )
    | prompt
    | debug #可以看到累计的提示词
    | llm
    | output_parser
)
while True:
    inp = input("请输入:")
    response = chain.invoke({
        "input":inp
    })
    #将对话内容保存到记忆中
    memory.save_context({"input": inp}, {"output": response})
    print(response)