说在前面
在构建像聊天机器人这样的LLM应用时,我们会希望它能够记住我们之前与它的聊天内容,即让他拥有记忆上下文的能力,也就是如何记住前面的对话内容,并能将其输入到语言模型中,这样会让我们的对话更加流畅。本节将讨论如何使用 LangChain 构建 LLM 应用的 Memory 单元,实现记忆存储功能。(视频时长17:04)
注: 所有的示例代码文件课程网站上都有(完全免费),并且是配置好的 Juptyernotebook 环境和配置好的 OPENAI_API_KEY
,不需要自行去花钱租用,建议代码直接上课程网站上运行。 课程网站
另外,LLM 的结果并不总是相同的。在执行代码时,可能会得到与视频中略有不同的答案。
Main Content
LangChain 针对复杂的记忆存储管理提供了多种选项,我们这里主要讲解以下四种 Memory。
- ConversationBufferMemory
- ConversationBufferWindowMemory
- ConversationTokenBufferMemory
- ConversationSummaryMemory
下面将通过实际的代码进行讲解如何使用 LangChain实现Memory的功能。
注: 下面的代码演示中涉及到 Chain 的使用,不必过分关注其语法,下一节会做 Chain 的讲解。
前置工作
- 这部分代码是完成 LLM 的初始调用设置。
import os# 从dotenv库导入load_dotenv和find_dotenv函数
from dotenv import load_dotenv, find_dotenv# 自动查找并加载位于当前目录或父目录中的.env文件,将其中定义的环境变量添加到系统的环境变量中
_ = load_dotenv(find_dotenv())# 导入warnings模块用于控制警告信息的显示
import warnings# 忽略所有的警告信息,以避免输出中出现不必要的警告,保持输出简洁
warnings.filterwarnings('ignore')
# 账户对于LLM模型弃用的处理
import datetime# 获取当前日期
current_date = datetime.datetime.now().date()# 定义模型应该更新为"gpt-3.5-turbo"的目标日期
target_date = datetime.date(2024, 6, 12)# 根据当前日期设置模型变量
# 如果当前日期晚于目标日期,则使用较新的模型"gpt-3.5-turbo"
# 否则,继续使用旧版本的模型"gpt-3.5-turbo-0301"
if current_date > target_date:llm_model = "gpt-3.5-turbo"
else:llm_model = "gpt-3.5-turbo-0301"# 注意:这里的逻辑是基于假设模型"gpt-3.5-turbo"是在2024年6月12日之后发布的更新版本。
- 导入需要的库和模块。
# 导入所需的库和模块# 从langchain.chat_models导入ChatOpenAI类,用于与OpenAI的聊天模型进行交互
from langchain.chat_models import ChatOpenAI# 从langchain.chains导入ConversationChain类,用于构建对话链,使得可以创建连续的、有上下文的对话
from langchain.chains import ConversationChain# 从langchain.memory导入ConversationBufferMemory类,用于保存对话历史记录,使对话具有记忆功能,能够记住之前的对话内容
from langchain.memory import ConversationBufferMemory
ConversationBufferMemory
ConversationBufferMemory 可以存储整个对话过程中的输入和输出信息,即用户的问题和模型的回答。
- 创建 ConversationBufferMemory 实例,用于保存对话的历史记录。这使得对话具有记忆功能,可以记住之前的交互内容
# 初始化ChatOpenAI实例,设置温度参数为0.0(意味着输出将更加确定,减少随机性),并使用之前定义的llm_model变量指定模型
llm = ChatOpenAI(temperature=0.0, model=llm_model)# 创建ConversationBufferMemory实例,用于保存对话的历史记录。这使得对话具有记忆功能,可以记住之前的交互内容
memory = ConversationBufferMemory()# 构建ConversationChain实例,传入llm和memory参数以启用对话链和记忆功能,并设置verbose=True以详细打印出对话过程中的信息
conversation = ConversationChain(llm=llm, memory=memory,verbose=True # 当设置为True时,会打印出更多的调试信息,便于开发和调试阶段查看对话流程
)
- 与 LLM 进行对话,展示 记忆存储效果。
首先告诉 LLM 我的名字为 Austin_C
。
conversation.predict(input="Hi, my name is Austin_C")
从这里可以看到我们输入给LLM 的prompt如下框内所示,说明我们所谓的memory的实现,就是将我们之前的对话内容添加到 prompt 中,即在下一轮对话时,将之前的对话作为 prompt 的一部分告诉 LLM,从而实现我们看到的上下文记忆功能,LLM 会看起来记得我们之前说过的话。
Prompt after formatting:
The following is a friendly conversation between a human and an AI. The AI is talkative and provides lots of specific details from its context. If the AI does not know the answer to a question, it truthfully says it does not know.Current conversation:
Human: Hi, my name is Andrew
AI: Hello Andrew! It's nice to meet you. How can I assist you today?
Human: Hi, my name is Austin_C
AI:
接着我们,再问 LLM 一个其他的问题。
conversation.predict(input="What is 1+1?")
然后,询问 LLM 我的名字(之前告诉过它),可以看到它正确的回答了出来。(如下图所示)
conversation.predict(input="What is my name?")
打印出存储的 Buffer。可以看到我们之前的所有对话内容都被存储起来。
print(memory.buffer)
从内存中加载当前存储的对话历史记录,返回一个包含对话上下文信息的字典。
# 从内存中加载当前存储的对话历史记录,返回一个包含对话上下文信息的字典
memory.load_memory_variables({})
- 我们可以提前录入 memory,来实现 memory 功能。
创建一个 ConversationBufferMemory 实例。
memory = ConversationBufferMemory()
我们通过 .save_context
方法来实现存入 memory。
memory.save_context({"input": "Hi"}, {"output": "What's up"})
打印存储的 memory。
print(memory.buffer)
从内存中加载当前存储的对话历史记录,返回一个包含对话上下文信息的字典。
memory.load_memory_variables({})
我们继续存入对话,并加载出来。可以看到之前的记录也在。
memory.save_context({"input": "Not much, just hanging"}, {"output": "Cool"})
memory.load_memory_variables({})
总结: 通过这部分的演示我们可以知道 LLM 的memory功能是如何实现的,并且知道如何通过 LangChain 的ConversationBufferMemory 类去实现 Memory 功能。同时,我们需要知道,随着对话长度的变化,我们需要的记忆存储量会变得越来越大,导致我们发送给 LLM 的 Token 量会变得越来越大,这会使我们对话的成本越来越大。所以 LangChain 提供了其他的几种 构建 memory 的方法,将在之后进行演示。
ConversationBufferWindowMemory
ConversationBufferWindowMemory 类如同它的名字一样只会保留一个窗口的记忆,即仅保留最后的若干轮对话的记忆。这样的操作可以减少我们存储的 memory 的大小,同时通过保留最新的对话记忆,会降低对我们的对话的流畅度的影响。
- 导入 ConversationBufferWindowMemory 类。
from langchain.memory import ConversationBufferWindowMemory
- 创建 ConversationBufferWindowMemory 实例,
k=1
表示只保存最近一轮的对话历史,通过不同的 k 值可以设定保存最近 k 轮得对话历史。
# 创建一个ConversationBufferWindowMemory实例,设置只保存最近一轮的对话历史
memory = ConversationBufferWindowMemory(k=1)
- 手动存入两轮对话的内容,然后打印,memory 的内容,检查发现的确只保存了最新一轮的对话记录。
memory.save_context({"input": "Hi"},{"output": "What's up"})
memory.save_context({"input": "Not much, just hanging"},{"output": "Cool"})
memory.load_memory_variables({})
- 下面进行一个对话演示,说明其效果。结果如下图所示。
llm = ChatOpenAI(temperature=0.0, model=llm_model)
memory = ConversationBufferWindowMemory(k=1)
conversation = ConversationChain(llm=llm, memory = memory,verbose=False
)
conversation.predict(input="Hi, my name is Austin_C")
conversation.predict(input="What is 1+1?")
conversation.predict(input="What is my name?")
可以看到只能保存最新一轮对话的内容,模型不能记得两轮之前的对话内容。
ConversationTokenBufferMemory
因为我们与 LLM 对话的成本主要体现在 Token 的数量上,所以出现了 ConversationTokenBufferMemory 这种 memory 方式,它只会保存最近的对话信息,并且保证总的消息长度不超过设置的最大 Token 限制。
from langchain.memory import ConversationTokenBufferMemory
from langchain.llms import OpenAI
llm = ChatOpenAI(temperature=0.0, model=llm_model)
- 设定 Token 的最大限制为 50,然后手动录入对话,然后打印memory中的内容,发现最开始的对话
AI is what?
被删除了,说明其超出我们的max_token_limit
的限制。我们可以通过设定不同的max_token_limit
来实现更符合我们应用场景的记忆功能。
memory = ConversationTokenBufferMemory(llm=llm, max_token_limit=50)
memory.save_context({"input": "AI is what?!"},{"output": "Amazing!"})
memory.save_context({"input": "Backpropagation is what?"},{"output": "Beautiful!"})
memory.save_context({"input": "Chatbots are what?"}, {"output": "Charming!"})
memory.load_memory_variables({})
ConversationSummaryMemory
ConversationSummaryMemory 的思路是,预期将 memory 的存储量限制在最近的若干对话的数量上,或是限制在 Token 的数量上,不如让 LLM 为所有的历史消息生成摘要,将该摘要作为我们的显示记忆存储起来,并且要求该摘要符合我们的 Token 上限。
- 创建 ConversationSummaryMemory 实例,并往其中存入长文本记忆,然后我们打印 memory,发现存储的不是我原始的长文本内容,而是对其总结后的摘要。如下图所示。
from langchain.memory import ConversationSummaryBufferMemory
# create a long string
schedule = "There is a meeting at 8am with your product team. \
You will need your powerpoint presentation prepared. \
9am-12pm have time to work on your LangChain \
project which will go quickly because Langchain is such a powerful tool. \
At Noon, lunch at the italian resturant with a customer who is driving \
from over an hour away to meet you to understand the latest in AI. \
Be sure to bring your laptop to show the latest LLM demo."memory = ConversationSummaryBufferMemory(llm=llm, max_token_limit=100)
memory.save_context({"input": "Hello"}, {"output": "What's up"})
memory.save_context({"input": "Not much, just hanging"},{"output": "Cool"})
memory.save_context({"input": "What is on the schedule today?"}, {"output": f"{schedule}"})
memory.load_memory_variables({})
- 通过对话场景进行演示。(这里使用的是我上面的录入的长文本的 memory)可以看到 LLM 使用摘要内容进行回答也能成功的实现记忆上下文的功能。
conversation = ConversationChain(llm=llm, memory = memory,verbose=True
)
conversation.predict(input="What would be a good demo to show?")
memory.load_memory_variables({})
注: 我们会注意到上面会出现 System
的标识,这并不是 OpenAI Chat 的系统消息,而是提示词中用来描述理事会话摘要的部分。
总结
本节通过创建 LLM 的聊天应用的案例,介绍了 LangChain 中的四种 memory 形式:
- ConversationBufferMemory: 该存储器允许存储消息和然后提取变量中的消息。
- ConversationBufferWindowMemory: 该存储器保存对话在一段时间内的互动列表。
- ConversationTokenBufferMemory: 该内存会在内存中保存最近交互的缓冲区,并使用标记长度而不是交互次数来决定何时刷新交互。
- ConversationSummaryMemory: 该记忆功能可创建一段对话的摘要。
通过这四种 memory 形式,我们可以使用 Langchain 实现有记忆存储的 LLM 聊天应用,使得我们的聊天更加顺畅,满足我们的需求。但同时,记忆存储功能不只能应用在聊天应用上,还可以运用在使用 LLM 进行信息检索等方面。另外,LangChain还提供了其他的几种 memory 形式:
- Vector data memory: 将文本(来自对话或其他地方)存储在矢量数据库中,并检索最相关的文本块。(该方法效果比较好)
- Entity memories: 利用 LLM,它可以记住特定实体的详细信息。
我们还可以同时使用多种 memory 形式,例如,使用 对话记忆 + 实体记忆 来调用个人记忆。我们还可以把对话存储在传统的数据库中(键值对或者 SQL中)。