在我们通过 web 端的聊天界面与 AI 对话时,AI 会记住你说过的话。这样,你可以在对话中引用之前的话语,或者在之后的对话中提到之前的话语。
但是如果我们像下面这样调用 API 的时候,就会发现 AI 不会记住我们之前说过的话:
from langchain_openai import ChatOpenAI chat = ChatOpenAI( model="yi-large", temperature=0.3, max_tokens=200, api_key='your key', base_url="https://api.lingyiwanwu.com/v1", ) response = chat.invoke('今天广州天气晴朗,26~35摄氏度。') print(response.content) response = chat.invoke('今天广州适合穿什么?') print(response.content)
输出:
这句话的意思是今天广州的天气非常好,晴朗无云,气温在26摄氏度到35摄氏度之间。这是一个适合户外活动的好天气,但也要注意防晒和补水,因为气温较高。 很抱歉,我无法提供实时天气信息或建议。要了解今天的广州适合穿什么,您可以查看当地的天气预报,了解当前的气温、湿度和天气状况,然后根据这些信息选择合适的衣物。 通常,广州属于亚热带季风气候,夏季炎热潮湿,冬季温和,春秋季节宜人。根据季节和天气预报,您可以选择穿短袖、长袖、薄外套或厚外套等。 别忘了查看是否需要携带雨具,因为广州的降雨量也比较丰富。
虽然我们告诉了 LLM 今天广州的天气,但是在第二次调用的时候,AI 并没有记住我们之前说过的话,所以不能依据当前的天气状况给我提供穿衣建议。
为什么 AI 不会记住我说过的话
这是因为大模型本身并不具备记忆功能。在我们每次使用大模型的 API 的时候,它都是基于训练模型时候的数据以及我们传递的信息来进行推理的。
如果让大模型记住我们说过的话,那么它需要存储的信息量会非常庞大,这样的成本是非常高昂的。
同时,如果每一次调用的时候,都在一个庞大的上下文中进行推理,那么推理的时间也会非常长,消耗的资源会非常多。
所以,大模型通常是不会记住我们说过的话的。
解决办法:我们自己记住
既然大模型记不住我们说过的话,那唯一的办法就是我们自己记住,然后下次调用的时候,将之前的话语传递给 AI。
from langchain_openai import ChatOpenAI from langchain_core.messages import HumanMessage, AIMessage chat = ChatOpenAI( model="yi-large", temperature=0.3, max_tokens=200, api_key='your key', base_url="https://api.lingyiwanwu.com/v1", ) messages = [ HumanMessage('今天广州天气晴朗,26~35摄氏度。'), ] response = chat.invoke(messages) messages.append(AIMessage(response.content)) messages.append(HumanMessage('今天广州适合穿什么?')) print(messages) response = chat.invoke(messages) print(response.content)
输出:
[ HumanMessage(content='今天广州天气晴朗,26~35摄氏度。'), AIMessage(content='这句话的意思是,今天广州的天气非常好,晴朗无云,气温在26摄氏度到35摄氏度之间。这是一个非常适合户外活动的天气,既不太热也不太冷。'), HumanMessage(content='今天广州适合穿什么?') ] 根据您提供的信息,今天广州的天气晴朗,气温在26到35摄氏度之间。这个温度范围适合穿着轻薄、透气的衣物。以下是一些建议: 1. 上衣:可以选择短袖T恤、薄衬衫或棉质衣物,以保持凉爽。 2. 下装:可以穿短裤、七分裤或轻薄的牛仔裤。 3. 鞋子:舒适的凉鞋、帆布鞋或运动鞋都是不错的选择。 4. 配件:如果需要外出,可以戴上一顶遮阳帽和太阳镜,以保护皮肤和眼睛不受紫外线伤害。 5. 防晒:由于天气晴朗,紫外线可能较强,建议涂抹防晒霜以保护皮肤。 请根据您的个人舒适度和活动需求来调整着装。如果您的活动包括室内外结合,可以准备一件轻薄的外套或披肩,以
输出的第一部分是我们问第二个问题的上下文,第二部分是 AI 的回答。
在这个例子中,我们将之前的对话保存在了 messages
中,然后在下一次调用的时候,将之前的对话传递给 AI。
当然,这里只是举个例子。真实使用中,我们可能会将一大段信息交给 LLM,然后让它来帮我们进行分析推理,然后可以问它问题从而得到答案。
对话内容记忆的抽象
上一个例子中,我们是每一次请求和响应的内容都保存在了 messages
中,然后传递给 AI。
这样操作可能会比较麻烦,因为消息历史会逐渐增长,直到达到 LLM 的最大上下文长度。
这个时候,我们就需要删除一部分历史消息,从而保证 LLM 可以正常处理我们的请求。
除了最大上下文限制的原因,太长的上下文也会带来大量的 token
消耗,这样会增加我们的成本。
因此,我们非常需要定期对历史消息进行处理,删除一部分意义不大的历史消息,或者删除那些最久远的消息,只保留最近的消息。
这跟人类的记忆一样,我们对近期发生的事情记忆深刻,而对很久远的事情记忆模糊。
ConversationBufferWindowMemory
为了解决这个问题,langchain
提供了一些处理历史消息的工具。
比如适合我上面说的这个场景的 RunnableWithMessageHistory
,它可以记住指定次数的消息,然后在超过指定次数的时候,删除最早的消息。
from langchain_core.chat_history import InMemoryChatMessageHistory from langchain_core.messages import AIMessage from langchain_core.runnables import RunnableWithMessageHistory from langchain_openai import ChatOpenAI from langchain.memory import ConversationBufferWindowMemory llm = ChatOpenAI( model="yi-large", temperature=0.3, max_tokens=200, api_key='your key', base_url="https://api.lingyiwanwu.com/v1", ) store = {} # memory is maintained outside the chain def get_session_history(session_id: str) -> InMemoryChatMessageHistory: if session_id not in store: store[session_id] = InMemoryChatMessageHistory() return store[session_id] memory = ConversationBufferWindowMemory( chat_memory=store[session_id], k=10, return_messages=True, ) assert len(memory.memory_variables) == 1 key = memory.memory_variables[0] messages = memory.load_memory_variables({})[key] store[session_id] = InMemoryChatMessageHistory(messages=messages) return store[session_id] chain = RunnableWithMessageHistory(llm, get_session_history) conf = {"configurable": {"session_id": "1"}} response = chain.invoke('今天广州天气晴朗,26~35摄氏度。', config=conf) # type: AIMessage print(f"response 1: {response.content}") response = chain.invoke('今天广州适合穿什么?', config=conf) # type: AIMessage print(f"response 2: {response.content}")
输出:
response 1: 这句话的意思是今天广州的天气非常好,晴朗无云,气温在26摄氏度到35摄氏度之间。这个温度范围对于夏天来说是比较舒适的,适合户外活动。 response 2: 根据您提供的信息,今天广州的天气晴朗,气温在26到35摄氏度之间。这个温度范围适合穿着轻薄、透气的衣物。以下是一些建议: 1. 上衣:可以选择短袖T恤、薄衬衫或棉质衣物,避免穿得过多导致出汗后衣服湿透。 2. 下装:可以穿短裤、七分裤或轻薄的牛仔裤。如果是在室内或者空调环境中,可以考虑带一件长裤以防着凉。 3. 鞋子:舒适的凉鞋、帆布鞋或运动鞋都是不错的选择。 4. 配件:可以戴一顶遮阳帽和太阳镜来保护皮肤和眼睛免受紫外线伤害。如果需要长时间在户外,可以考虑涂抹防晒霜。 5. 携带物品:由于气温较高,建议随身携带水瓶以保持水分,同时可以携带
相比之下,现在我们并不需要每次都手动保存历史消息,而是交给 ConversationBufferWindowMemory
来处理。
这样,我们就可以更加专注于对话的内容,而不用担心历史消息的处理。
在上面这个例子中,我们指定了 k=10
,也就是说,只保存最近的 20 条消息,
超过 20 条的消息之后会删除最早的消息(这是因为在底层实现中,会使用 k * 2
,而不是 k
)。
我们可以指定 k=1
来验证一下():
from langchain_core.chat_history import InMemoryChatMessageHistory from langchain_core.messages import AIMessage from langchain_core.runnables import RunnableWithMessageHistory from langchain_openai import ChatOpenAI from langchain.memory import ConversationBufferWindowMemory llm = ChatOpenAI( model="yi-large", temperature=0.3, max_tokens=200, api_key='your key', base_url="https://api.lingyiwanwu.com/v1", ) store = {} # memory is maintained outside the chain def get_session_history(session_id: str) -> InMemoryChatMessageHistory: if session_id not in store: store[session_id] = InMemoryChatMessageHistory() return store[session_id] memory = ConversationBufferWindowMemory( chat_memory=store[session_id], k=1, return_messages=True, ) assert len(memory.memory_variables) == 1 key = memory.memory_variables[0] # 返回最新的 k * 2 条消息 messages = memory.load_memory_variables({})[key] store[session_id] = InMemoryChatMessageHistory(messages=messages) return store[session_id] chain = RunnableWithMessageHistory(llm, get_session_history) conf = {"configurable": {"session_id": "1"}} response = chain.invoke('今天广州天气晴朗,26~35摄氏度。', config=conf) # type: AIMessage print(f"response 1: {response.content}") response = chain.invoke('这是一条无用的消息,请你忽略。', config=conf) # type: AIMessage print(f"response 2: {response.content}") response = chain.invoke('今天广州适合穿什么?', config=conf) # type: AIMessage print(f"response 3: {response.content}")
输出:
response 1: 这句话的意思是,今天广州的天气非常好,晴朗无云,气温在26摄氏度到35摄氏度之间。这是一个非常适合户外活动的天气,既不太热也不太冷。 response 2: 好的,我会忽略这条消息。如果您有其他问题或需要帮助,请随时告诉我! response 3: 很抱歉,我无法提供实时数据或当前的天气情况。........<一大堆>
因为我们使用了 k=1
,所以当我们交谈了三次之后,第一次发送的内容就会被删除了。
所以当我们问第三个问题的时候,AI 并没有记住我们之前说过的话。
本文例子用到的一些类的介绍
InMemoryChatMessageHistory
没有特殊功能,只有一个 messages
属性,用于保存消息,是 list
类型。
ConversationBufferWindowMemory
- 它有一个
chat_memory
属性,用于保存历史消息。 - 当我们从它的实例中获取消息的时候(调用它的
load_memory_variables
方法的时候),它只会返回最近的k * 2
条历史消息。
ConversationSummaryBufferMemory
除了 ConversationBufferWindowMemory
,langchain
还提供了 ConversationSummaryBufferMemory
,它会将历史消息进行摘要(当超过了指定长度的时候),然后保存摘要:
def prune(self) -> None: """Prune buffer if it exceeds max token limit""" buffer = self.chat_memory.messages curr_buffer_length = self.llm.get_num_tokens_from_messages(buffer) if curr_buffer_length > self.max_token_limit: pruned_memory = [] while curr_buffer_length > self.max_token_limit: pruned_memory.append(buffer.pop(0)) curr_buffer_length = self.llm.get_num_tokens_from_messages(buffer) self.moving_summary_buffer = self.predict_new_summary( pruned_memory, self.moving_summary_buffer )
prune
方法会在超过指定长度的时候,将历史消息进行摘要,然后保存摘要。
优缺点
优点:
- 控制了缓存内容大小
- 尽量记忆了对话的内容
缺点:
- 在缓存内容超出限制后,为控制缓存的大小,会持续通过大模型来总结较早的内容。
- 相应延迟增加很多
- 成本增加
总结
在使用 LLM 的时候,我们需要注意到 LLM 并不会记住我们之前说过的话。
但是我们可以自行保存历史消息,然后在下一次调用的时候,将之前的消息传递给 AI。
为了方便处理历史消息,langchain
提供了 ConversationBufferWindowMemory
这个工具,可以帮助我们保存历史消息,并在超过指定数量的时候删除最早的消息。