Skip to main content
Memory 是一个系统,用于记住之前的交互信息。对于 AI 代理来说,内存至关重要,因为它可以让它们记住之前的交互、从反馈中学习并适应用户偏好。随着代理处理越来越复杂的任务和多次用户交互,这种能力对于提高效率和用户满意度变得尤为重要。 本概念指南涵盖了两种类型的内存,基于其回忆范围:
  • 短时记忆,或线程范围内的内存。它通过在会话中维护消息历史来跟踪正在进行的对话。LangGraph 将短时记忆作为代理状态的一部分进行管理,该状态使用检查点持久化到数据库中,以便可以在任何时候恢复线程。当图被调用或步骤完成时,状态会更新,并在每个步骤开始时读取状态。
  • 长时记忆 存储用户特定的数据或应用程序级别的数据,并跨会话共享。它可以在任何时间、任何线程中回忆。记忆可以针对任何自定义命名空间,而不仅仅是单个线程 ID。LangGraph 提供存储器(参考文档:langgraph.store.base.BaseStore)来让您保存和回忆长时记忆。
短时与长时

短时记忆

短时记忆 让您的应用程序在单个线程或对话中记住之前的交互。一个线程将多个会话中的交互组织在一起,类似于电子邮件将消息分组在一个对话中。 LangGraph 将短时记忆作为代理状态的一部分进行管理,并通过线程范围的检查点持久化。此状态通常包括对话历史记录以及其他状态数据(例如上传的文件、检索到的文档或生成的产物)。通过在图的状态中存储这些内容,聊天机器人可以访问给定对话的完整上下文,同时保持不同线程之间的分离。

管理短时记忆

对话历史记录是最常见的短时记忆形式,并且长时间的对话对当今的语言模型提出了挑战。完整的对话历史可能无法容纳在一个语言模型的上下文中,导致不可恢复的错误。即使您的语言模型支持完整的上下文长度,大多数语言模型在长上下文中表现仍然不佳。它们会被过时或不相关的内容“分心”,同时响应时间变慢且成本更高。 聊天模型接受上下文使用消息,这些消息包括开发人员提供的说明(系统消息)和用户输入(人类消息)。在聊天应用程序中,消息交替出现,结果随着时间的推移,消息列表变得越来越长。由于上下文窗口有限,且令牌丰富的消息列表可能代价高昂,许多应用程序可以从手动删除或忘记过时的信息的技术中受益。 过滤 有关管理消息的常见技术,请参阅添加和管理记忆指南。

长时记忆

LangGraph 中的长时记忆允许系统在不同的对话或会话之间保留信息。与短时记忆不同,后者是线程范围内的,长时记忆保存在自定义“命名空间”内。 长时记忆是一个复杂的挑战,并没有一种通用的解决方案。然而,以下问题提供了一个框架,帮助您导航不同的技术:
  • 什么是类型的记忆?人类使用记忆来记住事实(语义记忆)、经历(情景记忆)和规则(程序性记忆)。AI 代理可以以相同的方式使用记忆。例如,AI 代理可以使用记忆来记住特定的用户信息以完成任务。
  • 您何时希望更新记忆? 记忆可以在代理的应用逻辑中进行更新(例如,“在热路径上”)。在这种情况下,代理通常会在响应用户之前决定记住事实。或者,记忆可以在后台任务中更新(即,在后台/异步运行并生成记忆的逻辑)。我们将在下面的部分解释这两种方法之间的权衡。
不同的应用程序需要不同类型的记忆。虽然类比并不完美,但研究人类记忆类型(例如CoALA 论文)可以提供一些见解。某些研究甚至将这些人类记忆类型映射到 AI 代理中使用的方法。
记忆类型存储的内容人类示例代理示例
语义事实我在学校学到的东西用户的特定事实
情景经历我做过的事情过去代理的操作
程序性指令直觉或运动技能系统提示

语义记忆

语义记忆,无论是人类还是 AI 代理,都涉及保留特定的事实和概念。在人类中,它可以包括在学校学到的信息以及对概念及其关系的理解。对于 AI 代理来说,语义记忆通常用于通过记住过去交互中的事实或概念来个性化应用程序。
语义记忆与“语义搜索”不同,“语义搜索”是一种使用“意义”(通常是嵌入)找到相似内容的技术。语义记忆是心理学术语,指的是存储事实和知识,而语义搜索是一种根据意义而不是精确匹配检索信息的方法。
语义记忆可以以不同的方式管理:

