Adding Long Term Memory to OpenGPTs

为 OpenGPTs 添加长期记忆

8 分钟阅读

三周前,我们推出了 OpenGPTs,这是一个以开源方式实现的 OpenAI GPT 和 Assistant API。OpenGPTs 允许实现对话式 Agent - 一种灵活且具有未来感的认知架构。Agent 的一个重要组成部分是记忆。在目前的实现中,GPT、OpenGPTs 和 Assistants API 实际上只支持基本的对话记忆。长期记忆是一个尚未充分探索的领域。在这篇博文中,我们将稍微谈谈我们对记忆的看法,为什么它如此未被充分探索,然后介绍我们在 OpenGPTs 中实现和公开的特定记忆,以创建一个“龙与地下城”地下城主。

LLM 是无状态的

LLM 本身是无状态的 - 如果你传入第一个输入,然后是第二个输入,它在处理第二个输入时不会记住第一个输入。因此,几乎所有的 LLM 也都是无状态的。这意味着,如果你想要拥有任何记忆的概念,用户需要在传递给 LLM 之前在他们那边维护这种状态。

OpenAI 几周前发布的 Assistants API 是第一个主要的例外。这个 API 允许你跟踪消息列表。然后你可以在这个消息列表上调用 Assistant(一个 LLM),它会将消息附加到该线程中。LLM 本身仍然是无状态的,但暴露的 API 不再是。

即使引入了这个,使用的主要记忆类型仍然只是对话记忆。

对话记忆

通过对话记忆,我们简单地指记住对话中先前消息的能力。这是通过跟踪先前消息的列表,然后将其传递到提示中,再传递到 LLM 中来完成的。

当先前消息的列表变得相当长时,这开始出现问题。首先,如果列表足够长,它可能会变得比上下文窗口更长。其次,即使它没有溢出上下文窗口,它也可能足够长,以至于 LLM 难以适当地关注所有消息(请参阅 Greg Kamradt 在这里进行的一些很棒的研究,关于长上下文窗口的局限性)。

处理这个问题的最明显方法是只使用最近的 N 条消息。这导致了缺乏长期记忆的缺点 - 机器人可能会忘记你之前说的一切。

那么你如何处理这个问题呢?

语义记忆

语义记忆可能是下一个最常用的记忆类型。这是指找到与当前消息相似的消息,并以某种方式将它们带入提示中。

这通常通过计算每条消息的嵌入,然后找到具有相似嵌入的其他消息来完成。这基本上与驱动检索增强生成 (RAG) 的想法相同。只不过你搜索的是消息而不是文档。

例如,如果用户问“我最喜欢的水果是什么”,我们可能会找到一条以前的消息,例如“我最喜欢的水果是蓝莓”(因为它们在嵌入空间中是相似的)。然后我们可以将以前的消息作为上下文传递进去。

然而,这种方法有一些缺陷。

挑战

首先:如果见解或信息分散在多条消息中,那么可能无法检索到正确的消息。例如,如果有一系列消息,例如

AI:你最喜欢的水果是什么?
人类:蓝莓

你需要检索这两条消息才能获得适当的信息。

其次:这没有考虑到时间。偏好或事实会随着时间而改变。我最喜欢的水果可能会随着时间而改变。如果我们仅基于语义相似性进行检索,则不会考虑到这一点。

第三:这对于所需的记忆类型相对没有倾向性。这对于 AGI 来说可能很好,但对于开发更狭窄、专注且实际有效的应用程序来说就不那么好了。

生成式 Agent

生成式 agent 是一篇出色的论文,不到一年前发表,做了一些最有趣的先进记忆工作。值得看看那篇论文,看看它是如何解决上面列出的一些挑战的。

