当前位置: 首页 > news >正文

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
# memory_buffer_example.pyfrom 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
# memory_window_example.pyfrom 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
# memory_summary_example.pyfrom 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: 适用于需要记忆和跟踪特定实体信息(如客户档案、产品属性)的对话场景。
http://www.xdnf.cn/news/1108729.html

相关文章:

  • 创建uniapp项目引入uni-id用户体系使用beforeRegister钩子创建默认昵称
  • 9. JVM垃圾回收
  • 12. JVM的垃圾回收器
  • Agent 设计模式
  • 前后端分离项目的完整部署(Jenkins自动化部署)
  • 【从零开始编写数据库:基于Python语言实现数据库ToyDB的ACID特性】
  • 27.【.NET8 实战--孢子记账--从单体到微服务--转向微服务】--单体转微服务--币种服务(一)
  • Android下一个简单的定时器,每隔一秒输出一个数字
  • Syntax Error: TypeError: Cannot set properties of undefined (setting ‘parent‘)
  • vue3 canvas 选择器 Canvas 增加页面性能
  • Kimi K2万亿参数开源模型原理介绍
  • 【论文阅读】HCCF:Hypergraph Contrastive Collaborative Filtering
  • 缓存三剑客解决方案
  • 【C语言】回调函数、转移表、qsort 使用与基于qsort改造冒泡排序
  • 利用docker部署前后端分离项目
  • 敏捷开发方法全景解析
  • SQL server之版本的初认知
  • C#枚举:从基础到高级的全方位解析
  • 《通信原理》学习笔记——第一章
  • 《Spring 中上下文传递的那些事儿》Part 11:上下文传递最佳实践总结与架构演进方向
  • 基于MCP的CI/CD流水线:自动化部署到云平台的实践
  • Vue Vue-route (5)
  • Adobe Illustrator关于图标创建的问题
  • 【跟我学运维】chkconfig jenkins on的含义
  • 初等行变换会改变矩阵的什么?不变改变矩阵的什么?求什么时需要初等行变换?求什么时不能初等行变换?
  • 回归(多项式回归)
  • 电网通俗解析术语2:一二次设备关联
  • 【PycharmPyqt designer桌面程序设计】
  • Effective Modern C++ 条款9:优先考虑别名声明而非typedef
  • Socket到底是什么(简单来说)