在默认情况下,无论是 LLM 还是代理都是无状态的,每次模型的调用都是独立于其他交互的。也就是说,我们每次通过 API 开始和大语言模型展开一次新的对话,它都不知道你其实昨天或者前天曾经和它聊过天了。
你肯定会说,不可能啊,每次和 ChatGPT 聊天的时候,ChatGPT 明明白白地记得我之前交待过的事情。
的确如此,ChatGPT 之所以能够记得你之前说过的话,正是因为它使用了记忆(Memory)机制,记录了之前的对话上下文,并且把这个上下文作为提示的一部分,在最新的调用中传递给了模型。在聊天机器人的构建中,记忆机制非常重要。
使用 ConversationChain
不过,在开始介绍 LangChain 中记忆机制的具体实现之前,先重新看一下我们上一节课曾经见过的 ConversationChain。
这个 Chain 最主要的特点是,它提供了包含 AI 前缀和人类前缀的对话摘要格式,这个对话格式和记忆机制结合得非常紧密。让我们看一个简单的示例,并打印出 ConversationChain 中的内置提示模板,你就会明白这个对话格式的意义了。
#初始化语言模型
from langchain_openai import ChatOpenAI
llm=ChatOpenAI(
openai_api_key='替换为你的API密钥',
base_url='https://api.chatanywhere.tech/v1',
model='gpt-3.5-turbo',
temperature=0.7,
)
#初始化对话链
from langchain.chains import ConversationChain
conv_chain = ConversationChain(
llm=llm,
)
#打印对话的模型
print(conv_chain.prompt.template)
使用 ConversationBufferMemory
在 LangChain 中,通过 ConversationBufferMemory(缓冲记忆)可以实现最简单的记忆机制。
#使用 ConversationBufferMemory在 LangChain 中,
#通过 ConversationBufferMemory(缓冲记忆)可以实现最简单的记忆机制。
from langchain.chains.conversation.memory import ConversationBufferMemory
conversation = ConversationChain(
llm=llm,
memory=ConversationBufferMemory()
)
#第一天的对话
#回合1
conversation("我姐姐明天要过生日,我需要一束生日花束。")
print("第一次对话后的记忆:", conversation.memory.buffer,"\n")
# 回合2
conversation("她喜欢粉色玫瑰,颜色是粉色的。")
print("第二次对话后的记忆:", conversation.memory.buffer,"\n")
# 回合3 (第二天的对话)
conversation("我又来了,还记得我昨天为什么要来买花吗?")
print("\n第三次对话后时提示:\n",conversation.prompt.template)
print("\n第三次对话后的记忆:\n", conversation.memory.buffer,"\n")
实际上,这些聊天历史信息,都被传入了 ConversationChain 的提示模板中的 {history} 参数,构建出了包含聊天记录的新的提示输入。
有了记忆机制,LLM 能够了解之前的对话内容,这样简单直接地存储所有内容为 LLM 提供了最大量的信息,但是新输入中也包含了更多的 Token(所有的聊天历史记录),这意味着响应时间变慢和更高的成本。而且,当达到 LLM 的令牌数(上下文窗口)限制时,太长的对话无法被记住(对于 text-davinci-003 和 gpt-3.5-turbo,每次的最大输入限制是 4096 个 Token)。
使用 ConversationBufferWindowMemory
ConversationBufferWindowMemory 是缓冲窗口记忆,它的思路就是只保存最新最近的几次人类和 AI 的互动。因此,它在之前的“缓冲记忆”基础上增加了一个窗口值 k。这意味着我们只保留一定数量的过去互动,然后“忘记”之前的互动。
#使用 ConversationBufferWindowMemory。ConversationBufferWindowMemory 是缓冲窗口记忆,
#它的思路就是只保存最新最近的几次人类和 AI 的互动。
from langchain.chains.conversation.memory import ConversationBufferWindowMemory
conversation = ConversationChain(
llm=llm,
memory=ConversationBufferWindowMemory(k=1)
)
# 第一天的对话
# 回合1
result = conversation("我姐姐明天要过生日,我需要一束生日花束。")
print(result)
# 回合2
result = conversation("\n她喜欢粉色玫瑰,颜色是粉色的。")
# 第二天的对话
# 回合3
result = conversation("\n我又来了,还记得我昨天为什么要来买花吗?")
print(result)
在给定的例子中,设置 k=1,这意味着窗口只会记住与 AI 之间的最新的互动,即只保留上一次的人类回应和 AI 的回应。
使用 ConversationSummaryMemory
上面说了,如果模型在第二轮回答的时候,能够说出“我可以帮你为你姐姐找到…”,那么在第三轮回答时,即使窗口大小 k=1,还是能够回答出正确答案。
这是为什么?因为模型在回答新问题的时候,对之前的问题进行了总结性的重述。ConversationSummaryMemory(对话总结记忆)的思路就是将对话历史进行汇总,然后再传递给 {history} 参数。这种方法旨在通过对之前的对话进行汇总来避免过度使用 Token。
ConversationSummaryMemory 有这么几个核心特点。
- 汇总对话:此方法不是保存整个对话历史,而是每次新的互动发生时对其进行汇总,然后将其添加到之前所有互动的“运行汇总”中。
- 使用 LLM 进行汇总:该汇总功能由另一个 LLM 驱动,这意味着对话的汇总实际上是由 AI 自己进行的。
- 适合长对话:对于长对话,此方法的优势尤为明显。虽然最初使用的 Token
数量较多,但随着对话的进展,汇总方法的增长速度会减慢。与此同时,常规的缓冲内存模型会继续线性增长。
#使用 ConversationSummaryMemory
#ConversationSummaryMemory(对话总结记忆)的思路就是将对话历史进行汇总,
#然后再传递给 {history} 参数。这种方法旨在通过对之前的对话进行汇总来避免过度使用 Token。
from langchain.chains.conversation.memory import ConversationSummaryMemory
# 初始化对话链
conversation = ConversationChain(
llm=llm,
memory=ConversationSummaryMemory(llm=llm)
)
# 第一天的对话
# 回合1
result = conversation("我姐姐明天要过生日,我需要一束生日花束。")
print(result)
# 回合2
result = conversation("\n她喜欢粉色玫瑰,颜色是粉色的。")
# 第二天的对话
# 回合3
result = conversation("\n我又来了,还记得我昨天为什么要来买花吗?")
print(result)
ConversationSummaryMemory 的优点是对于长对话,可以减少使用的 Token 数量,因此可以记录更多轮的对话信息,使用起来也直观易懂。不过,它的缺点是,对于较短的对话,可能会导致更高的 Token 使用。
另外,对话历史的记忆完全依赖于中间汇总 LLM 的能力,还需要为汇总 LLM 使用 Token,这增加了成本,且并不限制对话长度。通过对话历史的汇总来优化和管理 Token 的使用,ConversationSummaryMemory 为那些预期会有多轮的、长时间对话的场景提供了一种很好的方法。然而,这种方法仍然受到 Token 数量的限制。在一段时间后,我们仍然会超过大模型的上下文窗口限制。
使用 ConversationSummaryBufferMemory
我要为你介绍的最后一种记忆机制是 ConversationSummaryBufferMemory,即对话总结缓冲记忆,它是一种混合记忆模型,结合了上述各种记忆机制,包括 ConversationSummaryMemory 和 ConversationBufferWindowMemory 的特点。
这种模型旨在在对话中总结早期的互动,同时尽量保留最近互动中的原始内容。它是通过 max_token_limit 这个参数做到这一点的。当最新的对话文字长度在 300 字之内的时候,LangChain 会记忆原始对话内容;当对话文字超出了这个参数的长度,那么模型就会把所有超过预设长度的内容进行总结,以节省 Token 数量。
#使用 ConversationSummaryBufferMemory
#ConversationSummaryBufferMemory,即对话总结缓冲记忆,它是一种混合记忆模型,结合了上述各种记忆机制,
#包括 ConversationSummaryMemory 和 ConversationBufferWindowMemory 的特点。
#这种模型旨在在对话中总结早期的互动,同时尽量保留最近互动中的原始内容。
from langchain.chains.conversation.memory import ConversationSummaryBufferMemory
# 初始化对话链
conversation = ConversationChain(
llm=llm,
memory=ConversationSummaryBufferMemory
(
llm=llm,
max_token_limit=300
)
)
# 第一天的对话
# 回合1
result = conversation("我姐姐明天要过生日,我需要一束生日花束。")
print(result)
# 回合2
result = conversation("\n她喜欢粉色玫瑰,颜色是粉色的。")
# 第二天的对话
# 回合3
result = conversation("\n我又来了,还记得我昨天为什么要来买花吗?")
print(result)
不难看出,在第二回合,记忆机制完整地记录了第一回合的对话,但是在第三回合,它察觉出前两轮的对话已经超出了 300 个字节,就把早期的对话加以总结,以节省 Token 资源。ConversationSummaryBufferMemory 的优势是通过总结可以回忆起较早的互动,而且有缓冲区确保我们不会错过最近的互动信息。当然,对于较短的对话,ConversationSummaryBufferMemory 也会增加 Token 数量。
总体来说,ConversationSummaryBufferMemory 为我们提供了大量的灵活性。它是我们迄今为止的唯一记忆类型,可以回忆起较早的互动并完整地存储最近的互动。在节省 Token 数量方面,ConversationSummaryBufferMemory 与其他方法相比,也具有竞争力。