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

短期记忆

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

管理短期记忆

对话历史记录是短期记忆最常见的形式,而长对话对当今的 LLM 构成了挑战。完整的历史记录可能无法放入 LLM 的上下文窗口中,导致不可恢复的错误。即使你的 LLM 支持完整的上下文长度,大多数 LLM 在长上下文中仍然表现不佳。它们会被陈旧或离题的内容”分心”,同时还会遭受响应时间变慢和成本增加的困扰。 聊天模型使用消息接收上下文,其中包括开发者提供的指令(系统消息)和用户输入(人类消息)。在聊天应用程序中,消息在人类输入和模型响应之间交替,导致消息列表随着时间的推移而变长。由于上下文窗口是有限的,且包含大量 token 的消息列表成本高昂,许多应用程序可以从使用手动删除 or 遗忘陈旧信息的技术中受益。 Filter 有关管理消息的常见技术的更多信息,请参阅添加和管理记忆指南。

长期记忆

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

语义记忆

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

档案

记忆可以是关于用户、组织或其他实体(包括智能体本身)的单一、不断更新的”档案”,包含范围明确且特定的信息。档案通常只是一个 JSON 文档,其中包含你选择用来表示你的领域的各种键值对。 在记住档案时,你要确保每次都更新档案。因此,你会希望传入先前的档案并要求模型生成新档案(或一些应用于旧档案的 JSON 补丁)。随着档案变大,这可能会变得容易出错,并且可能受益于将档案拆分为多个文档,或在生成文档时进行严格解码以确保记忆模式保持有效。 Update profile

集合

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

情景记忆

情景记忆,无论是在人类还是 AI 智能体中,都涉及回忆过去的事件或行为。CoALA 论文很好地构建了这一点:事实可以写入语义记忆,而经历可以写入情景记忆。对于 AI 智能体,情景记忆通常用于帮助智能体记住如何完成任务。 在实践中,情景记忆通常通过少样本示例提示来实现,智能体从过去的序列中学习以正确执行任务。有时”展示”比”讲述”更容易,而且 LLM 善于从示例中学习。少样本学习让你通过更新带有输入-输出示例的提示来说明预期的行为,从而”编程”你的 LLM。虽然可以使用各种最佳实践来生成少样本示例,但挑战通常在于根据用户输入选择最相关的示例。 请注意,记忆存储只是将数据存储为少样本示例的一种方式。如果你希望有更多的开发者参与,或者将少样本更紧密地绑定到你的评估工具,你还可以使用 LangSmith 数据集来存储你的数据,并实现你自己的检索逻辑以根据用户输入选择最相关的示例。 请参阅这篇博客文章,展示了少样本提示以提高工具调用性能,以及这篇博客文章,使用少样本示例将 LLM 与人类偏好对齐。

程序记忆

程序记忆,无论是在人类还是 AI 智能体中,都涉及记住用于执行任务的规则。在人类中,程序记忆就像是如何执行任务的内在知识,例如通过基本的运动技能和平衡来骑自行车。另一方面,情景记忆涉及回忆特定的经历,例如你第一次成功地骑自行车没有辅助轮,或者通过风景优美的路线进行难忘的骑行。对于 AI 智能体,程序记忆是模型权重、智能体代码和智能体提示词的组合,它们共同决定了智能体的功能。 在实践中,智能体修改其模型权重或重写其代码是相当罕见的。然而,智能体修改自己的提示词更为常见。 改进智能体指令的一种有效方法是通过”反思”或元提示。这涉及用当前的指令(例如,系统提示词)以及最近的对话或明确的用户反馈来提示智能体。然后智能体根据此输入改进自己的指令。这种方法对于指令难以预先指定的任务特别有用,因为它允许智能体从交互中学习和适应。 例如,我们建立了一个推文生成器,使用外部反馈和提示词重写来为 Twitter 生成高质量的论文摘要。在这种情况下,具体的摘要提示词很难先验地指定,但用户批评生成的推文并提供有关如何改进摘要过程的反馈相当容易。 下面的伪代码展示了你如何使用 LangGraph 记忆存储来实现这一点,使用存储来保存提示词,使用 update_instructions 节点获取当前提示词(以及在 state["messages"] 中捕获的与用户对话的反馈),更新提示词,并将新提示词保存回存储。然后,call_model 从存储中获取更新后的提示词并使用它来生成响应。
// Node that *uses* the instructions
const callModel = async (state: State, store: BaseStore) => {
    const namespace = ["agent_instructions"];
    const instructions = await store.get(namespace, "agent_a");
    // Application logic
    const prompt = promptTemplate.format({
        instructions: instructions[0].value.instructions
    });
    // ...
};

// Node that updates instructions
const updateInstructions = async (state: State, store: BaseStore) => {
    const namespace = ["instructions"];
    const currentInstructions = await store.search(namespace);
    // Memory logic
    const prompt = promptTemplate.format({
        instructions: currentInstructions[0].value.instructions,
        conversation: state.messages
    });
    const output = await llm.invoke(prompt);
    const newInstructions = output.new_instructions;
    await store.put(["agent_instructions"], "agent_a", {
        instructions: newInstructions
    });
    // ...
};
Update instructions

写入记忆

智能体写入记忆主要有两种方法:“在热路径中””在后台” Hot path vs background

在热路径中

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

在后台

将创建记忆作为单独的后台任务有几个优点。它消除了主应用程序中的延迟,将应用程序逻辑与记忆管理分离,并允许智能体更专注于完成任务。这种方法还提供了在记忆创建时间上的灵活性,以避免重复工作。 然而,这种方法也有其自身的挑战。确定记忆写入的频率变得至关重要,因为不频繁的更新可能会使其他线程没有新的上下文。决定何时触发记忆形成也很重要。常见的策略包括在设定的时间段后调度(如果有新事件发生则重新调度),使用 cron 调度,或允许用户或应用程序逻辑手动触发。 请参阅我们的 memory-service 模板作为参考实现。

记忆存储

LangGraph 将长期记忆作为 JSON 文档存储在存储中。每个记忆都在自定义 namespace(类似于文件夹)和不同的 key(像文件名)下组织。命名空间通常包括用户或组织 ID 或其他标签,以便于组织信息。这种结构实现了记忆的分层组织。然后通过内容过滤器支持跨命名空间搜索。
import { InMemoryStore } from "@langchain/langgraph";

const embed = (texts: string[]): number[][] => {
    // Replace with an actual embedding function or LangChain embeddings object
    return texts.map(() => [1.0, 2.0]);
};

// InMemoryStore saves data to an in-memory dictionary. Use a DB-backed store in production use.
const store = new InMemoryStore({ index: { embed, dims: 2 } });
const userId = "my-user";
const applicationContext = "chitchat";
const namespace = [userId, applicationContext];

await store.put(
    namespace,
    "a-memory",
    {
        rules: [
            "User likes short, direct language",
            "User only speaks English & TypeScript",
        ],
        "my-key": "my-value",
    }
);

// get the "memory" by ID
const item = await store.get(namespace, "a-memory");

// search for "memories" within this namespace, filtering on content equivalence, sorted by vector similarity
const items = await store.search(
    namespace,
    {
        filter: { "my-key": "my-value" },
        query: "language preferences"
    }
);
有关记忆存储的更多信息,请参阅持久化指南。

了解更多