资料档案

记忆可以是一个单个的、不断更新的“资料档案”,包含关于用户、组织或其他实体(包括代理本身)的具体且范围明确的信息。一个资料档案通常只是一个 JSON 文档,其中包含您选择的各种键值对来表示您的领域。 在记住资料档案时,您需要确保每次都在更新它。因此,您需要传递之前的资料档案并请求模型生成新的资料档案(或应用到旧资料档案的某些JSON 贴片)。随着资料档案变得越来越大,这可能会变得容易出错,并且可能需要将一个资料档案拆分成多个文档或将生成文档时进行严格解码以确保记忆模式保持有效。 更新资料档案

集合

或者,记忆可以是一个不断更新和扩展的文档集合。每个单独的记忆可以更狭隘地范围,并且更容易生成,这意味着您不太可能随着时间的推移而丢失信息。对于语言模型来说,生成新对象来处理新信息比与现有资料档案进行协调要容易得多。因此,文档集合往往会提高下游召回率 然而,这将一些复杂性转移到了记忆更新上。现在模型必须在列表中删除更新现有项,这可能会很棘手。此外,某些模型可能默认过度插入,而其他模型则默认过度更新。参见Trustcall 包以了解管理此问题的一种方法,并考虑使用评估工具(例如 LangSmith)来帮助您调整行为。 处理文档集合也使复杂性转移到了对列表的记忆搜索上。Store 目前支持语义搜索和按内容过滤。 最后,使用记忆集合可能会使为模型提供全面的上下文变得具有挑战性。虽然单个记忆可能遵循特定模式,但这种结构可能无法捕获所有上下文或记忆之间的关系。因此,在使用这些记忆生成响应时,模型可能会缺乏重要的上下文信息,而这些信息在统一资料档案方法中会更容易获得。 更新列表 无论采用哪种记忆管理方法,中心思想是代理将使用语义记忆来为响应提供依据,这通常会导致更个性化和相关的交互。

情景记忆

情景记忆 无论是人类还是 AI 代理,都涉及回忆过去的事件或行为。CoALA 论文很好地阐述了这一点:事实可以写入语义记忆中,而经历则可以写入情景记忆中。对于 AI 代理来说,情景记忆通常用于帮助代理记住如何完成任务。 在实践中,情景记忆通常通过少样本示例提示实现,其中代理从过去的序列学习以正确执行任务。有时“展示”比“讲述”更容易,语言模型可以从示例中学得很好。少样本学习允许您通过更新提示并提供输入-输出示例来“编程”您的语言模型以说明预期的行为。虽然可以使用各种最佳实践生成少样本示例,但挑战通常在于根据用户输入选择最相关的示例。 请注意,记忆存储器只是将数据作为少样本示例存储的一种方式。如果您希望有更多开发人员参与或更紧密地将少样本示例与评估框架结合,则还可以使用 LangSmith 数据集 来存储您的数据并实现自己的检索逻辑以根据用户输入选择最相关的示例。 请参阅此博客文章,展示少样本提示如何提高工具调用性能,以及此博客文章使用少样本示例来使语言模型与人类偏好保持一致。

程序性记忆

