内存
memory模块的主要目的是存储维护对话上下文所需的信息。
您可以在taskweaver/memory/memory.py中找到实现代码。
我们已在concepts章节介绍了Round和Post等各种概念,
这些是Memory模块的基础构建单元。
内存中存储的信息分为两种:
- 对话历史: 这包括用户与TaskWeaver中各种角色之间迄今为止发生的对话。
- 共享内存: 这包括在TaskWeaver中各角色之间有意共享的信息。
让我们简要讨论这两种类型的信息。
按角色分类的对话历史记录
TaskWeaver智能体由一个或多个角色组成。每个角色都有自己的对话历史。 在TaskWeaver中,我们以星型拓扑结构编排角色,其中规划器位于中心,角色位于外围。 用户仅与规划器交互,而规划器则与各个角色交互,进行规划并指示其他角色执行任务, 如下图所示。
尽管这种固定的编排方式存在局限性,但它保留了各角色的独立性。每个角色无需了解其他角色,甚至无需知晓它们的存在。对于任何外围角色而言,规划器(Planner)是唯一的接触点——即其真正的"用户",每个角色只需专注于自身专长领域。规划器需要负责调用多个角色来处理复杂任务,并通过协调它们来实现目标。
每个角色的对话历史都存储在内存中。当某个角色需要准备回复时,它可以参考对话历史来理解对话的上下文。具体来说,这通常是为大语言模型准备提示词的过程,包含之前所有的聊天轮次和当前请求。每个角色只关心自己发送或接收的帖子。
共享内存
虽然我们希望保持各角色的独立性,但在某些情况下角色之间需要共享信息。一个常见场景是两个外围角色之间的信息共享。例如,Planner可能会要求Code Interpreter根据Data Scientist角色提供的指南生成代码。这种情况下,Planner需要与Code Interpreter共享该指南。理论上,这可以通过Planner向Code Interpreter重复指南来实现,如下图所示。
然而,我们发现规划器可能无法准确重复指南,从而导致沟通失误。
另一个使用场景是某个角色需要存储一些在所有角色间共享的控制状态。例如,智能体需要处理多种类型的任务。当前用户请求的"类型"仅由一个角色(例如TypeDeterminer)确定,但其他所有角色都需要知道该类型以准备响应。在这种情况下,确定类型的角色可以将类型存储在共享内存中,其他角色可以通过引用共享内存来获取类型。
箭头中的数字表示信息流的顺序。
基于上述原因,我们引入了共享内存(Shared Memory)的概念。共享内存是一种特殊的Attachment,会被附加到想要共享信息的角色的帖子中。该附件在extra字段中包含一个SharedMemoryEntry实例。SharedMemoryEntry包含以下字段:
class SharedMemoryEntry:
type: Literal[...] # The type of the shared memory entry
content: str # The content of the shared information
scope: Literal["round", "conversation"] # The scope of the shared information
id: str # The id of the shared memory entry
理解scope字段对于理解共享内存至关重要。scope字段决定了共享信息的范围。
如果scope是round,共享信息仅对当前轮次有效。
否则,如果scope是conversation,共享信息对整个会话都有效。
有人可能会问,为什么不将共享信息存储在单独的数据结构中,而是放在帖子的附件里。 原因在于,如果某轮对话失败,我们需要同时移除该轮中的共享信息。 通过将共享信息存储在附件中,作为帖子的一部分,我们可以根据对话轮次状态轻松筛选出共享信息。 这与设计数据库事务操作日志系统的思路类似。
共享信息的使用方可以通过type来获取对当前聊天轮次有效的共享内存条目,包括对话相关的条目。然而,如果某个角色存在多个相同type的共享内存条目,则只有最新的条目有效。换句话说,来自同一角色的同类型共享内存条目,后写入的会覆盖先前的。
在taskweaver/planner/planner.py中提供了共享内存的参考实现,其中Planner角色会与其他角色共享计划细节。