此情可待成追忆,只是当时已惘然。我们人类会有很多或美好或痛苦的回忆,有的回忆会渐渐模糊,有的回忆午夜梦醒,会浮上心头。

然而现在的大语言模型都是没有记忆的,都是无状态的,大语言模型自身不会记住和你对话之间的历史消息。根本用不着“时时勤拂拭”,天然就是“本来无一物”。每一次的请求交互、api调用都是独立的,完全没有关联。那些聊天机器人看起来有记忆,是因为借助代码的帮助,提供历史消息作为和LLM对话的上下文。嗯,就跟我们大脑不太够用了,要拿小本本或者打开Obsidian/Notion/语雀……来查找一样。(你去拜访某些单位,还可以看到前台拿着一本已经翻到包浆的小本子来查电话。)

所以,现在的大语言模型,就跟福尔摩斯一样,可能作为推理引擎更加好用:只要提供足够的上下文信息,那么即使坐在家中,也比愚蠢的苏格兰警探更清楚案情。(可以考虑打造一个叫“夏洛克”的大语言模型? )运筹帷幄之中,决胜千里之外。

本节我们就来看一下LangChain提供的4种Memory(记忆)组件(Vector data memory和Entity memory不展开),每种组件都有其适用场景。

主要的记忆组件

  • ConversationBufferMemory

这个记忆组件允许储存对话的消息,并且可以把消息抽取到一个变量。

  • ConversationBufferWindowMemory

这个记忆会保持K轮对话的列表。只保存最近的K轮对话。旧对话会清除。

  • ConversationTokenBufferMemory

这个记忆组件跟ConversationBufferWindowMemory差不多,同样把旧对话清除,只是是按Token的长度限制。

  • ConversationSummaryMemory

这个记忆组件会调用大语言模型,对旧的会话进行总结。

  • Vector data memory

这个组件把文本(来自会话或者其他地方的)保存到向量数据库,检索最相关的文本块。

  • Entity memories

调用LLM,记住关于特定实体的细节信息。

可以同时使用多个记忆组件,如调用会话记忆+实体记忆来检索个人信息。还可以将会话内容保存到传统数据库(如键值存储Redis或者关系数据库mysql等等),应用要落地这个是必不可少的。

下面来具体看每个组件的例子。

同样是先通过.env文件初始化环境,具体操作参考上一篇。

import os

from dotenv import load_dotenv, find_dotenv
_ = load_dotenv(find_dotenv()) # read local .env file

import warnings
warnings.filterwarnings('ignore')

deployment = "gpt-35-turbo"
model = "gpt-3.5-turbo"

ConversationBufferMemory

# from langchain.chat_models import ChatOpenAI
from langchain.chat_models import AzureChatOpenAI
from langchain.chains import ConversationChain
from langchain.memory import ConversationBufferMemory

llm = AzureChatOpenAI(temperature=0.0, model_name=model, deployment_name=deployment)
memory = ConversationBufferMemory()
conversation = ConversationChain(
    llm=llm, 
    memory = memory,
    verbose=True #设置为True,可以看到对话的详细过程
)
conversation.predict(input="你好,我是西滨。")
conversation.predict(input="1+1等于多少?")
conversation.predict(input="你还记得我的名字?")

这里会创建ConversationChain,Chain是LangChain的核心概念,后面会详细讲述,这里先不管。memory = ConversationBufferMemory() 创建一个ConversationBufferMemory传给ConversationChain,我们打开verbose,看一下具体的输出。

基于LangChain的LLM应用开发3——记忆-LMLPHP

一开始LangChain自动发送一段提示(***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.)***过去开始对话,后面我们可以看到,每次对话的信息都会自动发过去,经过一轮对话之后,再问Ai“你还记得我的名字?”,Ai毫不犹豫的回答:“当然记得!你是西滨。”

上面我们用memory这个变量来保存记忆,如果输出memory.buffer,可以看到对话的所有消息:

Human: 你好,我是西滨。
AI: 你好,西滨!很高兴认识你。我是一个AI助手,可以回答你的问题和提供帮助。有什么我可以帮你的吗?
Human: 1+1等于多少?
AI: 1+1等于2。
Human: 你还记得我的名字?
AI: 当然记得!你是西滨。

可以手工调用save_context方法来把上下文信息传进去:

memory = ConversationBufferMemory()
memory.save_context({"input": "Hi"}, 
                    {"output": "What's up"})
memory.save_context({"input": "Not much, just hanging"}, 
                    {"output": "Cool"})
memory.load_memory_variables({})

调用load_memory_variables({})来查看对应的记忆内容。(load_memory_variables中的花括号{}是一个空词典,可以在这里传递额外的参数进行高级定制)

ConversationBufferMemory可以存储到目前为止的对话消息,看起来很完美,但是随着对话越来越长,所需的记忆存储量也变得非常大,而向LLM发送大量Token的成本也会增加(现在大模型一般按照Token的数量收费,而且还是双向收费,你懂的)。

解决这个问题,LangChain有三个不同的记忆组件来处理。

ConversationBufferWindowMemory

ConversationBufferWindowMemory 只保留一个窗口的记忆,也就是只保留最后若干轮对话消息。注意,这个跟微软的Bing Chat不太一样,微软是每个话题保留30轮,30轮对话一到,自动转向新话题;ConversationBufferWindowMemory的策略就是计算机算法典型的“滑动窗口”,永远都是保留最新的若干轮对话。

