主题
概念
记忆是智能体系统的核心组件。它让你能够存取过去发生的信息。
在 LlamaIndex 里,定制记忆通常有两种做法:沿用现成的 BaseMemory 类,或者自己写一个。
agent 运行过程中会调用 memory.put() 存信息、调用 memory.get() 取信息。
注意:ChatMemoryBuffer 已弃用。未来的某个版本里,默认将被更灵活、能支持更复杂配置的 Memory 类取代,本节示例都会使用 Memory 类。目前框架各处默认使用 ChatMemoryBuffer,它创建一个基础的聊天历史缓冲区,给 agent 提供能塞进 token 限制的最近 X 条消息。Memory 类的运作方式类似,但更灵活,支持更复杂的记忆配置。
用量
使用 Memory 类,你可以创建一个同时具备短期记忆(即一个消息的先进先出队列)与可选长期记忆(即随时间不断抽取信息)的记忆对象。
为 agent 配置记忆
你可以在调用 run() 方法时把记忆传进去,为 agent 设置 memory:
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)1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
手动管理记忆
你也可以直接调用 memory.put_messages() 与 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
)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
取回 agent 的最新记忆
要从 agent 拿到最新的记忆,可以从 agent context 中取出:
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.store.get("memory")
chat_history = memory.get()1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
定制记忆
短期记忆
默认情况下,Memory 类会保存能塞进 token 限制的最近 X 条消息。你可以给 Memory 类传 token_limit 与 chat_history_token_ratio 参数来自定义这一行为。
token_limit(默认:30000):短期与长期记忆总共最多存放多少 token。chat_history_token_ratio(默认:0.7):短期聊天历史占总 token 上限的比例。一旦聊天历史超过这个比例,最早的消息就会被刷入长期记忆(前提是启用了长期记忆)。token_flush_size(默认:3000):当聊天历史超出 token 限制时,一次刷入长期记忆的 token 数量。
memory = Memory.from_defaults(
session_id="my_session",
token_limit=40000,
chat_history_token_ratio=0.7,
token_flush_size=3000,
)1
2
3
4
5
6
2
3
4
5
6
长期记忆
长期记忆由 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=[...],
),
]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
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
这里我们配置了三个记忆块:
core_info:一个静态记忆块,存放关于用户的某些核心信息。静态内容可以是一个字符串,也可以是TextBlock、ImageBlock等ContentBlock对象组成的列表。这些信息始终会被插入记忆。extracted_info:一个抽取型记忆块,会从聊天历史里抽取信息。这里我们传入了用于从刷出的聊天历史中抽取事实的llm,并把max_facts设为 50。一旦抽取的事实数超过这个上限,max_facts里的内容会被自动摘要并压缩,为新信息腾出空间。vector_memory:一个向量记忆块,把成批聊天消息存入向量库并按需取回。每一批就是一批被刷出的聊天消息。这里我们传入了用于存取的vector_store与embed_model。
你还会注意到我们为每个块设置了 priority。它决定当「记忆块内容(即长期记忆)+ 短期记忆」超出 Memory 对象的 token 上限时该如何处理。
当记忆块过长时,它们会被自动“截断”。默认的含义很简单:把它们从记忆中移除,直到重新腾出空间。你可以通过实现自定义截断逻辑的记忆块子类来改变这一行为。
priority=0:该块始终保留在记忆中。priority=1, 2, 3, 等等:决定记忆超出 token 上限时各块的截断顺序,从而使「短期记忆 + 长期记忆」的总内容不超过token_limit。
现在,把这些块传给 Memory 类:
memory = Memory.from_defaults(
session_id="my_session",
token_limit=40000,
memory_blocks=blocks,
insert_method="system",
)1
2
3
4
5
6
2
3
4
5
6
随着记忆被使用,短期记忆会逐渐填满。一旦短期记忆超过 chat_history_token_ratio,那些能装进 token_flush_size 的最早消息就会被刷出,并交给每个记忆块处理。
读取记忆时,短期记忆与长期记忆会被合并。Memory 对象会确保「短期记忆 + 长期记忆」的内容不超过 token_limit;如果超出,就会对各记忆块调用 .truncate(),并按 priority 决定截断顺序。
提示
默认使用 tiktoken 统计 token。想自定义,可以把 tokenizer_fn 参数设成一个可调用的对象:给它一个字符串,它返回一个列表,列表长度就作为 token 数。
一旦记忆积累了足够的信息,我们可能会看到类似这样的内容:
# 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)1
2
3
4
2
3
4
打印出来大致是这样:
<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>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
这里,记忆被插入到系统消息中,并为每个记忆块留出专门的小节。
自定义记忆块
除了预置的记忆块,你也可以创建自己的记忆块。
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 ""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
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
这里我们定义了一个记忆块,用来统计用户提到某个特定名字的次数。
它的 truncate 方法很简单,直接返回一个空字符串。
远端记忆
默认情况下,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,
)1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
自定义聊天存储
如果 SQLAlchemy 不适合你的基础设施,可以实现 AsyncDBChatStore 接口并作为 chat_store 传入。给定 chat_store 时,那些 SQL 专用参数(table_name、async_database_uri、async_engine、db_schema)会被忽略。
from llama_index.core.memory import Memory
from llama_index.core.storage.chat_store.base_db import (
AsyncDBChatStore,
MessageStatus,
)
class MyChatStore(AsyncDBChatStore):
# implement get_messages, add_message(s), set_messages,
# delete_message(s), delete_oldest_messages,
# archive_oldest_messages, count_messages and get_keys
...
memory = Memory.from_defaults(
session_id="my_session",
token_limit=40000,
chat_store=MyChatStore(),
)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
记忆与工作流上下文
读到这里,你可能已经遇到过这样的场景:在使用 Workflow 时,需要序列化一个 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)1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
而在其他场景,比如使用 FunctionAgent、AgentWorkflow 或 ReActAgent 并定制了记忆时,你就需要把它作为单独的运行参数传进去(尤其是超出默认配置之后,Memory 对象是不可序列化的)。
response = await agent.run("Hello!", memory=memory)1
最后,还有一些情形(比如人在环路)需要你同时提供 Context(用于恢复工作流)与 Memory(用于存储聊天历史)。
response = await agent.run("Hello!", ctx=ctx, memory=memory)1
(已弃用)记忆类型
在 llama_index.core.memory 里,我们提供几种不同的记忆类型:
ChatMemoryBuffer:基础记忆缓冲区,保存能塞进 token 限制的最近 X 条消息。ChatSummaryMemoryBuffer:记忆缓冲区,保存能塞进 token 限制的最近 X 条消息,并在对话过长时周期性地对其做摘要。VectorMemory:把聊天消息存入向量库并从中取回。它不保证消息顺序,返回的是与最新用户消息最相似的那些消息。SimpleComposableMemory:把多个记忆组合在一起的记忆。通常用于把VectorMemory与ChatMemoryBuffer或ChatSummaryMemoryBuffer组合使用。
示例
下面可以看到几个记忆实际使用的例子:
**注意:**以下为已弃用的示例: