LangChain 内存(Memory)

1. 为什么需要内存?

大型语言模型(LLM)本身是无状态的。这意味着每次你向 LLM 发送一个请求(Prompt),它都会独立处理这个请求,完全不记得之前任何的交互。这在构建一次性问答应用时没问题,但在需要多轮对话(比如聊天机器人、智能客服)的场景中,LLM 需要"记住"之前的对话内容,才能理解上下文并做出连贯的回复。

内存就是 LangChain 解决这个问题的方案。它允许你在 LLM 应用中注入和管理对话历史,让 LLM 具备"长期"记忆的能力。

简单来讲就是存储历史记录,然后在每次使用 LLM 时,不仅传入当前输入,还有历史记录

2. 内存的基本工作原理

在 LangChain 中,内存通常扮演着连接用户输入LLM 提示的桥梁。

  1. 保存历史:每次用户和 LLM 进行交互后,内存组件都会捕获并存储这次对话的输入和输出。
  2. 加载历史:在下一次 LLM 调用之前,内存会根据需要从其存储中加载相关的对话历史。
  3. 注入提示:加载的对话历史会被格式化并注入到 LLM 的提示(Prompt)中,作为上下文的一部分。这样,LLM 就能"看到"之前的对话内容,从而理解当前问题的背景。

3. 最简单的内存:对话缓冲区(ConversationBufferMemory)

ConversationBufferMemory 是 LangChain 中最基础也是最常用的内存类型。它非常简单粗暴:记住所有对话的原始文本。它会将完整的对话历史原封不动地存储起来,并在每次调用时将所有历史添加到提示中。
ConversationBufferMemory 流程 发送给LLM 保存到内存 从内存加载 添加到Prompt LLM Chain/Agent 用户输入 LLM LLM 输出 ConversationBufferMemory

python 复制代码
# memory_buffer_example.py

from langchain_openai import ChatOpenAI
from langchain.chains import ConversationChain
from langchain.memory import ConversationBufferMemory
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
import os

# --- 配置部分 ---
# os.environ["OPENAI_API_KEY"] = "YOUR_OPENAI_API_KEY"

if not os.getenv("OPENAI_API_KEY"):
    print("错误:请设置环境变量 OPENAI_API_KEY 或在代码中取消注释并设置您的密钥。")
    exit()

print("--- ConversationBufferMemory 示例开始 ---")

# 1. 定义 LLM
llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0.7)

# 2. 定义内存
# memory_key 是在 Prompt 中用来引用对话历史的变量名
# return_messages=True 表示内存返回的是消息对象列表,而不是单个字符串
memory = ConversationBufferMemory(memory_key="chat_history", return_messages=True)
print("已创建 ConversationBufferMemory。")

# 3. 定义带有历史占位符的 Prompt
# MessagesPlaceholder 用于告诉 Prompt 在这里插入消息列表 (chat_history)
prompt = ChatPromptTemplate.from_messages([
    ("system", "你是一个友好的AI助手,擅长进行多轮对话。"),
    MessagesPlaceholder(variable_name="chat_history"), # 聊天历史的占位符
    ("human", "{input}") # 当前用户输入
])
print("已创建包含 chat_history 占位符的 Prompt。")

# 4. 创建 ConversationChain
# ConversationChain 是 LangChain 提供的一个预构建的链,专为处理对话而设计
# 它会自动处理内存的注入和更新
conversation = ConversationChain(
    llm=llm,
    memory=memory,
    prompt=prompt,
    verbose=True # 打印详细日志,可以看到内存注入到 Prompt 的过程
)
print("已创建 ConversationChain。")

# 5. 进行多轮对话
print("\n--- 开始对话 ---")

# 第一轮
print("\n用户: 你好,我叫小明。你叫什么名字?")
response1 = conversation.invoke({"input": "你好,我叫小明。你叫什么名字?"})
print(f"AI: {response1['response']}")

# 第二轮
print("\n用户: 很高兴认识你!我之前告诉你我叫什么名字了?")
response2 = conversation.invoke({"input": "很高兴认识你!我之前告诉你我叫什么名字了?"})
print(f"AI: {response2['response']}")

# 第三轮
print("\n用户: 帮我写一句关于编程的诗。")
response3 = conversation.invoke({"input": "帮我写一句关于编程的诗。"})
print(f"AI: {response3['response']}")

print("\n--- 对话结束 ---")
print("\n--- ConversationBufferMemory 示例结束 ---")
  • ConversationBufferMemory 将所有交互都作为 HumanMessageAIMessage 存储起来。
  • MessagesPlaceholder 是一个非常关键的组件,它告诉 LangChain 在构建最终发送给 LLM 的提示时,应该将 chat_history 这个变量的内容(即内存中的消息列表)插入到这里。
  • ConversationChain 是一个便利的链,它自动处理了内存的读写,简化了对话应用的构建。

4. 限制历史长度:对话缓冲区窗口内存(ConversationBufferWindowMemory)