from langchain.memory import ConversationBufferWindowMemory
memory = ConversationBufferWindowMemory(k=1)               
memory.save_context({"input": "Hi"},
                    {"output": "What's up"})
memory.save_context({"input": "Not much, just hanging"},
                    {"output": "Cool"})

memory.load_memory_variables({})

上面的k设为了1,那只保留最后1轮对话,对话信息就只剩下:
{'history': 'Human: Not much, just hanging\nAI: Cool'}
前面的”Human:Hi?\nAI: What’s up“ 已经去掉了。

实际应用,我们会更多的采用ConversationBufferWindowMemory(K通常会设得比较大,需要根据具体情景调整),而不是ConversationBufferMemory,可以防止记忆存储量随着对话的进行而无限增长,同时也有比较好的效果。。

ConversationTokenBufferMemory

ConversationTokenBufferMemory通过另一种方式来解决记忆存储量增长的问题:限制保存在记忆的令牌数量。

要先安装tiktoken,底层用于计算Token数目:
!pip install tiktoken

from langchain.memory import ConversationTokenBufferMemory
llm = AzureChatOpenAI(temperature=0.0, model_name=model, deployment_name=deployment)
memory = ConversationTokenBufferMemory(llm=llm, max_token_limit=30)
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({})

注意上面token的限制设为了30,则最终保留下来的消息只有这些,保证总的消息内容长度不超过设置的令牌限制值max_token_limit。(这里涉及到计算Token的算法,不能按字符数计算。每种LLM计算Token的算法都不一样,所以调用ConversationTokenBufferMemory要把llm传进去。):

显然,没有保留最近两轮完整的对话消息,所以这个组件的效果可能没有ConversationBufferWindowMemory好,但是调用Api的性价比高一点。

ConversationSummaryMemory

ConversationSummaryMemory可以算是ConversationTokenBufferMemory的变体,同样是按令牌数限制,但是当它发现令牌数超了,不是把旧的消息丢掉,而是把当前所有的消息进行摘要,直到摘要的文本令牌数不超过设置的限制。

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=400)
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({})

如果max_token_limit设置为400,因为400个令牌足以存储所有的文本,可以看到ConversationSummaryMemory没有做任何的动作,把所有的消息都原样保存:

但是如果把max_token_limit设置为100,ConversationSummaryMemory会调用LLM,把消息保存为下面的摘要:

{'history': 'System: The human and AI exchange greetings. The human mentions that they are not doing much and the AI responds with a casual remark. The human then asks about their schedule for the day. The AI provides a detailed schedule, including a meeting with the product team, working on the LangChain project, and a lunch meeting with a customer interested in AI. The AI emphasizes the importance of bringing a laptop to showcase the latest LLM demo during the lunch meeting.'}

如果是问没那么精确的问题,LLM仍然可以回答:

conversation = ConversationChain(
    llm=llm, 
    memory = memory,
    verbose=False
)
conversation.predict(input="What would be a good demo to show?")

'A good demo to show would be the latest Language Learning Model (LLM) demo. It showcases real-time translations, pronunciation feedback, grammar suggestions, interactive exercises, and quizzes. These features make it an ideal way to highlight the capabilities of our AI technology in the education and language learning industry.’

但是再问具体的信息,由于摘要已经丢失了详细信息,所以LLM开始胡说八道。

conversation.predict(**input="When will the meeting hold today?"**)

'The meeting with the product team is scheduled for 10:00 AM today.’

展望

虽然LangChain提供了不同的记忆组件,但是真正用起来还是有点麻烦,好消息是根据路透社报道,11月6日在OpenAI的开发者大会上,ChatGPT将推出带有记忆能力的大模型,也就是有状态的API接口。[3]

和大模型一次对话的内容量称之为Context,是一个很重要的指标。Context越大,意味着你可以和大模型对话的次数越多,传递的信息越多,那么大模型反馈给你的结果才会更加准确。如果Context不够大,那么你只能抛弃一些信息,自然拿到的结果就会产生偏差。

现在GPT-4默认的Context是8K,如果要支持32K的Context,则价格直接翻倍。Claude大模型支持的Context更大,可以支持100K的Context,所以Claude对于很多PDF文档阅读支持得很好。还有国产的Kimi Chat,据说支持约 20 万汉字的上下文,2.5 倍于 Anthropic 公司的 Claude-100k(实测约 8 万字),8 倍于 OpenAI 公司的 GPT-4-32k(实测约 2.5 万字)。可以说Context容量大小,也是大模型的核心竞争力之一。

但这一切即将成为过去,GPT即将支持记忆能力。也就是GPT会通过缓存的方式记录之前和用户的对话。你不需要那么大的Context容量了,多次对话的性能和单次对话都是一样的。而且在大模型端,通过缓存的方式,可以极大降低应用的开销,让成本直接节省到二十分之一。

期待……

参考

  1. 短课程:https://learn.deeplearning.ai/langchain/lesson/3/memory
  2. 文档:https://python.langchain.com/docs/modules/memory/
  3. Report: OpenAI to Introduce Updates to Make AI Models More Affordable: https://www.pymnts.com/news/artificial-intelligence/2023/openai-introduce-updates-make-ai-models-more-affordable/
10-23 00:29