Memory模块¶
Memory模块是ERNIEbot Agent的一个组件,可以作为无状态的LLM的记忆能力的补充,从而提升对话系统中的多轮交互效果。Memory的机制可以理解为将过去的消息进行存储并传递给LLM,从而LLM可以复用和对用户的问题相关的信息进行回答。
因此对于Memory模块的设计主要考虑两个维度:
- 存储容量:Memory模块能存储的消息不是无限多的,其不能超过LLM的上下文窗口长度,因此当消息超过了窗口长度后,需要将不相干的消息进行删除。
- 语义相关度:Memory模块的作用时用于回答用户问题,因此需要保证存储的消息尽量与问题相关。
目前,从存储容量角度出发,ERNIE Bot Agent中建立了三个的Memory相关类:
WholeMemory:全量记忆功能,存储所有消息。SlidingWindowMemory:滑窗截断消息,限制对话轮数。LimitTokensMemory:Token限制memory,限制对话memory种的token数量。
WholeMemory¶
WholeMemory是全量记忆,存储所有消息。
from erniebot_agent.memory import AIMessage, HumanMessage, WholeMemory
memory = WholeMemory()
humanmessage = HumanMessage("请将这个图片中的单词识别出来")
aimessage = AIMessage("好,这个图片中的单词为meticulous") # Fake AIMessage
memory.add_message(humanmessage)
memory.add_message(aimessage)
humanmessage = HumanMessage("这个单词meticulous是什么意思呢?")
aimessage = AIMessage("这个单词的意思是挑剔的,关注细节的") # Fake AIMessage
memory.add_message(humanmessage)
memory.add_message(aimessage)
print(memory.get_messages())
[<HumanMessage role: 'user', content: '请将这个图片中的单词识别出来', token_count: 14>, <AIMessage role: 'assistant', content: '好,这个图片中的单词为meticulous', token_count: 11>, <HumanMessage role: 'user', content: '这个单词meticulous是什么意思呢?', token_count: 21>, <AIMessage role: 'assistant', content: '这个单词的意思是挑剔的,关注细节的', token_count: 16>]
上面的例子可以看到,我们实例化了WholeMemory,然后通过memory.add_message(msg)方法将消息添加到memory中,并通过memory.get_messages()方法获取所有消息。
除了使用memory.add_message(msg)方法添加消息外,我们也可以通过memory.add_messages(msgs)方法批量添加消息。
messages = [
HumanMessage("请帮我把这个单词meticulous存储到单词本中"),
AIMessage("好的,单词meticulous已经存储到单词本中"),
HumanMessage("请问现在我的单词本中都有什么单词呢?"),
AIMessage("单词中目前有单词:meticulous"),
]
memory.add_messages(messages)
print(memory.get_messages())
[<HumanMessage role: 'user', content: '请将这个图片中的单词识别出来', token_count: 14>, <AIMessage role: 'assistant', content: '好,这个图片中的单词为meticulous', token_count: 11>, <HumanMessage role: 'user', content: '这个单词meticulous是什么意思呢?', token_count: 21>, <AIMessage role: 'assistant', content: '这个单词的意思是挑剔的,关注细节的', token_count: 16>, <HumanMessage role: 'user', content: '请帮我把这个单词meticulous存储到单词本中', token_count: 25>, <AIMessage role: 'assistant', content: '好的,单词meticulous已经存储到单词本中', token_count: 14>, <HumanMessage role: 'user', content: '请问现在我的单词本中都有什么单词呢?', token_count: 18>, <AIMessage role: 'assistant', content: '单词中目前有单词:meticulous', token_count: 9>]
我们使用memory.add_messages(messages)添加了一组消息,然后使用memory.get_messages()来获取所有消息,可以发现所有的message都正确加入。
如果在对话结束想要删除所有的消息记录,我们可以使用memory.clear_chat_history()方法。
memory.clear_chat_history()
print(memory.get_messages())
[]
其他带截断功能的memory¶
ERNIEbot Agent中还提供了SlidingWindowMemory和LimitTokensMemory两个memory模块,它们分别对应于滑动窗口截断记忆和token数量限制截断记忆。这两类阶段记忆的原理是类似的,即在加入消息时根据不同限制条件对历史消息进行删除。
SlidingWindowMemory¶
SlidingWindowMemory(max_round, retained_round)是滑动窗口截断记忆,在memory中存储固定轮数的消息。其中max_round表示memory中消息最多存储的轮数,retained_round表示在memory中会保留的初始消息的轮数,用于初始消息比较重要的场景。
LimitTokensMemory¶
LimitTokensMemory(max_token_limit)是token数量限制截断记忆,在memory中存储固定数量的token。其中max_token_limit表示memory中最多存储的消息的token数量,超过这个限制后,从头开始对消息进行删除。
下面我们给出两段代码示例,分别展示SlidingWindowMemory和LimitTokensMemory的使用效果。
messages = [
HumanMessage("请帮我把这个单词meticulous存储到单词本中"),
AIMessage("好的,单词meticulous已经存储到单词本中"),
HumanMessage("请问现在我的单词本中都有什么单词呢?"),
AIMessage("单词中目前有单词:meticulous"),
HumanMessage("我想对单词本中的单词全部打印出对应的中文含义用于记忆"),
AIMessage("好的,单词本中包括:meticulous的意思是挑剔的,关注细节的"),
]
from erniebot_agent.memory import SlidingWindowMemory
memory = SlidingWindowMemory(max_round=2, retained_round=0)
memory.add_messages(messages)
print("裁剪前的消息为:", messages)
print("裁剪后的消息为:", memory.get_messages())
裁剪前的消息为: [<HumanMessage role: 'user', content: '请帮我把这个单词meticulous存储到单词本中', token_count: 25>, <AIMessage role: 'assistant', content: '好的,单词meticulous已经存储到单词本中', token_count: 14>, <HumanMessage role: 'user', content: '请问现在我的单词本中都有什么单词呢?', token_count: 18>, <AIMessage role: 'assistant', content: '单词中目前有单词:meticulous', token_count: 9>, <HumanMessage role: 'user', content: '我想对单词本中的单词全部打印出对应的中文含义用于记忆', token_count: 26>, <AIMessage role: 'assistant', content: '好的,单词本中包括:meticulous的意思是挑剔的,关注细节的', token_count: 21>] 裁剪后的消息为: [<HumanMessage role: 'user', content: '请问现在我的单词本中都有什么单词呢?', token_count: 18>, <AIMessage role: 'assistant', content: '单词中目前有单词:meticulous', token_count: 9>, <HumanMessage role: 'user', content: '我想对单词本中的单词全部打印出对应的中文含义用于记忆', token_count: 26>, <AIMessage role: 'assistant', content: '好的,单词本中包括:meticulous的意思是挑剔的,关注细节的', token_count: 21>]
from erniebot_agent.memory import SlidingWindowMemory
memory = SlidingWindowMemory(max_round=2, retained_round=1)
memory.add_messages(messages)
print("裁剪前的消息为:", messages)
print("裁剪后的消息为:", memory.get_messages())
The token count of the message has been set before The token count of the message has been set before The token count of the message has been set before
裁剪前的消息为: [<HumanMessage role: 'user', content: '请帮我把这个单词meticulous存储到单词本中', token_count: 25>, <AIMessage role: 'assistant', content: '好的,单词meticulous已经存储到单词本中', token_count: 14>, <HumanMessage role: 'user', content: '请问现在我的单词本中都有什么单词呢?', token_count: 18>, <AIMessage role: 'assistant', content: '单词中目前有单词:meticulous', token_count: 9>, <HumanMessage role: 'user', content: '我想对单词本中的单词全部打印出对应的中文含义用于记忆', token_count: 26>, <AIMessage role: 'assistant', content: '好的,单词本中包括:meticulous的意思是挑剔的,关注细节的', token_count: 21>] 裁剪后的消息为: [<HumanMessage role: 'user', content: '请帮我把这个单词meticulous存储到单词本中', token_count: 25>, <AIMessage role: 'assistant', content: '好的,单词meticulous已经存储到单词本中', token_count: 14>, <HumanMessage role: 'user', content: '我想对单词本中的单词全部打印出对应的中文含义用于记忆', token_count: 26>, <AIMessage role: 'assistant', content: '好的,单词本中包括:meticulous的意思是挑剔的,关注细节的', token_count: 21>]
上述两个例子我们分别展示SlidingWindowMemory保留一轮消息,和同时保留首轮消息的效果。
from erniebot_agent.memory import LimitTokensMemory
memory = LimitTokensMemory(max_token_limit=60)
memory.add_messages(messages)
print("裁剪前的消息为:", messages)
print("裁剪后的消息为:", memory.get_messages())
The token count of the message has been set before The token count of the message has been set before The token count of the message has been set before
裁剪前的消息为: [<HumanMessage role: 'user', content: '请帮我把这个单词meticulous存储到单词本中', token_count: 25>, <AIMessage role: 'assistant', content: '好的,单词meticulous已经存储到单词本中', token_count: 14>, <HumanMessage role: 'user', content: '请问现在我的单词本中都有什么单词呢?', token_count: 18>, <AIMessage role: 'assistant', content: '单词中目前有单词:meticulous', token_count: 9>, <HumanMessage role: 'user', content: '我想对单词本中的单词全部打印出对应的中文含义用于记忆', token_count: 26>, <AIMessage role: 'assistant', content: '好的,单词本中包括:meticulous的意思是挑剔的,关注细节的', token_count: 21>] 裁剪后的消息为: [<AIMessage role: 'assistant', content: '单词中目前有单词:meticulous', token_count: 9>, <HumanMessage role: 'user', content: '我想对单词本中的单词全部打印出对应的中文含义用于记忆', token_count: 26>, <AIMessage role: 'assistant', content: '好的,单词本中包括:meticulous的意思是挑剔的,关注细节的', token_count: 21>]
细心的读者可以观察到,我们在上面的裁剪中,似乎不仅需要满足token限制的条件,因为输出的消息长度本来为两条也可以满足token数量限制要求。这是因为我们还有一个LLM输入需要为奇数条消息的限制条件,由于三条消息超过了token限制,因此最后只剩下了一条消息。
总结¶
综上,我们展示了Memory的设计出发点,并展示了三种Memory的使用方法和裁剪效果。
可以看到,我们讲到了两种memory的设计出发点,但是并未考虑第二点语义信息,因此后续我们还会持续补充更多的memory满足不同应用和效果的需求。