跳转到内容

使用LlamaIndex记忆减少多轮对话混淆

近期研究显示,大型语言模型在多轮对话中的性能会显著下降。

为避免这种情况,我们可以在LlamaIndex中实现自定义的短期和长期记忆机制,确保对话轮次不会过长,并在此过程中不断压缩记忆。

使用本笔记本中的代码,您可能会在自己的智能体中看到改进,因为它致力于限制聊天历史中的对话轮次。

注意: 本笔记本使用 llama-index-core>=0.12.37 进行测试,因为该版本包含了一些修复,使其能够良好运行。

%pip install -U llama-index-core llama-index-llms-openai
import os
os.environ["OPENAI_API_KEY"] = "sk-..."

要实现这一点,我们需要两样东西

  1. 一个记忆块,将所有过去的聊天消息压缩为单个字符串,同时保持令牌限制
  2. 一个使用该内存块的 Memory 实例,其配置的令牌限制确保多轮对话始终刷新到内存块中进行处理

首先,自定义内存块:

import tiktoken
from pydantic import Field
from typing import List, Optional, Any
from llama_index.core.llms import ChatMessage, TextBlock
from llama_index.core.memory import Memory, BaseMemoryBlock
class CondensedMemoryBlock(BaseMemoryBlock[str]):
current_memory: List[str] = Field(default_factory=list)
token_limit: int = Field(default=50000)
tokenizer: tiktoken.Encoding = tiktoken.encoding_for_model(
"gpt-4o"
) # all openai models use 4o tokenizer these days
async def _aget(
self, messages: Optional[List[ChatMessage]] = None, **block_kwargs: Any
) -> str:
"""Return the current memory block contents."""
return "\n".join(self.current_memory)
async def _aput(self, messages: List[ChatMessage]) -> None:
"""Push messages into the memory block. (Only handles text content)"""
# construct a string for each message
for message in messages:
text_contents = "\n".join(
block.text
for block in message.blocks
if isinstance(block, TextBlock)
)
memory_str = f"<message role={message.role}>"
if text_contents:
memory_str += f"\n{text_contents}"
# include additional kwargs, like tool calls, when needed
# filter out injected session_id
kwargs = {
key: val
for key, val in message.additional_kwargs.items()
if key != "session_id"
}
if kwargs:
memory_str += f"\n({kwargs})"
memory_str += "\n</message>"
self.current_memory.append(memory_str)
# ensure this memory block doesn't get too large
message_length = sum(
len(self.tokenizer.encode(message))
for message in self.current_memory
)
while message_length > self.token_limit:
self.current_memory = self.current_memory[1:]
message_length = sum(
len(self.tokenizer.encode(message))
for message in self.current_memory
)

然后,一个 Memory 实例,在配置短期记忆的极低令牌限制时使用该模块:

block = CondensedMemoryBlock(name="condensed_memory")
memory = Memory.from_defaults(
session_id="test-mem-01",
token_limit=60000,
token_flush_size=5000,
async_database_uri="sqlite+aiosqlite:///:memory:",
memory_blocks=[block],
insert_method="user",
# Prevent the short-term chat history from containing too many turns!
# This limit will effectively mean that the short-term memory is always flushed
chat_history_token_ratio=0.0001,
)

让我们使用一些模拟消息来探索其用法,并观察内存是如何管理的。

initial_messages = [
ChatMessage(role="user", content="Hello! My name is Logan"),
ChatMessage(role="assistant", content="Hello! How can I help you?"),
ChatMessage(role="user", content="What is the capital of France?"),
ChatMessage(role="assistant", content="The capital of France is Paris"),
]
await memory.aput_messages(initial_messages)

接下来,让我们添加下一条用户消息!

await memory.aput_messages(
[ChatMessage(role="user", content="What was my name again?")]
)

这样,我们就能在发送给大型语言模型之前查看聊天记录的样子。

chat_history = await memory.aget()
for message in chat_history:
print(message.role)
print(message.content)
print()
MessageRole.USER
<memory>
<condensed_memory>
<message role=MessageRole.USER>
Hello! My name is Logan
</message>
<message role=MessageRole.ASSISTANT>
Hello! How can I help you?
</message>
<message role=MessageRole.USER>
What is the capital of France?
</message>
<message role=MessageRole.ASSISTANT>
The capital of France is Paris
</message>
</condensed_memory>
</memory>
What was my name again?

