Skip to main content
内存 是一个记录先前交互信息的系统。对于 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 数据集 存储数据,并实现自己的检索逻辑以根据用户输入选择最相关的示例。 请参阅这篇博客文章,展示少样本提示如何改进工具调用性能,以及这篇博客文章,使用少样本示例使 LLM 与人类偏好保持一致。

程序记忆

程序记忆,无论是在人类还是 AI 代理中,都涉及记住用于执行任务的规则。在人类中,程序记忆类似于执行任务的内化知识,例如通过基本运动技能和平衡骑自行车。另一方面,情景记忆涉及回忆特定经历,例如第一次成功骑自行车而无需训练轮,或一次难忘的风景路线骑行。对于 AI 代理,程序记忆是模型权重、代理代码和代理提示的组合,共同决定了代理的功能。 在实践中,代理修改其模型权重或重写其代码的情况相当少见。然而,代理修改自己的提示更为常见。 一种有效的方法是通过”反思” 或元提示来完善代理的指令。这涉及用其当前指令(例如系统提示)以及最近的对话或明确的用户反馈来提示代理。然后,代理根据此输入完善其自己的指令。这种方法对于指令难以预先指定的任务特别有用,因为它允许代理从交互中学习和适应。 例如,我们构建了一个Tweet 生成器,使用外部反馈和提示重写来为 Twitter 生成高质量的论文摘要。在这种情况下,特定的摘要提示很难先验指定,但用户很容易对生成的 Tweet 进行批评并提供有关如何改进摘要过程的反馈。 以下伪代码显示了如何使用 LangGraph 内存存储 实现此功能,使用存储保存提示,update_instructions 节点获取当前提示(以及从 state["messages"] 中捕获的与用户对话的反馈),更新提示,并将新提示保存回存储。然后,call_model 从存储中获取更新后的提示并使用它生成响应。
# 使用指令的节点
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 工具来更新内存内容字符串,决定是否以及如何在每个用户消息中使用此工具。请参阅我们的 memory-agent 模板作为参考实现。

在后台

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

内存存储

LangGraph 将长期记忆作为 JSON 文档存储在存储中。每个记忆都组织在自定义命名空间(类似于文件夹)和不同的(如文件名)下。命名空间通常包括用户或组织 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"
)
有关内存存储的更多信息,请参阅持久化指南。