ConversationBufferMemory 的一个缺点是,如果对话很长,内存中的历史会不断增长,导致每次发送给 LLM 的提示越来越长,最终可能超出 LLM 的上下文窗口限制(Context Window Limit),并增加 API 成本。

ConversationBufferWindowMemory 解决了这个问题,它只记住最近 N 轮的对话
ConversationBufferWindowMemory 流程 发送给LLM 保存到内存 (仅保留最新N轮) 从内存加载 (仅加载最新N轮) 添加到Prompt LLM Chain/Agent 用户输入 LLM LLM 输出 ConversationBufferWindowMemory

python 复制代码
# memory_window_example.py

from langchain_openai import ChatOpenAI
from langchain.chains import ConversationChain
from langchain.memory import ConversationBufferWindowMemory
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
import os

# --- 配置部分 ---
# os.environ["OPENAI_API_KEY"] = "YOUR_OPENAI_API_KEY"

if not os.getenv("OPENAI_API_KEY"):
    print("错误:请设置环境变量 OPENAI_API_KEY 或在代码中取消注释并设置您的密钥。")
    exit()

print("--- ConversationBufferWindowMemory 示例开始 ---")

llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0.7)

# 定义窗口大小为 2,表示只记住最近 2 轮(4条消息:2用户+2AI)对话
memory = ConversationBufferWindowMemory(memory_key="chat_history", return_messages=True, k=2)
print("已创建 ConversationBufferWindowMemory,窗口大小 k=2。")

prompt = ChatPromptTemplate.from_messages([
    ("system", "你是一个友好的AI助手,只记得最近的对话。"),
    MessagesPlaceholder(variable_name="chat_history"),
    ("human", "{input}")
])

conversation = ConversationChain(
    llm=llm,
    memory=memory,
    prompt=prompt,
    verbose=True
)
print("已创建 ConversationChain。")

# 5. 进行多轮对话,观察内存如何截断
print("\n--- 开始对话 ---")

# 第一轮
print("\n用户: 我喜欢吃苹果。")
response1 = conversation.invoke({"input": "我喜欢吃苹果。"})
print(f"AI: {response1['response']}")

# 第二轮
print("\n用户: 你呢?你喜欢什么水果?")
response2 = conversation.invoke({"input": "你呢?你喜欢什么水果?"})
print(f"AI: {response2['response']}")

# 第三轮 - 此时第一轮对话(用户说"我喜欢吃苹果")应该被"遗忘"
print("\n用户: 我之前喜欢什么水果来着?")
response3 = conversation.invoke({"input": "我之前喜欢什么水果来着?"})
print(f"AI: {response3['response']}") # AI可能无法准确回答,因为它忘记了第一轮

# 第四轮 - 此时第二轮对话应该被遗忘
print("\n用户: 你呢?你刚才喜欢什么水果?")
response4 = conversation.invoke({"input": "你呢?你刚才喜欢什么水果?"})
print(f"AI: {response4['response']}")

print("\n--- 对话结束 ---")
print("\n--- ConversationBufferWindowMemory 示例结束 ---")
  • k=2 参数控制了窗口大小。这意味着内存将只保留最近的 2 轮完整的对话(即 2 条用户消息和 2 条 AI 消息)。
  • 你会发现,在第三轮对话中,模型可能无法回忆起第一轮用户提到的"苹果",因为它已经超出了窗口范围。

5. 总结历史:对话摘要内存(ConversationSummaryMemory)

ConversationBufferWindowMemory 虽然限制了历史长度,但可能会丢失早期对话的关键信息。ConversationSummaryMemory 旨在解决这个问题:它不直接存储所有历史,而是使用一个 LLM 对对话历史进行摘要,然后将这个摘要作为上下文提供给主 LLM。

这样,无论对话多长,每次传递给 LLM 的都是一个简洁的摘要,既能保持上下文,又不会超出令牌限制。
ConversationSummaryMemory 流程 发送给LLM 添加到历史 LLM生成摘要 摘要添加到Prompt LLM Chain/Agent 用户输入 LLM LLM 输出 完整对话历史 ConversationSummaryMemory

python 复制代码
# memory_summary_example.py

from langchain_openai import ChatOpenAI
from langchain.chains import ConversationChain
from langchain.memory import ConversationSummaryMemory
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
import os

# --- 配置部分 ---
# os.environ["OPENAI_API_KEY"] = "YOUR_OPENAI_API_KEY"

if not os.getenv("OPENAI_API_KEY"):
    print("错误:请设置环境变量 OPENAI_API_KEY 或在代码中取消注释并设置您的密钥。")
    exit()

print("--- ConversationSummaryMemory 示例开始 ---")

# 定义一个用于生成摘要的 LLM
# 摘要LLM可以是一个更小的、成本更低的模型,或者与主LLM相同
summary_llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)
main_llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0.7)

# 1. 定义内存
# memory_key 和 return_messages 类似之前的内存
# llm 参数指定了用于生成摘要的 LLM
memory = ConversationSummaryMemory(llm=summary_llm, memory_key="chat_history", return_messages=True)
print("已创建 ConversationSummaryMemory。")