太棒了!即使我们添加了许多消息,它也会被压缩成一条用户消息!

接下来让我们尝试使用一个实际的智能体。

在这里,我们可以创建一个FunctionAgent,它使用一些简单的工具来利用我们的记忆。

from llama_index.core.agent.workflow import FunctionAgent
from llama_index.llms.openai import OpenAI
def multiply(a: float, b: float) -> float:
"""Multiply two numbers."""
return a * b
def divide(a: float, b: float) -> float:
"""Divide two numbers."""
return a / b
def add(a: float, b: float) -> float:
"""Add two numbers."""
return a + b
def subtract(a: float, b: float) -> float:
"""Subtract two numbers."""
return a - b
llm = OpenAI(model="gpt-4.1-mini")
agent = FunctionAgent(
tools=[multiply, divide, add, subtract],
llm=llm,
system_prompt="You are a helpful assistant that can do simple math operations with tools.",
)
block = CondensedMemoryBlock(name="condensed_memory")
memory = Memory.from_defaults(
session_id="test-mem-01",
token_limit=60000,
token_flush_size=5000,
async_database_uri="sqlite+aiosqlite:///:memory:",
memory_blocks=[block],
insert_method="user",
# Prevent the short-term chat history from containing too many turns!
# This limit will effectively mean that the short-term memory is always flushed
chat_history_token_ratio=0.0001,
)
resp = await agent.run("What is (3214 * 322) / 2?", memory=memory)
print(resp)
The value of (3214 * 322) / 2 is 517454.0.
current_chat_history = await memory.aget()
for message in current_chat_history:
print(message.role)
print(message.content)
print()
MessageRole.ASSISTANT
The value of (3214 * 322) / 2 is 517454.0.
MessageRole.USER
<memory>
<condensed_memory>
<message role=MessageRole.USER>
What is (3214 * 322) / 2?
</message>
<message role=MessageRole.ASSISTANT>
({'tool_calls': [{'index': 0, 'id': 'call_U78I0CSWETFQlRBCWPpswEmq', 'function': {'arguments': '{"a": 3214, "b": 322}', 'name': 'multiply'}, 'type': 'function'}, {'index': 1, 'id': 'call_3eFXqalMN9PyiCVEYE073bEl', 'function': {'arguments': '{"a": 3214, "b": 2}', 'name': 'divide'}, 'type': 'function'}]})
</message>
<message role=MessageRole.TOOL>
1034908
({'tool_call_id': 'call_U78I0CSWETFQlRBCWPpswEmq'})
</message>
<message role=MessageRole.TOOL>
1607.0
({'tool_call_id': 'call_3eFXqalMN9PyiCVEYE073bEl'})
</message>
<message role=MessageRole.ASSISTANT>
({'tool_calls': [{'index': 0, 'id': 'call_GvtLKm7FCzlaucfYnaxOLBVW', 'function': {'arguments': '{"a":1034908,"b":2}', 'name': 'divide'}, 'type': 'function'}]})
</message>
<message role=MessageRole.TOOL>
517454.0
({'tool_call_id': 'call_GvtLKm7FCzlaucfYnaxOLBVW'})
</message>
</condensed_memory>
</memory>

完美!由于记忆库中还没有新的用户消息,它添加了一条包含我们当前记忆的消息。在下一条用户消息到来时,该记忆和用户消息将会像我们之前看到的那样合并。

让我们尝试几个后续问题来确认这工作正常

resp = await agent.run(
"What was the last question I asked you?", memory=memory
)
print(resp)
The last question you asked was: "What is (3214 * 322) / 2?"
resp = await agent.run(
"And how did you go about answering that message?", memory=memory
)
print(resp)
To answer your question "What is (3214 * 322) / 2?", I followed these steps:
1. First, I multiplied 3214 by 322.
2. Then, I divided the result of that multiplication by 2.
3. Finally, I provided you with the result of the calculation, which is 517454.0.