在《生成式 Agent》中,他们使用多种技术构建记忆

  • 近期性:他们基于最近的消息获取记忆(对话记忆和在获取后仅根据时间戳上调消息权重的组合)。
  • 相关性:他们获取相关消息(语义记忆
  • 反思:他们不仅仅获取原始消息,而是使用 LLM 反思消息,然后获取这些反思。

反思的使用可以帮助解决上面提出的第一个问题 - 如果信息分散在多条消息中,通过反思多条消息,可以生成一个综合。

第二个问题通过近期性权重得到部分解决。然而,可能没有完全解决。

第三个问题并没有真正解决,这仍然是一种非常通用的记忆形式(但这没关系,这就是它的目标)。

长期记忆抽象

当我们想到长期记忆时,最通用的抽象是

  • 存在一些随时间跟踪的状态
  • 此状态在某个周期更新
  • 此状态以某种方式组合到提示中

那么相关的问题就变成了

1. 跟踪的状态是什么?
2. 状态是如何更新的?
3. 状态是如何使用的?

让我们看看上面三种不同类型的记忆是如何发挥作用的。

对话记忆

  • 跟踪的状态是消息列表
  • 状态通过在每次轮次后附加最近的消息来更新
  • 状态通过将消息插入提示中来组合到提示中

语义记忆

  • 跟踪的状态是消息的向量存储
  • 状态通过在每次轮次后向量化并插入消息来更新
  • 状态通过在每次轮次后查询它以获取相似消息来组合到提示中

生成式 Agent

  • 跟踪的状态是记忆的向量存储,以及最近记忆的列表
  • 状态以多种方式更新。首先,在每次轮次后,新的记忆被插入到最近记忆列表中。然后,计算该记忆的嵌入。然后,在 N 轮次后,对最近的记忆进行反思,并将其插入到列表和向量存储中。
  • 状态通过基于近期性和相关性的加权组合选择记忆(或记忆的反思)来组合到提示中。

应用特定记忆

所有这些记忆形式都相当通用。如果你试图构建 AGI,这很棒。但如果你试图构建更狭窄的应用程序,则可靠性和性能较低。

💡
记忆是认知架构的一部分。正如认知架构一样,我们在实践中发现,更应用特定的记忆形式可以在提高应用程序的可靠性和性能方面发挥重要作用。

因此,当你在构建应用程序时,我们强烈建议提出上述问题

  • 跟踪的状态是什么?
  • 状态是如何更新的?
  • 状态是如何使用的?

当然,这说起来容易做起来难。然后,即使你能够回答这些问题,你实际上又该如何构建它呢?

我们决定在 OpenGPTs 中尝试一下,并构建一种具有特定记忆类型的特定聊天机器人。

龙与地下城聊天机器人

我们决定构建一个可以可靠地充当龙与地下城游戏地下城主的聊天机器人。我们为此想要的特定记忆类型是什么?

跟踪的状态是什么?

我们首先要确保跟踪游戏中涉及的角色。他们是谁,他们的描述等等。这似乎是应该知道的事情。我们称之为 character 角色记忆。

然后,我们也想跟踪游戏本身的状态。到目前为止发生了什么,他们在哪里等等。我们称之为 quest 任务记忆。

我们决定将其分为两个不同的事物 - 因此我们实际上跟踪和更新了两个不同的状态:character 角色和 quest 任务。

状态是如何更新的?

对于角色描述,我们只想在开始时更新一次。因此,我们希望我们的聊天机器人收集所有相关信息,更新该状态,然后永远不再更新它。

之后,我们希望我们的聊天机器人尝试在每回合更新任务状态。如果它认为不需要更新,那么我们就不会更新它。否则,我们将用 LLM 生成的新状态覆盖当前的任务状态。

状态是如何使用的?

我们希望角色描述和任务状态始终插入到提示中。这非常简单,因为它们都是文本,所以只是一些提示工程,其中包含这些变量的一些占位符。

认知架构

值得注意的是,对于这个聊天机器人,我们使用了与通用 Agent 略有不同的认知架构。也就是说,我们使用了状态机的一个版本。聊天机器人处于两种状态之一

  1. 角色构建状态:收集有关用户角色的信息
  2. 任务模式状态:领导任务

当 LLM 确定它已经收集了足够多的玩家角色信息时,这两种状态之间就会发生转换。当这种情况发生时,它会更新 character 角色记忆。character 角色记忆的存在充当了聊天机器人应该处于任务讲述状态的标志。

观看实际效果

要观看实际效果,你可以访问已部署版本的 OpenGPTs。你可以在这里查看源代码。

如果你想从头开始创建具有这种记忆类型的“龙与地下城”聊天机器人,在创建新机器人时,你可以选择 dungeons_and_dragons 类型。

在向它发送消息时,它会首先采访你以获取有关你的角色的信息。

一旦它获得了足够的信息,它就会将其保存到 CharacterNotebook(我们对包含角色信息的记忆的命名)。

之后,它将引导你完成一项任务。在不同的时间点,StateNotebook 将会更新(这是我们对包含任务状态的记忆的命名)。

一切都记录在 LangSmith 中,因此我们可以查看幕后发生的事情

请注意,这并不完美!绝对需要进行一些提示工程来改进任务随时间的更新。尽管如此,我们希望这可以作为一个具体的例子,说明我们如何看待长期记忆,以及一个具体的自定义实现。

结论

长期记忆是一个非常未被充分探索的主题。部分原因可能是因为它要么 (1) 非常通用,并且趋向于 AGI,要么 (2) 非常特定于应用程序,并且难以通用地讨论。

在 LangChain,我们认为大多数需要长期记忆形式的应用程序可能更适合应用特定的记忆。在这种情况下,批判性地思考以下内容变得很重要

  • 跟踪的状态是什么?
  • 状态是如何更新的?
  • 状态是如何使用的?

我们正在构建一个框架(和工具)来帮助简化这一点 - LangChain、OpenGPTs、LangSmith。尽管如此,由于它的应用特定性质,因此很难在抽象层面进行工作。如果你是一家正在开发需要长期记忆的应用程序的公司,并且需要帮助,请联系 hello@langchain.dev