跳到内容

内存

记忆是一种记录过往交互信息的系统。对于AI代理而言,记忆至关重要,因为它使代理能够记住过往交互,从反馈中学习,并适应用户偏好。随着代理处理涉及大量用户交互的更复杂任务,这种能力对于效率和用户满意度都变得至关重要。

本概念指南根据记忆的召回范围,涵盖了两种类型的记忆:

  • 短期记忆,或线程范围的记忆,通过维护会话内的消息历史来跟踪正在进行的对话。LangGraph 将短期记忆作为代理状态的一部分进行管理。状态通过检查点持久化到数据库中,以便线程可以随时恢复。当图被调用或步骤完成时,短期记忆会更新,并在每个步骤开始时读取状态。

  • 长期记忆存储跨会话的用户特定或应用级别数据,并对话线程共享。它可以在任何时候任何线程中被召回。记忆的范围可以限定在任何自定义命名空间,而不仅仅是单个线程ID内。LangGraph 提供了存储参考文档)来让您保存和召回长期记忆。

短期记忆

短期记忆让您的应用程序记住单个线程或对话中的过往交互。一个线程在一个会话中组织多个交互,类似于电子邮件将消息分组到单个对话中的方式。

LangGraph 将短期记忆作为代理状态的一部分进行管理,通过线程范围的检查点进行持久化。此状态通常可以包括对话历史以及其他有状态数据,例如上传的文件、检索到的文档或生成的工件。通过将这些数据存储在图的状态中,机器人可以访问给定对话的完整上下文,同时保持不同线程之间的分离。

管理短期记忆

对话历史是短期记忆最常见的形式,而长对话对当今的LLM构成了挑战。完整的历史记录可能无法完全放入LLM的上下文窗口中,从而导致不可恢复的错误。即使您的LLM支持完整的上下文长度,大多数LLM在长上下文中表现仍然不佳。它们会被陈旧或跑题的内容“分散注意力”,同时响应时间更慢,成本更高。

聊天模型使用消息来接受上下文,这些消息包括开发者提供的指令(系统消息)和用户输入(人类消息)。在聊天应用程序中,消息在人类输入和模型响应之间交替,导致消息列表随着时间增长。由于上下文窗口有限且富含令牌的消息列表可能成本高昂,许多应用程序可以从手动删除或遗忘陈旧信息的技巧中受益。

有关管理消息的常见技巧的更多信息,请参阅添加和管理记忆指南。

长期记忆

长期记忆在 LangGraph 中允许系统跨不同对话或会话保留信息。与线程范围的短期记忆不同,长期记忆保存在自定义的“命名空间”中。

长期记忆是一个复杂挑战,没有一劳永逸的解决方案。然而,以下问题提供了一个框架,帮助您掌握不同的技巧:

  • 记忆的类型是什么?人类使用记忆来记住事实(语义记忆)、经验(情景记忆)和规则(程序性记忆)。AI代理也可以以相同的方式使用记忆。例如,AI代理可以使用记忆来记住用户的具体事实以完成任务。

  • 您希望何时更新记忆?记忆可以作为代理应用程序逻辑的一部分进行更新(例如,“在热路径中”)。在这种情况下,代理通常会在响应用户之前决定记住事实。或者,记忆可以作为后台任务进行更新(在后台/异步运行并生成记忆的逻辑)。我们将在下面的章节中解释这些方法的权衡。

记忆类型

不同的应用程序需要不同类型的记忆。尽管这个类比并不完美,但研究人类记忆类型可以带来启发。一些研究(例如CoALA 论文)甚至将这些人类记忆类型映射到了AI代理中使用的类型。

记忆类型 存储内容 人类示例 代理示例
语义 事实 我在学校学到的东西 关于用户的事实
情景 经验 我做过的事情 过去的代理行为
程序性 指令 本能或运动技能 代理系统提示

语义记忆

语义记忆,在人类和AI代理中,都涉及特定事实和概念的保留。在人类中,它可能包括在学校学到的信息以及对概念及其关系的理解。对于AI代理而言,语义记忆常用于通过记住过去交互中的事实或概念来个性化应用程序。

注意

语义记忆不同于“语义搜索”,后者是一种利用“意义”(通常是嵌入)查找相似内容的技术。语义记忆是心理学中的一个术语,指存储事实和知识,而语义搜索是一种基于意义而非精确匹配来检索信息的方法。

档案

语义记忆可以通过不同的方式进行管理。例如,记忆可以是一个单一的、持续更新的“档案”,其中包含关于用户、组织或其他实体(包括代理本身)的范围明确且具体的信息。档案通常只是一个JSON文档,包含您选择用于表示您领域中的各种键值对。

在记录档案时,您会希望确保每次都更新该档案。因此,您会希望传入先前的档案并要求模型生成新档案(或一些适用于旧档案的JSON补丁)。随着档案变得越来越大,这可能会变得容易出错,并且可能受益于将档案拆分为多个文档或在生成文档时进行严格解码,以确保记忆模式保持有效。

集合

或者,记忆可以是一个文档集合,这些文档会随着时间持续更新和扩展。每个独立的记忆可以更狭窄地限定范围,更容易生成,这意味着您随时间丢失信息的可能性更小。对于LLM来说,为新信息生成对象比将新信息与现有档案进行协调更容易。因此,文档集合倾向于导致下游召回率更高

然而,这会将一些复杂性转移到记忆更新上。模型现在必须删除更新列表中的现有项目,这可能很棘手。此外,有些模型可能默认过度插入,而另一些则可能默认过度更新。请参阅 Trustcall 包以了解一种管理此问题的方法,并考虑使用评估工具(例如 LangSmith)来帮助您调整行为。

处理文档集合也会将复杂性转移到对列表的记忆搜索上。Store 目前支持语义搜索按内容过滤