程序性记忆 无论是人类还是 AI 代理,都涉及记住执行任务的规则。在人类中,程序性记忆是通过基本运动技能和平衡内化如何执行任务的知识,例如骑自行车。另一方面,情景记忆涉及回忆特定的经历,如第一次成功骑车而没有训练轮或一次难忘的风景路线骑行经历。对于 AI 代理来说,程序性记忆是由模型权重、代理代码以及代理提示组成的集合,共同决定了代理的功能。 在实践中,很少有代理会修改其模型权重或重写其代码。然而,代理更常见的是修改自己的提示。 通过“反思”或元提示来改进代理指令的一种有效方法是反思代理。这涉及用当前指令(例如系统提示)以及最近的对话或显式用户反馈来提示代理。然后,代理根据这些输入调整其自己的指令。这种方法特别适用于那些在一开始就难以明确指定说明的任务,因为它允许代理从其交互中学习和适应。 例如,我们使用外部反馈和提示重写构建了一个推文生成器,以生成高质量的推文摘要。在这种情况下,特定的总结提示在事先很难明确指定,但用户很容易批评生成的推文并提供关于如何改进总结过程的反馈。 以下伪代码展示了您可能使用 LangGraph 记忆存储器来实现这一点的方式:使用存储器保存提示,“更新指令”节点获取当前提示(以及在 state["messages"] 中捕获的对话与用户的交互),更新提示,然后将新提示保存回存储器。然后,“调用模型”节点从存储器中获取更新后的提示并使用它生成响应。
# 使用指令的节点
def call_model(state: State, store: BaseStore):
    namespace = ("agent_instructions", )
    instructions = store.get(namespace, key="agent_a")[0]
    # 应用逻辑
    prompt = prompt_template.format(instructions=instructions.value["instructions"])
    ...

# 更新指令的节点
def update_instructions(state: State, store: BaseStore):
    namespace = ("instructions",)
    instructions = store.search(namespace)[0]
    # 内存逻辑
    prompt = prompt_template.format(instructions=instructions.value["instructions"], conversation=state["messages"])
    output = llm.invoke(prompt)
    new_instructions = output['new_instructions']
    store.put(("agent_instructions",), "agent_a", {"instructions": new_instructions})
    ...
更新指令

写入记忆

代理写入记忆有两种主要方法:“在热路径上”和“在后台”。 热路径与后台

在热路径上

在运行时创建记忆既有优势也有挑战。积极的一面是,这种方法允许实时更新,使新记忆立即可用于后续交互。它还提供了透明性,因为用户可以被通知何时创建和存储了记忆。 然而,这也带来了挑战。如果代理需要一个新工具来决定要记住什么,这可能会增加复杂性。此外,关于保存到记忆中的内容进行推理可能会影响代理的延迟。最后,代理必须在创建记忆和其他职责之间多任务处理,这可能影响创建的记忆的数量和质量。 例如,ChatGPT 使用 save_memories 工具将记忆作为内容字符串插入,并根据每个用户消息决定是否以及如何使用此工具。请参阅我们的记忆代理模板以获取参考实现。

在后台

在单独的后台任务中创建记忆具有多个优势。它消除了主要应用程序中的延迟,将应用逻辑与内存管理分离,并允许代理更专注于完成任务。这种方法还提供了灵活性,在创建记忆时避免冗余工作。 然而,这也带来了自己的挑战。确定写入记忆的频率至关重要,因为不频繁更新可能会使其他线程缺乏新的上下文。决定何时触发记忆形成也很重要。常见策略包括在固定时间间隔后调度(如果发生新事件则重新安排),使用 cron 调度或允许用户或应用逻辑手动触发。 请参阅我们的记忆服务模板以获取参考实现。

记忆存储

LangGraph 将长时记忆作为 JSON 文档存储在存储器中。每个记忆都组织在一个自定义的 namespace(类似于文件夹)和一个独特的 key(如文件名)。命名空间通常包括用户或组织 ID 或其他标签,使信息更容易组织。这种结构支持跨命名空间搜索。然后通过内容过滤器支持跨命名空间搜索。
from langgraph.store.memory import InMemoryStore


def embed(texts: list[str]) -> list[list[float]]:
    # 用实际的嵌入函数或 LangChain 嵌入对象替换
    return [[1.0, 2.0] * len(texts)]


# InMemoryStore 将数据保存到内存字典中。在生产使用中,请使用数据库支持的存储器。
store = InMemoryStore(index={"embed": embed, "dims": 2})
user_id = "my-user"
application_context = "chitchat"
namespace = (user_id, application_context)
store.put(
    namespace,
    "a-memory",
    {
        "rules": [
            "User likes short, direct language",
            "User only speaks English & python",
        ],
        "my-key": "my-value",
    },
)
# 通过 ID 获取“记忆”
item = store.get(namespace, "a-memory")
# 在此命名空间内搜索“记忆”,根据内容等价性进行过滤,按向量相似度排序
items = store.search(
    namespace, filter={"my-key": "my-value"}, query="language preferences"
)
有关存储器的更多信息,请参阅持久化指南。

更多信息