在我们通过 web 端的聊天界面与 AI 对话时,AI
会记住你说过的话。这样,你可以在对话中引用之前的话语,或者在之后的对话中提到之前的话语。
但是如果我们像下面这样调用 API 的时候,就会发现 AI
不会记住我们之前说过的话:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 from langchain_openai import ChatOpenAIchat = 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)
输出:
1 2 3 4 5 这句话的意思是今天广州的天气非常好,晴朗无云,气温在26摄氏度到35摄氏度之间。这是一个适合户外活动的好天气,但也要注意防晒和补水,因为气温较高。 很抱歉,我无法提供实时天气信息或建议。要了解今天的广州适合穿什么,您可以查看当地的天气预报,了解当前的气温、湿度和天气状况,然后根据这些信息选择合适的衣物。 通常,广州属于亚热带季风气候,夏季炎热潮湿,冬季温和,春秋季节宜人。根据季节和天气预报,您可以选择穿短袖、长袖、薄外套或厚外套等。 别忘了查看是否需要携带雨具,因为广州的降雨量也比较丰富。
虽然我们告诉了 LLM 今天广州的天气,但是在第二次调用的时候,AI
并没有记住我们之前说过的话,所以不能依据当前的天气状况给我提供穿衣建议。
为什么 AI 不会记住我说过的话
这是因为大模型本身并不具备记忆功能。在我们每次使用大模型的 API
的时候,它都是基于训练模型时候的数据以及我们传递的信息来进行推理的。
如果让大模型记住我们说过的话,那么它需要存储的信息量会非常庞大,这样的成本是非常高昂的。
同时,如果每一次调用的时候,都在一个庞大的上下文中进行推理,那么推理的时间也会非常长,消耗的资源会非常多。
所以,大模型通常是不会记住我们说过的话的。
解决办法:我们自己记住
既然大模型记不住我们说过的话,那唯一的办法就是我们自己记住,然后下次调用的时候,将之前的话语传递给
AI。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 from langchain_openai import ChatOpenAIfrom langchain_core.messages import HumanMessage, AIMessagechat = 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)
输出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 [ 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
,它可以记住指定次数的消息,然后在超过指定次数的时候,删除最早的消息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 from langchain_core.chat_history import InMemoryChatMessageHistoryfrom langchain_core.messages import AIMessagefrom langchain_core.runnables import RunnableWithMessageHistoryfrom langchain_openai import ChatOpenAIfrom langchain.memory import ConversationBufferWindowMemoryllm = ChatOpenAI( model="yi-large" , temperature=0.3 , max_tokens=200 , api_key='your key' , base_url="https://api.lingyiwanwu.com/v1" , ) store = {} 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) print (f"response 1: {response.content} " )response = chain.invoke('今天广州适合穿什么?' , config=conf) print (f"response 2: {response.content} " )
输出:
1 2 3 4 5 6 7 8 response 1: 这句话的意思是今天广州的天气非常好,晴朗无云,气温在26摄氏度到35摄氏度之间。这个温度范围对于夏天来说是比较舒适的,适合户外活动。 response 2: 根据您提供的信息,今天广州的天气晴朗,气温在26到35摄氏度之间。这个温度范围适合穿着轻薄、透气的衣物。以下是一些建议: 1. 上衣:可以选择短袖T恤、薄衬衫或棉质衣物,避免穿得过多导致出汗后衣服湿透。 2. 下装:可以穿短裤、七分裤或轻薄的牛仔裤。如果是在室内或者空调环境中,可以考虑带一件长裤以防着凉。 3. 鞋子:舒适的凉鞋、帆布鞋或运动鞋都是不错的选择。 4. 配件:可以戴一顶遮阳帽和太阳镜来保护皮肤和眼睛免受紫外线伤害。如果需要长时间在户外,可以考虑涂抹防晒霜。 5. 携带物品:由于气温较高,建议随身携带水瓶以保持水分,同时可以携带
相比之下,现在我们并不需要每次都手动保存历史消息,而是交给
ConversationBufferWindowMemory
来处理。
这样,我们就可以更加专注于对话的内容,而不用担心历史消息的处理。
在上面这个例子中,我们指定了
k=10
,也就是说,只保存最近的 20 条消息, 超过 20
条的消息之后会删除最早的消息(这是因为在底层实现中,会使用
k * 2
,而不是 k
)。
我们可以指定 k=1
来验证一下():
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 from langchain_core.chat_history import InMemoryChatMessageHistoryfrom langchain_core.messages import AIMessagefrom langchain_core.runnables import RunnableWithMessageHistoryfrom langchain_openai import ChatOpenAIfrom langchain.memory import ConversationBufferWindowMemoryllm = ChatOpenAI( model="yi-large" , temperature=0.3 , max_tokens=200 , api_key='your key' , base_url="https://api.lingyiwanwu.com/v1" , ) store = {} 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 ] 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) print (f"response 1: {response.content} " )response = chain.invoke('这是一条无用的消息,请你忽略。' , config=conf) print (f"response 2: {response.content} " )response = chain.invoke('今天广州适合穿什么?' , config=conf) print (f"response 3: {response.content} " )
输出:
1 2 3 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
,它会将历史消息进行摘要(当超过了指定长度的时候),然后保存摘要:
1 2 3 4 5 6 7 8 9 10 11 12 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
这个工具,可以帮助我们保存历史消息,并在超过指定数量的时候删除最早的消息。