跳到内容

内存#

概念#

内存是代理系统的一个核心组件。它允许您存储和检索过去的信息。

在 LlamaIndex 中,您通常可以通过使用现有的 BaseMemory 类或创建自定义类来定制内存。

随着代理运行,它将调用 memory.put() 来存储信息,并调用 memory.get() 来检索信息。

注意: ChatMemoryBuffer 已弃用。在未来的版本中,默认将替换为 Memory 类,后者更灵活,允许进行更复杂的内存配置。本节中的示例将使用 Memory 类。默认情况下,整个框架使用 ChatMemoryBuffer 来创建聊天历史的基本缓冲区,该缓冲区为代理提供符合令牌限制的最后 X 条消息。Memory 类操作类似,但更灵活,并允许进行更复杂的内存配置。

用法#

使用 Memory 类,您可以创建一个既包含短期记忆(即消息的 FIFO 队列)又可选包含长期记忆(即随时间提取信息)的内存。

为代理配置内存#

您可以通过将其传入 run() 方法来为代理设置内存

from llama_index.core.agent.workflow import FunctionAgent
from llama_index.core.memory import Memory

memory = Memory.from_defaults(session_id="my_session", token_limit=40000)

agent = FunctionAgent(llm=llm, tools=tools)

response = await agent.run("<question that invokes tool>", memory=memory)

手动管理内存#

您还可以通过直接调用 memory.put()memory.get() 并传入聊天历史来手动管理内存。

from llama_index.core.agent.workflow import FunctionAgent
from llama_index.core.llms import ChatMessage
from llama_index.core.memory import Memory


memory = Memory.from_defaults(session_id="my_session", token_limit=40000)
memory.put_messages(
    [
        ChatMessage(role="user", content="Hello, world!"),
        ChatMessage(role="assistant", content="Hello, world to you too!"),
    ]
)
chat_history = memory.get()

agent = FunctionAgent(llm=llm, tools=tools)

# passing in the chat history overrides any existing memory
response = await agent.run(
    "<question that invokes tool>", chat_history=chat_history
)

从代理检索最新内存#

您可以通过从代理上下文中获取最新内存。

from llama_index.core.workflow import Context

ctx = Context(agent)

response = await ctx.run("<question that invokes tool>", ctx=ctx)

# get the memory
memory = await ctx.get("memory")
chat_history = memory.get()

定制内存#

短期记忆#

默认情况下,Memory 类将存储符合令牌限制的最后 X 条消息。您可以通过向 Memory 类传入 token_limitchat_history_token_ratio 参数来定制此行为。

  • token_limit (默认值:30000): 要存储的短期和长期令牌的最大数量。
  • chat_history_token_ratio (默认值:0.7): 短期聊天历史中的令牌数与总令牌限制的比率。如果聊天历史超过此比率,最旧的消息将被刷新到长期记忆中(如果启用长期记忆)。
  • token_flush_size (默认值:3000): 当聊天历史超过令牌限制时,要刷新到长期记忆中的令牌数。
memory = Memory.from_defaults(
    session_id="my_session",
    token_limit=40000,
    chat_history_token_ratio=0.7,
    token_flush_size=3000,
)

长期记忆#

长期记忆表示为 Memory Block 对象。这些对象接收从短期记忆中刷新的消息,并可选地处理它们以提取信息。然后在检索记忆时,短期记忆和长期记忆会合并在一起。

目前,有三种预定义的记忆块

  • StaticMemoryBlock: 存储静态信息的记忆块。
  • FactExtractionMemoryBlock: 从聊天历史中提取事实的记忆块。
  • VectorMemoryBlock: 从向量数据库存储和检索批量聊天消息的记忆块。

默认情况下,根据 insert_method 参数,记忆块将被插入到系统消息或最新的用户消息中。

这听起来有点复杂,但实际上很简单。让我们看一个例子

from llama_index.core.memory import (
    StaticMemoryBlock,
    FactExtractionMemoryBlock,
    VectorMemoryBlock,
)