最后,使用记忆集合可能会使得向模型提供全面上下文变得具有挑战性。虽然单个记忆可能遵循特定的模式,但这种结构可能无法捕获完整的上下文或记忆之间的关系。因此,在使用这些记忆生成响应时,模型可能缺乏重要的上下文信息,而这些信息在统一档案方法中会更容易获得。

无论采用何种记忆管理方法,核心点是代理将使用语义记忆来支撑其响应,这通常会导致更个性化和相关的交互。

情景记忆

情景记忆,在人类和AI代理中,都涉及回忆过去的事件或行为。CoALA 论文对此有很好的阐述:事实可以写入语义记忆,而经验可以写入情景记忆。对于AI代理而言,情景记忆常用于帮助代理记住如何完成任务。

在实践中,情景记忆通常通过少样本示例提示来实现,代理从中学习过去的序列以正确执行任务。有时“展示”比“告诉”更容易,而且LLM从示例中学习效果很好。少样本学习允许您通过用输入-输出示例更新提示来“编程”您的LLM,以说明预期行为。虽然可以使用各种最佳实践来生成少样本示例,但挑战往往在于根据用户输入选择最相关的示例。

请注意,记忆存储只是将数据存储为少样本示例的一种方式。如果您希望有更多的开发者参与,或者将少样本与您的评估框架更紧密地结合起来,您还可以使用LangSmith 数据集来存储您的数据。然后,可以直接使用动态少样本示例选择器来实现相同的目标。LangSmith 将为您索引数据集,并根据关键词相似性(使用类似 BM25 的算法进行关键词相似性匹配)来检索与用户输入最相关的少样本示例。

有关 LangSmith 中动态少样本示例选择的示例用法,请参阅此操作视频。另请参阅这篇博客文章,展示了如何使用少样本提示来提高工具调用性能,以及这篇博客文章,介绍了如何使用少样本示例使LLM与人类偏好保持一致。

程序性记忆

程序性记忆,在人类和AI代理中,都涉及记住执行任务的规则。在人类中,程序性记忆类似于内化的任务执行知识,例如通过基本运动技能和平衡来骑自行车。另一方面,情景记忆涉及回忆具体的经验,例如您第一次成功地不带辅助轮骑自行车,或者一次难忘的风景优美的自行车骑行。对于AI代理而言,程序性记忆是模型权重、代理代码和代理提示的组合,它们共同决定了代理的功能。

在实践中,代理修改其模型权重或重写其代码是相当不常见的。然而,代理修改自己的提示则更为常见。

改进代理指令的一种有效方法是通过“反思”或元提示。这包括用代理当前的指令(例如,系统提示)以及最近的对话或明确的用户反馈来提示代理。然后代理根据此输入完善自己的指令。这种方法对于难以预先指定指令的任务特别有用,因为它允许代理从交互中学习和适应。

例如,我们构建了一个推文生成器,利用外部反馈和提示重写来为 Twitter 生成高质量的论文摘要。在这种情况下,特定的摘要提示很难先验地指定,但用户很容易对生成的推文进行评价并提供如何改进摘要过程的反馈。

下面的伪代码展示了如何使用 LangGraph 记忆存储来实现这一点:利用存储来保存提示,update_instructions 节点获取当前提示(以及从用户对话中捕获的 state["messages"] 中的反馈),更新提示,并将新提示保存回存储。然后,call_model 从存储中获取更新后的提示并用它来生成响应。

# Node that *uses* the instructions
def call_model(state: State, store: BaseStore):
    namespace = ("agent_instructions", )
    instructions = store.get(namespace, key="agent_a")[0]
    # Application logic
    prompt = prompt_template.format(instructions=instructions.value["instructions"])
    ...

# Node that updates instructions
def update_instructions(state: State, store: BaseStore):
    namespace = ("instructions",)
    current_instructions = store.search(namespace)[0]
    # Memory logic
    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 工具将记忆作为内容字符串进行插入更新,并决定如何以及何时对每个用户消息使用此工具。请参阅我们的 memory-agent 模板作为参考实现。

在后台

将记忆创建作为独立的后台任务提供了几个优点。它消除了主应用程序中的延迟,将应用程序逻辑与记忆管理分离,并允许代理更专注于任务完成。这种方法还在记忆创建的时机上提供了灵活性,以避免重复工作。

然而,这种方法也有其自身的挑战。确定记忆写入的频率变得至关重要,因为不频繁的更新可能会导致其他线程缺乏新的上下文。决定何时触发记忆形成也同样重要。常见策略包括在设定的时间段后安排(如果发生新事件则重新安排)、使用 cron 计划,或允许用户或应用程序逻辑手动触发。

请参阅我们的 memory-service 模板作为参考实现。

记忆存储

LangGraph 将长期记忆作为 JSON 文档存储在存储中。每个记忆都组织在一个自定义的 namespace(类似于文件夹)和一个独特的 key(类似于文件名)下。命名空间通常包含用户或组织ID或其他标签,以便于组织信息。这种结构实现了记忆的层次化组织。然后通过内容过滤器支持跨命名空间搜索。

from langgraph.store.memory import InMemoryStore


def embed(texts: list[str]) -> list[list[float]]:
    # Replace with an actual embedding function or LangChain embeddings object
    return [[1.0, 2.0] * len(texts)]


# InMemoryStore saves data to an in-memory dictionary. Use a DB-backed store in production use.
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",
    },
)
# get the "memory" by ID
item = store.get(namespace, "a-memory")
# search for "memories" within this namespace, filtering on content equivalence, sorted by vector similarity
items = store.search(
    namespace, filter={"my-key": "my-value"}, query="language preferences"
)

有关记忆存储的更多信息,请参阅持久化指南。