langchain 入门指南 - 让 AI 记住你说过的话

在我们通过 web 端的聊天界面与 AI 对话时,AI 会记住你说过的话。这样,你可以在对话中引用之前的话语,或者在之后的对话中提到之前的话语。

但是如果我们像下面这样调用 API 的时候,就会发现 AI 不会记住我们之前说过的话:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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)

输出:

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 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)

输出:

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 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}")

输出:

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 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}")

输出:

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

除了 ConversationBufferWindowMemorylangchain 还提供了 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 这个工具,可以帮助我们保存历史消息,并在超过指定数量的时候删除最早的消息。