以编程方式使用文档

内存 是一个记住先前交互信息的系统。对于 AI 代理来说,内存至关重要,因为它使它们能够记住之前的交互、从反馈中学习并适应用户偏好。随着代理处理越来越复杂的任务并产生大量用户交互,这种能力对于效率和用户满意度都变得必不可少。

本概念指南涵盖两种类型的内存,基于它们的召回范围:

  • * 短期记忆, or 线程作用域的内存,通过在会话内维护消息历史来跟踪正在进行的对话。LangGraph 将短期记忆作为代理的 状态的一部分进行管理。状态使用 检查点 持久化到数据库,以便可以随时恢复线程。短期记忆在调用图或完成步骤时更新,并在每个步骤开始时读取状态。
  • * 长期记忆 跨会话存储用户特定或应用级数据,并 _跨_ 对话线程共享。它可以 _随时_ 被召回 _在任何线程中_。记忆被作用域到任何自定义命名空间,而不仅仅是在单个线程 ID 内。LangGraph 提供 存储 (参考文档)让您保存和召回长期记忆。

!短期与长期

短期记忆

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

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

管理短期记忆

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

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

!过滤

有关管理消息的常见技术的更多信息,请参阅 添加和管理内存 guide.

长期记忆

长期记忆 在 LangGraph 中,允许系统在不同的对话或会话中保留信息。与短期记忆不同的是 **thread-scoped**,长期记忆保存在自定义的「命名空间」中。

长期记忆是一个复杂的挑战,没有放之四海而皆准的解决方案。但是,以下问题提供了一个框架来帮助你选择不同的技术:

  • * 记忆的类型是什么?人类使用记忆来记住事实(语义记忆)、经验(情景记忆),和规则(程序性记忆)。人工智能代理可以以相同的方式使用记忆。例如,人工智能代理可以使用记忆来记住关于用户的特定事实以完成任务。
  • * 你希望在什么时候更新记忆? Memory can be updated as part of an agent's application logic (e.g., "on the hot path"). In this case, the agent typically decides to remember facts before responding to a user. Alternatively, memory can be updated as a background task (logic that runs in the background / asynchronously and generates memories). We explain the tradeoffs between these approaches in the 下方的章节.

不同的应用程序需要不同类型的记忆。虽然类比并不完美,但研究 人类记忆类型 可以很有启发性。一些研究(例如 CoALA 论文)甚至将这些人类记忆类型映射到人工智能代理使用的记忆类型。

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

语义记忆

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

语义记忆可以以不同的方式管理:

配置文件

记忆可以是一个单一的、持续更新的「配置文件」,其中包含关于用户、组织或其他实体(包括代理本身)的范围明确且具体的信息。配置文件通常只是一个 JSON 文档,包含你选择用于表示领域的各种键值对。

记住配置文件时,你要确保你是在 **更新** 配置文件每次。这样一来,你需要传入之前的配置文件,然后 让模型生成一个新的配置文件 (或一些 JSON 补丁 来应用到旧的配置文件)。随着配置文件变大,这可能会变得容易出错,可能需要将配置文件拆分成多个文档或 **严格的** 在生成文档时进行解码,以确保记忆模式保持有效。

!更新资料

集合

或者,记忆可以是一个不断更新和扩展的文档集合。每个独立的记忆可以范围更窄、更容易生成,这意味着你不太可能随着时间 **丢失** 信息。LLM 生成 _新_ 对象来处理新信息,比将新信息与现有资料进行协调要容易得多。因此,文档集合往往会导致 更高的下游召回率.

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

使用文档集合也会将复杂性转移到记忆 **搜索** 和对列表的 Store 目前支持 语义搜索按内容筛选.

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

!更新列表

无论采用何种记忆管理方法,关键是代理将使用语义记忆来 为响应奠定基础,这通常会带来更加个性化和相关的交互。

情景记忆

情景记忆,无论是人类还是AI代理,都涉及回忆过去的事件或行动。 CoALA 论文 很好地阐述了这一观点:事实可以被写入语义记忆,而 *经验* 可以被写入情景记忆。对于AI代理来说,情景记忆通常用于帮助代理记住如何完成一项任务。

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

请注意,记忆 存储 只是将数据存储为少样本示例的一种方式。如果你想让开发者更多地参与,或将少样本更紧密地与你的评估框架结合,你也可以使用 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",)
    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 使用一个 保存_记忆 工具来 Upsert 记忆作为内容字符串,在每条用户消息时决定是否以及如何使用此工具。请参阅我们的 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"
)

有关记忆存储的更多信息,请参阅 持久化 guide.

了解更多