prompt = ChatPromptTemplate.from_messages([
    ("system", "你是一个友好的AI助手,能记住所有对话的重要摘要。"),
    MessagesPlaceholder(variable_name="chat_history"), # 这里传入的是摘要
    ("human", "{input}")
])

conversation = ConversationChain(
    llm=main_llm,
    memory=memory,
    prompt=prompt,
    verbose=True
)
print("已创建 ConversationChain。")

# 5. 进行多轮对话,观察内存如何进行摘要
print("\n--- 开始对话 ---")

# 第一轮
print("\n用户: 我的项目遇到了一个问题,需要你的帮助。我正在开发一个Python脚本,它需要处理大量文本数据。")
response1 = conversation.invoke({"input": "我的项目遇到了一个问题,需要你的帮助。我正在开发一个Python脚本,它需要处理大量文本数据。"})
print(f"AI: {response1['response']}")

# 第二轮
print("\n用户: 具体来说,我需要对文本进行分词和词性标注。你有什么建议的库吗?")
response2 = conversation.invoke({"input": "具体来说,我需要对文本进行分词和词性标注。你有什么建议的库吗?"})
print(f"AI: {response2['response']}")

# 第三轮
print("\n用户: 好的,我会试试 NLTK 和 SpaCy。你认为哪个更适合大型项目?")
response3 = conversation.invoke({"input": "好的,我会试试 NLTK 和 SpaCy。你认为哪个更适合大型项目?"})
print(f"AI: {response3['response']}")

# 第四轮 - 此时内存会包含前三轮的摘要
print("\n用户: 好的,谢谢你的建议。我的项目主要是关于中文文本处理。")
response4 = conversation.invoke({"input": "好的,谢谢你的建议。我的项目主要是关于中文文本处理。"})
print(f"AI: {response4['response']}")

print("\n--- 对话结束 ---")
# 打印最终的摘要内容
print("\n当前内存中的摘要:")
print(memory.buffer)

print("\n--- ConversationSummaryMemory 示例结束 ---")

说明:

  • ConversationSummaryMemory 需要一个 llm 参数来执行摘要任务。这个 llm 可以是与主 LLM 相同的模型,也可以是专门为摘要优化的模型。
  • 通过 verbose=True 观察输出,你会发现每次调用时,LLM 接收到的 chat_history 变量会是一个不断更新的摘要字符串,而不是原始的完整消息列表。

6. 其他常用内存类型

LangChain 还提供了其他更高级或更具体用途的内存类型:

  • ConversationSummaryBufferMemory : 结合了 ConversationBufferWindowMemoryConversationSummaryMemory 的特点。它会保留最近 N 轮的完整对话,而将 N 轮之前的对话进行摘要。这样可以在短期内提供精确上下文,同时长期保持摘要记忆。
  • VectorStoreRetrieverMemory: 这种内存将对话历史存储在向量数据库中。当需要回忆信息时,它会像 RAG 那样,根据当前查询在向量数据库中检索最相关的历史片段,而不是全部或摘要。这对于需要记忆超长对话或从大量历史中检索特定信息非常有用。
  • EntityMemory: 专注于从对话中识别和记忆特定的实体(如人名、地点、概念),并将其存储在一个结构化(通常是 JSON)的知识图中。当对话中再次提到这些实体时,LLM 可以直接引用其存储的信息。

7. 选择合适的内存策略

  • ConversationBufferMemory: 适用于短对话、简单场景或调试。
  • ConversationBufferWindowMemory: 适用于需要限制上下文长度、但仍需保持一定近期对话完整性的场景。
  • ConversationSummaryMemory: 适用于长对话,需要保持核心上下文但又不能超出 LLM 上下文窗口的场景。
  • ConversationSummaryBufferMemory: 结合了短期精确记忆和长期摘要记忆的优点。
  • VectorStoreRetrieverMemory: 适用于需要从海量、复杂对话历史中智能检索相关片段的场景,或构建具备"知识库"的聊天机器人。
  • EntityMemory: 适用于需要记忆和跟踪特定实体信息(如客户档案、产品属性)的对话场景。
相关推荐
IT_陈寒41 分钟前
Vue这个坑我跳了两次,原来问题出在这
前端·人工智能·后端
新新技术迷1 小时前
Node给AI接口做SSE代理与鉴权
人工智能
redreamSo2 小时前
大模型是不是到顶了?瓶颈到底在哪
人工智能·openai
Oo9202 小时前
Tool Use 背后的技术逻辑
人工智能
姗姗来迟了2 小时前
Vue3封装AI流式对话组件踩坑实录
人工智能
码上天下3 小时前
用Pinia管理AI多会话状态
人工智能
用户054324329703 小时前
Next.js接大模型流式SSE实操踩坑
人工智能
Lihua奏3 小时前
# 机器学习:机器是怎么从数据里学出规则的
机器学习
Assby3 小时前
从 Function Calling 到 MCP:理解 Agent 工具调用的底层通信机制
人工智能·后端
小星AI4 小时前
Claude Code 从入门到精通,一步到位
人工智能