记忆概述
记忆是一个能够记录先前交互信息的系统。对于AI智能体而言,记忆至关重要,它使智能体能够记住过去的交互、从反馈中学习并适应用户偏好。随着智能体处理愈发复杂且交互频繁的任务,这种能力对于提升效率和用户满意度都变得不可或缺。本概念指南根据调用范围介绍两种类型的记忆:
- 短期记忆,或称线程作用域记忆,通过维护会话内的消息历史来跟踪正在进行的对话。LangGraph将短期记忆作为智能体状态的一部分进行管理。该状态通过线程作用域的检查点持久化到数据库中,使得线程可以随时恢复。当调用图或完成某个步骤时,短期记忆会更新,并在每个步骤开始时读取状态。
- 长期记忆存储跨会话的用户特定数据或应用级数据,并在不同的对话线程间共享。它可以随时在任何线程中被调用。记忆的作用域可以限定于任何自定义的命名空间,而不仅仅是单个线程ID。LangGraph提供了存储(参考文档)供您保存和调用长期记忆。
短期记忆
短期记忆使您的应用程序能够记住单个线程或对话中的先前交互。线程组织了一次会话中的多次交互,类似于电子邮件将消息分组到单个会话中的方式。LangGraph将短期记忆作为智能体状态的一部分进行管理,并通过线程作用域的检查点进行持久化。此状态通常可包括对话历史以及其他有状态数据,例如上传的文件、检索到的文档或生成的产物。通过将这些存储在图的状态中,智能体可以访问特定对话的完整上下文,同时保持不同线程之间的隔离。
管理短期记忆
对话历史是最常见的短期记忆形式,而冗长的对话对当今的大型语言模型构成了挑战。完整的对话历史可能无法放入LLM的上下文窗口,从而导致无法恢复的错误。即使您的LLM支持完整的上下文长度,大多数LLM在处理长上下文时性能仍然不佳。它们会被陈旧或离题的内容“干扰”,同时还会导致响应时间变慢和成本增加。聊天模型通过消息来接受上下文,这些消息包括开发人员提供的指令(系统消息)和用户输入(人类消息)。在聊天应用中,消息在人类输入和模型响应之间交替,形成一条随时间增长的消息列表。由于上下文窗口有限且包含大量令牌的消息列表成本高昂,许多应用可以通过使用技术手段手动移除或遗忘陈旧信息而受益。
有关管理消息的常用技术的更多信息,请参阅《添加和管理记忆指南》。
长期记忆
LangGraph中的长期记忆允许系统在不同的对话或会话间保留信息。与限于线程作用域的短期记忆不同,长期记忆保存在自定义的“命名空间”中。长期记忆是一个复杂的挑战,没有通用的解决方案。然而,以下问题提供了一个框架,帮助您理解不同的技术:
- 记忆的类型是什么?人类利用记忆来记住事实(语义记忆)、经历(情景记忆)和规则(程序性记忆)。AI智能体可以以同样的方式使用记忆。例如,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从存储中获取更新后的提示词并用其生成响应。
# 使用指令的节点defcall_model(state:State,store:BaseStore):namespace=("agent_instructions",)instructions=store.get(namespace,key="agent_a")[0]# 应用逻辑prompt=prompt_template.format(instructions=instructions.value["instructions"])...# 更新指令的节点defupdate_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工具来更新或插入作为内容字符串的记忆,并决定是否以及如何随每条用户消息使用此工具。请参阅我们的记忆-智能体模板作为参考实现。
后台写入
作为一个单独的后台任务创建记忆有几个优点。它消除了主应用中的延迟,将应用逻辑与记忆管理分离,并允许智能体更专注地完成任务。这种方法还提供了在时间安排上创建记忆的灵活性,以避免重复工作。然而,这种方法也有其自身的挑战。确定记忆写入的频率变得至关重要,因为不频繁的更新可能会使其他线程缺乏新的上下文。决定何时触发记忆形成也很重要。常见的策略包括:设定时间段后调度(如果发生新事件则重新调度)、使用cron计划调度,或允许用户或应用逻辑手动触发。请参阅我们的记忆-服务模板作为参考实现。
记忆存储
LangGraph将长期记忆作为JSON文档存储在存储区中。每个记忆都组织在一个自定义的命名空间(类似于文件夹)和一个唯一的键(类似于文件名)之下。命名空间通常包含用户ID、组织ID或其他标签,以便于组织信息。这种结构支持记忆的层次化组织。然后,可以通过内容过滤器支持跨命名空间的搜索。
fromlanggraph.store.memoryimportInMemoryStoredefembed(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":["用户喜欢简短直接的语言","用户只说英语和Python",],"my-key":"my-value",},)# 通过ID获取“记忆”item=store.get(namespace,"a-memory")# 在此命名空间内搜索“记忆”,根据内容相等性过滤,并按向量相似度排序items=store.search(namespace,filter={"my-key":"my-value"},query="language preferences")有关记忆存储区的更多信息,请参阅《持久化指南》。