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

短期记忆

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

管理短期记忆

对话历史是最常见的短期记忆形式,而长对话对今天的LLM来说是一个挑战。完整的历史可能无法适应LLM的上下文窗口,从而导致不可恢复的错误。即使你的LLM支持完整的上下文长度,大多数LLM在长上下文中仍然表现不佳。它们会被过时或离题的内容“分散注意力”,同时遭受响应时间变慢和成本增加的问题。 聊天模型通过消息接受上下文,这些消息包括开发者提供的指令(系统消息)和用户输入(人类消息)。在聊天应用中,消息在人类输入和模型响应之间交替,从而形成一个随着时间的推移而变长的消息列表。由于上下文窗口有限,且富标记的消息列表可能成本高昂,许多应用可以从使用手动删除或忘记过时信息的技巧中受益。 有关管理消息的常见技术信息,请参阅添加和管理记忆指南。

长期记忆

长期记忆在LangGraph中允许系统在不同对话或会话中保留信息。与线程作用域的短期记忆不同,长期记忆被保存在自定义的“命名空间”中。 长期记忆是一个复杂的挑战,没有一劳永逸的解决方案。然而,以下问题提供了一个框架,帮助您了解不同的技术:
  • 什么是记忆的类型?人类使用记忆来记住事实(语义记忆)、经验(情景记忆)和规则(程序记忆)。AI智能体也可以以相同的方式使用记忆。例如,AI智能体可以使用记忆来记住关于用户的特定事实,以完成一项任务。
  • 你希望在何时更新记忆?记忆可以作为智能体应用逻辑的一部分进行更新(例如,“在热点路径上”)。在这种情况下,智能体通常会在回应用户之前决定记住事实。或者,记忆也可以作为后台任务进行更新(在后台/异步运行的逻辑并生成记忆)。我们将在下文部分中解释这两种方法之间的权衡。

内存类型

不同的应用程序需要各种类型的内存。虽然这种类比并不完美,但研究人类记忆类型可能会有所启发。一些研究(例如,CoALA论文)甚至将这些人类记忆类型映射到AI智能体中使用的那些。
内存类型存储内容人类例子智能体例子
语义事实我在学校学到的知识关于用户的资料
情景经验我做过的事情过去的智能体行为
程序指令本能或运动技能智能体系统提示

语义记忆

语义记忆,在人类和AI智能体中,都涉及对特定事实和概念的记忆。在人类中,这可能包括在学校学到的信息和理解概念及其关系。对于AI智能体来说,语义记忆通常用于通过记住过去交互中的事实或概念来个性化应用程序。
语义记忆与“语义搜索”不同,“语义搜索”是一种利用“意义”(通常作为嵌入)来查找相似内容的技术。语义记忆是心理学中的一个术语,指的是存储事实和知识,而语义搜索是一种基于意义而非精确匹配来检索信息的方法。
个人资料
语义记忆可以以不同的方式管理。例如,记忆可以是一个单一、持续更新的“配置文件”,其中包含关于用户、组织或其他实体(包括智能体本身)的广泛且具体的信息。配置文件通常只是一个包含您选择的代表您领域的各种键值对的JSON文档。 在记住一个配置文件时,您需要确保每次都更新该配置文件。因此,您需要传入上一个配置文件,并要求模型生成一个新的配置文件(或应用于旧配置文件的某些[JSON补丁](https://github.com/hinthornw/trustcall))。随着配置文件变大,这可能会变得容易出错,并且将配置文件拆分为多个文档或生成文档时进行**严格**解码可能有助于确保内存模式保持有效。
收集
或者,记忆可以是一组随着时间的推移不断更新和扩展的文档。每个单独的记忆可以更加具体且更容易生成,这意味着随着时间的推移,你丢失信息的可能性更小。对于大型语言模型(LLM)来说,生成新的对象以适应新信息比将新信息与现有档案相协调要容易得多。因此,文档集合往往会导致下游更高的召回率 然而,这导致了一些复杂性,即记忆更新。现在,模型必须现在 删除更新 列表中的现有条目,这可能很棘手。此外,一些模型可能默认为过度插入,而其他模型可能默认为过度更新。请参阅 Trustcall 包以了解一种管理方法,并考虑评估(例如,使用 LangSmith 这样的工具)以帮助您调整行为。 与文档集合一起工作也将复杂性转移到列表的 搜索 中。当前的 Store 支持两种方式:语义搜索按内容过滤 最后,使用一系列记忆可能会使向模型提供全面上下文变得具有挑战性。虽然个别记忆可能遵循特定的模式,但这种结构可能无法捕捉到记忆的全部上下文或它们之间的关系。因此,当使用这些记忆来生成响应时,模型可能会缺少在统一配置文件方法中更容易获得的重要上下文信息。 无论采用何种内存管理方法,核心点在于智能体将使用语义记忆来定位其响应,这通常会导致更加个性化和相关的交互。

事件记忆

情景记忆在人类和AI智能体中,涉及回忆过去的事件或行为。CoALA论文对此进行了很好的阐述:事实可以写入语义记忆,而经验可以写入情景记忆。对于AI智能体来说,情景记忆通常用于帮助智能体记住如何完成任务。 在实践中,情景记忆通常通过少量示例提示来实现,智能体通过学习过去的序列来正确执行任务。有时“展示”比“讲述”更容易,而大型语言模型(LLM)从示例中学习得很好。少量学习让您可以通过更新输入-输出示例来展示预期行为,从而”编程”您的LLM。虽然可以使用各种最佳实践来生成少量示例,但挑战通常在于根据用户输入选择最相关的示例。 请注意,存储 是存储数据作为少样本示例的一种方式。如果您希望有更多的开发者参与,或者将少样本与您的评估工具更紧密地结合,您也可以使用 LangSmith 数据集 来存储您的数据。然后,可以直接使用动态少样本示例选择器来实现相同的目标。LangSmith 将为您索引数据集,并基于关键词相似度(使用类似 BM25 的算法 进行基于关键词的相似度)检索与用户输入最相关的少样本示例。 查看这个视频了解LangSmith中动态少量示例选择的示例用法。此外,还可以参考这篇博客文章,展示了如何通过少量示例提示来提高工具调用性能,以及这篇博客文章,它使用少量示例来使大型语言模型与人类偏好保持一致。

程序性记忆

程序性记忆,在人类和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使用了一个save_memories工具来更新记忆内容字符串,并决定是否以及如何使用该工具处理每个用户的消息。请参阅我们的记忆智能体模板作为参考实现。

在后台

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

内存存储

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"
)
有关内存存储的更多信息,请参阅持久化指南。