blocks = [
    StaticMemoryBlock(
        name="core_info",
        static_content="My name is Logan, and I live in Saskatoon. I work at LlamaIndex.",
        priority=0,
    ),
    FactExtractionMemoryBlock(
        name="extracted_info",
        llm=llm,
        max_facts=50,
        priority=1,
    ),
    VectorMemoryBlock(
        name="vector_memory",
        # required: pass in a vector store like qdrant, chroma, weaviate, milvus, etc.
        vector_store=vector_store,
        priority=2,
        embed_model=embed_model,
        # The top-k message batches to retrieve
        # similarity_top_k=2,
        # optional: How many previous messages to include in the retrieval query
        # retrieval_context_window=5
        # optional: pass optional node-postprocessors for things like similarity threshold, etc.
        # node_postprocessors=[...],
    ),
]

在这里,我们设置了三个记忆块

  • core_info: 一个静态记忆块,存储关于用户的一些核心信息。静态内容可以是字符串或 TextBlockImageBlockContentBlock 对象的列表。此信息将始终插入到内存中。
  • extracted_info: 一个提取记忆块,将从聊天历史中提取信息。在这里,我们传入了用于从刷新的聊天历史中提取事实的 llm,并将 max_facts 设置为 50。如果提取的事实数量超过此限制,max_facts 将被自动汇总和减少,以便为新信息留出空间。
  • vector_memory: 一个向量记忆块,将从向量数据库存储和检索批量聊天消息。每个批量是刷新的聊天消息列表。在这里,我们传入了用于存储和检索聊天消息的 vector_storeembed_model

您还会注意到,我们为每个块设置了 priority(优先级)。这用于确定当记忆块内容(即长期记忆)+ 短期记忆超出 Memory 对象上的令牌限制时的处理方式。

当记忆块变得太长时,它们会自动“截断”。默认情况下,这仅仅意味着它们从内存中被移除,直到有足够的空间。这可以通过实现自己截断逻辑的记忆块子类进行定制。

  • priority=0: 此块将始终保留在内存中。
  • priority=1, 2, 3, 等: 这决定了当内存超出令牌限制时,记忆块被截断的顺序,以帮助整体短期记忆 + 长期记忆内容小于或等于 token_limit

现在,让我们将这些块传入 Memory

memory = Memory.from_defaults(
    session_id="my_session",
    token_limit=40000,
    memory_blocks=blocks,
    insert_method="system",
)

随着内存的使用,短期记忆将填满。一旦短期记忆超出 chat_history_token_ratio,符合 token_flush_size 的最旧消息将被刷新并发送到每个记忆块进行处理。

检索内存时,短期记忆和长期记忆将合并在一起。Memory 对象将确保短期记忆 + 长期记忆内容小于或等于 token_limit。如果更长,将使用 priority 来确定截断顺序,对记忆块调用 .truncate() 方法。

提示

默认情况下,使用 tiktoken 计算令牌。要定制此功能,您可以将 tokenizer_fn 参数设置为自定义可调用对象,该对象给定一个字符串,返回一个列表。然后使用列表的长度来确定令牌计数。

一旦内存收集了足够的信息,我们可能会看到这样的内存输出

# optionally pass in a list of messages to get, which will be forwarded to the memory blocks
chat_history = memory.get(messages=[...])

print(chat_history[0].content)

它将打印类似如下的内容

<memory>
<static_memory>
My name is Logan, and I live in Saskatoon. I work at LlamaIndex.
</static_memory>
<fact_extraction_memory>
<fact>Fact 1</fact>
<fact>Fact 2</fact>
<fact>Fact 3</fact>
</fact_extraction_memory>
<retrieval_based_memory>
<message role='user'>Msg 1</message>
<message role='assistant'>Msg 2</message>
<message role='user'>Msg 3</message>
</retrieval_based_memory>
</memory>

在这里,内存被插入到系统消息中,并为每个记忆块设置了特定部分。

定制记忆块#

虽然有预定义的记忆块可用,但您也可以创建自己的自定义记忆块。

from typing import Optional, List, Any
from llama_index.core.llms import ChatMessage
from llama_index.core.memory.memory import BaseMemoryBlock

# use generics to define the output type of the memory block
# can be str or List[ContentBlock]
class MentionCounter(BaseMemoryBlock[str]):
    """
    A memory block that counts the number of times a user mentions a specific name.
    """
    mention_name: str = "Logan"
    mention_count: int = 0

    async def _aget(self, messages: Optional[List[ChatMessage]] = None, **block_kwargs: Any) -> str:
        return f"Logan was mentioned {self.mention_count} times."

    async def _aput(self, messages: List[ChatMessage]) -> None:
        for message in messages:
            if self.mention_name in message.content:
                self.mention_count += 1

    async def atruncate(self, content: str, tokens_to_truncate: int) -> Optional[str]:
        return ""

在这里,我们定义了一个记忆块,用于计算用户提及特定名称的次数。

它的截断方法很简单,只返回一个空字符串。

远程内存#

默认情况下,Memory 类使用内存中的 SQLite 数据库。您可以通过更改数据库 URI 来插入任何远程数据库。

您可以定制表名,也可以可选地直接传入异步引擎。这对于管理自己的连接池很有用。

from llama_index.core.memory import Memory

memory = Memory.from_defaults(
    session_id="my_session",
    token_limit=40000,
    async_database_uri="postgresql+asyncpg://postgres:mark90@localhost:5432/postgres",
    # Optional: specify a table name
    # table_name="memory_table",
    # Optional: pass in an async engine directly
    # this is useful for managing your own connection pool
    # async_engine=engine,
)

内存 vs 工作流上下文#

在本文档的这一点上,您可能遇到过使用工作流并序列化 Context 对象以保存和恢复特定工作流状态的情况。工作流 Context 是一个复杂的对象,它保存关于工作流的运行时信息,以及在工作流步骤之间共享的键/值对。

相比之下,Memory 对象是一个更简单的对象,仅保存 ChatMessage 对象,以及可选的用于长期记忆的 MemoryBlock 对象列表。

在大多数实际情况下,您最终会同时使用两者。如果您没有定制内存,则序列化 Context 对象就足够了。

from llama_index.core.workflow import Context

ctx = Context(workflow)

# serialize the context
ctx_dict = ctx.to_dict()

# deserialize the context
ctx = Context.from_dict(workflow, ctx_dict)

在其他情况下,例如使用 FunctionAgentAgentWorkflowReActAgent 时,如果您定制了内存,那么您将需要将其作为单独的运行时参数提供(特别是由于除了默认情况外,Memory 对象不可序列化)。

response = await agent.run("Hello!", memory=memory)

最后,在某些情况(例如人在回路)下,您需要同时提供 Context(用于恢复工作流)和 Memory(用于存储聊天历史)。

response = await agent.run("Hello!", ctx=ctx, memory=memory)

(已弃用)内存类型#

llama_index.core.memory 中,我们提供了几种不同的内存类型

  • ChatMemoryBuffer: 一个基本内存缓冲区,存储符合令牌限制的最后 X 条消息。
  • ChatSummaryMemoryBuffer: 一个内存缓冲区,存储符合令牌限制的最后 X 条消息,并在对话过长时定期汇总对话。
  • VectorMemory: 一个内存,用于从向量数据库存储和检索聊天消息。它不保证消息的顺序,并返回与最新用户消息最相似的消息。
  • SimpleComposableMemory: 一个组合多个内存的内存。通常用于将 VectorMemoryChatMemoryBufferChatSummaryMemoryBuffer 结合使用。

示例#

您可以在下方找到一些内存实际应用的示例

注意:已弃用的示例: - 聊天内存缓冲区 - 聊天摘要内存缓冲区 - 组合式内存 - 向量内存 - Mem0 内存