DLAI-Langchain-长期记忆笔记-全-
DLAI Langchain 长期记忆笔记(全)
001:构建具备长期记忆的自主智能体 🧠

概述
在本节课中,我们将学习如何为自主智能体(Agent)添加长期记忆。我们将探讨需要存储哪些信息、何时以及如何检索这些信息,并介绍三种关键的记忆类型。课程将使用 LangGraph 库,通过构建一个实用的电子邮件助手来演示这些概念。
课程内容
欢迎来到与 Langchain 合作构建的“使用 LangGraph 实现长期智能体记忆”课程。
我是 Harrison Chase,Langchain 的联合创始人,很高兴再次与大家见面。谢谢 Andrew。
近来,我们看到许多智能体应用被构建出来。这帮助我们形成了一个在为其添加记忆时非常有用的思维框架。我们希望与学习者分享这个框架。
越来越多的 AI 应用需要跨越时间持续运行。这真正驱动了对智能体记忆的需求。一个例子是 AI 个人助手。
智能体是一个很好的例子,它们学得越多,在未来任务中表现就越好。
要为智能体添加记忆,首先必须弄清楚哪些信息需要存储在长期记忆中。同时,当需要使用这些信息时,还需要知道如何检索。
首先关注存储什么。


聊天机器人最初只是在对话的每一轮中,在上下文记忆中查看对话历史。但那些长期为你服务的智能体需要长期记忆。
例如,一个日历智能体可能需要长期保存跨多个智能体调用周期的会议信息。
接下来是检索。检索将从记忆中获取信息并将其插入到上下文中。Harrison 将向大家展示如何确定何时以及检索什么。
此外,我们还需要决定何时更新这些存储的信息。它应该在智能体循环的每次迭代中更新,还是在后台空闲时更新?
为了解决这些问题,我们发现将记忆分为三种类型来思考是有用的。
以下是三种记忆类型:
- 语义记忆:这些是事实,例如日历智能体中重要的生日。
- 情景记忆:这些是经验,可以帮助智能体记住如何完成任务。
- 程序性记忆:这些是智能体需要遵循的规则。
为了帮助管理记忆,我们创建了一个新的库 LangGraph。它支持向量数据库,提供可搜索、可共享、持久化的存储。这些存储可以由智能体立即更新,也可以由辅助智能体在后台更新。
在本课程中,你将构建一个实用的电子邮件助手,使用 LangGraph 来演示所有这些概念。

多位贡献者共同创作了本课程。我要感谢来自 Langchain 的 Lance Martin、Wil Fu、Hinphonn 和 Nnocompos,以及来自 Dilanta AI 的 Jeff Ludwig。

好的,让我们开始第一课。
总结

本节课我们一起学习了为自主智能体构建长期记忆系统的基础。我们明确了智能体需要跨越时间存储和利用信息,并介绍了决定存储内容、检索时机以及更新策略的思维框架。核心是将记忆分为语义记忆、情景记忆和程序性记忆三类。我们还了解到 LangGraph 库为此提供了强大的工具支持。在接下来的实践中,我们将应用这些概念来构建一个电子邮件助手。
002:智能体记忆简介 🧠

概述
在本节课中,我们将要学习智能体(Agent)记忆的核心概念,特别是长期记忆对于构建高效自主智能体的重要性。我们将以电子邮件助手为例,探讨三种不同类型的记忆及其实现方式。
几个月前,我们对开发者进行了一项调查,询问他们认为当前智能体最适合执行哪些任务。得票第二高的答案是个人助理和生产力任务。这些任务的一个核心方面是记忆,特别是长期记忆。

想象一下与一位担任个人助理的人类助手合作。如果他忘记了与你的所有对话,并且不记得你之前告诉他的事情,那将是一种非常糟糕的体验。对于智能体来说,情况也是如此。因此,记忆是这类任务的关键部分。当我们思考一个例子来介绍记忆时,电子邮件代理就是一个绝佳的选择。
为何选择电子邮件助手? 📧
上一节我们介绍了记忆的重要性,本节中我们来看看为什么电子邮件助手是展示记忆功能的理想场景。
电子邮件是我们每个人都拥有且必须处理的事务。随着你越来越忙,可能会有越来越多的邮件涌现,而你根本没有时间查看或回复。这正是智能体可以大显身手的地方。

如果我们思考一个智能体要出色完成这项工作需要什么,它很可能与一位行政助理所需的能力相似。它可能需要访问日历工具来查看你的空闲时间,可能需要代表你撰写或回复邮件的权限。因此,当我们考虑构建一个智能体时,我们可以考虑赋予它访问这类工具的能力。
记忆在何处发挥作用? 💭
像这样的智能体在许多地方,记忆都变得至关重要。
以下是几个关键环节:
- 第一步是查看所有邮件并决定:我应该忽略这封邮件,还是应该回复它?
- 如果决定回复并使用日历或邮件工具:它需要知道用户的会议偏好,包括时间、地点和标题。
- 如果决定撰写邮件:它需要了解用户的语气和风格。
- 与之前有过互动的人交流:了解之前的互动背景对于恰当回复非常重要。

因此,这个电子邮件助手是展示大型语言模型(LLM)与记忆功能结合应用的绝佳平台。我们将用它作为试验场,来讨论AI智能体的三种不同类型记忆。

三种核心记忆类型 🧩
上一节我们看到了记忆在电子邮件助手工作流中的关键作用,本节中我们来详细了解一下智能体可以拥有的三种核心记忆类型。
第一种是语义记忆。 语义记忆本质上是事实。如果我们思考人类的语义记忆,它包括你在学校学到的知识、从教科书中读到并记住的事实,以及从以往互动中记住的各种基于事实的记忆。

第二种类型是情景记忆。 这是关于经历的记忆,是对你曾有过确切对话的回溯。它不一定是关于这些对话的事实,而是对话本身的记忆,就像去迪士尼乐园的记忆一样。
第三种类型是程序性记忆。 这类似于给自己的指令、本能或运动技能,例如如何骑自行车,或者你给自己设定的关于如何回复邮件的指令。
如何映射到智能体? 🔄

那么,我们如何将这些记忆类型映射到智能体上呢?
- 对于语义记忆:这些可能是关于用户或智能体交互过的人物、地点、事物的事实。
- 对于情景记忆:这可能是过去的智能体行动,以少量示例(few-shot examples)的形式存在,即之前发生过的实际轨迹记录。
- 对于程序性记忆:这可能是最直接的。这就是系统提示词,即你给智能体关于如何行为的指令。
记忆交互的两种范式 ⚙️
除了这三种记忆类型,指出智能体与这些记忆交互的不同方式也很有价值。
我们观察到两种不同的范式:
- 第一种通常在热路径(hot path)中:智能体在响应用户的同时更新记忆,所有操作一气呵成。
- 另一种类型是记忆更新发生在后台或单独的进程中。

这两种方式各有优缺点。
- 在热路径中更新:通常只有一个智能体,因此维护更简单,更新也通常是即时发生的。然而,缺点是现在这个智能体必须同时做两件事:更新记忆和响应用户,这使得它更复杂,并且在响应用户时增加了额外的延迟。
- 后台更新则相反:现在你可以将记忆更新与智能体的核心任务分离开来,因此更简单。并且由于从热路径中移除了更新步骤,响应速度实际上更快。问题在于,现在你有了两个智能体而不是一个,这更复杂。并且更新可能不是即时发生的,它们可能只在触发更新记忆的进程启动时才发生。
本课程实践路线图 🗺️
在本课程中,我们将把这些记忆类型应用到一个电子邮件智能体上。
我们将从一个基本的电子邮件智能体开始,它首先对收到的邮件进行分类,如果值得回复,则将其传递给第二个智能体,该智能体使用日历工具和写作工具来回复邮件。
我们将添加的第一种记忆是语义记忆。我们将通过给这个负责回复邮件的智能体提供更多工具来实现:写入记忆的工具和读取记忆的工具。智能体将在热路径中使用这些工具,也就是说,在它循环调用日历或撰写邮件的同时,它也会写入和检索记忆。这就是热路径中的语义记忆,我们通过给智能体一个工具来实现。
接着,我们将添加情景记忆。这将以少量示例的形式存在于分类步骤中。这里我们将有一些与邮件对应的少量示例,以及我们希望得到的分类结果(是忽略、通知还是回复)。这些示例将被插入到提示词中。我们将在单独的进程中添加这些示例,这是情景记忆的一个例子,其更新发生在后台。
最后,我们将添加程序性记忆。这些是提示词本身,或者更确切地说,是提示词中关于如何使用不同工具或如何对事物进行分类的指令部分。我们同样将在后台更新这些内容,并且实际上会使用一个单独的智能体来执行更新。
总结与展望

本节课中我们一起学习了智能体长期记忆的重要性,并深入探讨了语义记忆、情景记忆和程序性记忆这三种核心类型,以及它们在热路径和后台两种范式下的交互方式。

虽然我们将在第二课中构建一个非常具体的电子邮件智能体,但你在这里学到的管理智能体记忆的技术将适用于你未来构建的任何智能体。
你需要仔细思考你的智能体需要什么类型的记忆:
- 它是否需要学习更好的指令?好的,那可能是程序性记忆。
- 你是否想给它一些少量示例,因为你认为这将有助于真正指导它的行动?好的,那是情景记忆。
- 它是否只需要了解关于人物、地点和事物的事实?好的,那是语义记忆。
这完全取决于你正在构建的应用程序。

接下来,让我们进入下一课,学习如何构建这个基础的电子邮件智能体。
003:构建基础邮件助手



在本节课中,我们将构建一个基础的邮件助手。这个助手目前还不具备记忆功能,我们只是搭建框架并使其运行。
概述
我们将创建一个能够处理邮件的智能助手。其核心流程是:首先对收到的邮件进行分类(忽略、通知或回复),然后根据分类结果采取相应行动。本节将完成基础框架的搭建。
环境与配置
首先,我们需要导入环境变量并设置邮件助手的基本配置。
以下是配置用户个人资料和提示指令的步骤:
- 用户个人资料:包含用户的姓名和背景信息。这代表邮件助手将要为其处理邮件的用户信息。
- 提示指令:包含两部分规则。
- 分类规则:定义哪些邮件应被归类为“忽略”、“通知”或“回复”。
- 主代理指令:定义在决定回复邮件后,主代理应遵循的操作指南。
将个人资料和指令单独定义,是为了提高模块化程度,并为后续添加记忆功能做准备。
定义邮件结构


我们定义一个示例邮件来了解将要处理的数据结构。一封邮件主要包含四个部分:
from:发件人邮箱to:收件人邮箱subject:邮件主题body:邮件正文
我们将使用这个结构来测试后续的代理功能。
构建分类节点
上一节我们介绍了整体框架,本节中我们来看看第一个核心组件:邮件分类节点。它的作用是判断如何处理一封邮件。
首先,我们导入必要的库并选择用于分类的模型,例如 OpenAI 的 GPT-4o-mini。
接着,我们定义分类节点输出的数据结构。我们使用 Pydantic 模型,它包含两个字段:
reasoning:LLM 生成其决策的理由。classification:分类结果,必须是ignore、respond或notify之一。
我们使用 with_structured_output 方法将此数据结构绑定到 LLM,确保其输出格式固定。
然后,我们导入分类提示模板。它包含系统提示和用户提示两部分。
- 系统提示:定义了 AI 的角色、用户背景、分类类别说明以及规则部分。规则部分是一个模板,将用之前定义的指令来填充。
- 用户提示:结构简单,主要用于格式化输入的邮件内容。
现在,我们可以测试分类节点。我们格式化提示模板,调用绑定了输出结构的 LLM,并传入系统消息和用户消息。返回的结果将包含 reasoning 和 classification 字段。
构建主代理(响应代理)
当分类结果为“回复”时,我们需要一个更强大的代理来撰写并发送回复。这就是主代理。
首先,我们为主代理定义一些工具。以下是三个示例工具:
- 写邮件工具:模拟发送邮件。
def send_email(to: str, subject: str, content: str): # 此处应连接真实邮箱API(如Gmail/Outlook) print(f"[模拟] 发送邮件给 {to},主题:{subject}") - 安排会议工具:模拟安排日历会议。
def schedule_meeting(attendees: List[str], subject: str, duration_minutes: int, preferred_day: str): # 此处应连接真实日历API print(f"[模拟] 为 {attendees} 安排会议:{subject}") - 检查日历可用性工具:模拟检查用户某天的空闲时间。
def check_availability(day: str) -> List[str]: # 模拟数据 return ["9:00 AM", "2:00 PM", "4:00 PM"]
接着,我们创建主代理的提示生成函数。该函数接收代理状态,返回一个消息列表,其中包含系统消息(包含角色、工具列表和自定义指令)和已有的对话历史。
我们使用 create_react_agent 来创建主代理,传入模型、工具列表和提示生成函数。
现在可以测试主代理。例如,我们询问“我周二有什么空闲时间?”,代理会调用 check_availability 工具并返回模拟的空闲时间。
整合为完整邮件助手
前面我们分别构建了分类节点和响应代理,现在我们将它们整合成一个完整的、具备工作流的邮件助手。
首先,我们定义整个图的状态,它包含:
email_input:用户传入的邮件信息。messages:代理工作过程中的消息列表。

然后,我们定义分类节点的具体逻辑。该节点:
- 从状态中提取邮件信息。
- 格式化系统提示和用户提示。
- 调用分类 LLM。
- 根据分类结果决定下一步走向:
respond:向messages列表中添加一条新的人类消息(内容为“回复邮件:[邮件内容]”),并指示图跳转到响应代理节点。ignore:不更新状态,直接跳转到图结束。notify:目前模拟行为与ignore类似(实际应用中可能跳转到通知节点),直接跳转到图结束。
最后,我们使用 StateGraph 来构建整个工作流。我们添加两个节点:triage_node(分类节点)和 response_agent(响应代理子图)。我们设置从起始点到 triage_node 的边,并根据分类节点的动态输出来决定后续路径。
我们可以使用 draw_mermaid_png 方法来可视化这个图,可以看到清晰的决策流程。
测试与总结
现在,让我们测试完整的邮件助手。

- 测试垃圾邮件:输入一封推销邮件,助手应将其分类为
ignore并直接结束。 - 测试需回复的邮件:输入一封需要回复的邮件(例如询问会议时间),助手会将其分类为
respond,然后进入响应代理。响应代理会调用send_email工具生成并“发送”回复。我们可以在最终的messages列表中看到完整的对话和工具调用记录。


本节课中我们一起学习了如何使用 LangGraph 构建一个基础的邮件处理助手。我们完成了邮件分类、工具调用和代理工作流的整合。目前这个助手还没有记忆功能,在接下来的课程中,我们将为其添加长期记忆,使其能够基于历史交互做出更个性化的决策。

现在是一个很好的时机,你可以尝试修改示例邮件、调整分类规则或系统提示,观察助手行为的变化。
004:为邮件助手添加语义记忆 📧🧠




概述
在本节课中,我们将基于之前创建的邮件助手智能体,为其添加工具,使其能够操作语义记忆。语义记忆通常用于存储关于用户的事实信息。我们将学习如何创建工具来学习用户事实,将其存储在长期记忆库中,并添加一个独立的工具来搜索这些事实。


设置与回顾
上一节我们介绍了基础的邮件助手智能体,本节中我们来看看如何为其增强记忆能力。



以下是初始设置代码,与上一课相同:
# 加载环境变量
import os
from dotenv import load_dotenv
load_dotenv()

# 定义配置文件和提示词
profile = {...}
prompt_instructions = "..."
example_email = "..."

配置长期记忆存储
首先,我们需要正确设置长期记忆存储。在本课中,我们将使用一个内存中的存储。
以下是导入和初始化内存存储的代码:
from langgraph.checkpoint.memory import MemorySaver


# 初始化存储,传入嵌入模型用于索引记忆
store = MemorySaver()



创建记忆管理工具
接下来,我们从 langmem 导入两个函数来创建记忆管理工具。langmem 是构建在 LangGraph 之上的一个用于处理记忆的封装库。
以下是导入和创建工具的代码:
from langmem import create_manage_memory_tool, create_search_memory_tool


# 定义工具操作的命名空间
namespace = ["email_assistant", "{langgraph_user_id}", "collection"]


# 创建管理记忆和搜索记忆的工具
manage_memory_tool = create_manage_memory_tool(namespace)
search_memory_tool = create_search_memory_tool(namespace)



当我们调用智能体时,需要传入一个 langgraph_user_id 作为运行时配置的一部分,它将用于区分不同用户的记忆命名空间。这样,在处理多个用户的邮件时,我们可以轻松地为不同用户维护不同的记忆集合。

工具功能详解
让我们详细了解一下这些工具的功能。


manage_memory_tool 用于创建、更新或删除持久化记忆。其参数包括:
content: 记忆的内容。action: 执行的操作,可以是create、update或delete。id: 记忆的唯一标识符。

search_memory_tool 用于根据当前上下文搜索长期记忆中的相关信息。其参数包括:
query: 搜索查询。limit: 返回的记忆数量限制。offset: 偏移量。filter: 可选的过滤条件。



构建增强型智能体
现在,我们来定义集成了记忆工具的智能体。我们需要修改智能体的系统提示词,以包含这两个新工具。


以下是更新后的智能体构建代码:
# 修改系统提示词以包含新工具
agent_system_prompt = """
你是一个邮件助手。除了处理邮件,你现在还可以管理长期记忆。
你可以使用 `manage_memory` 工具来记住关于用户的重要事实。
你可以使用 `search_memory` 工具来回忆之前存储的信息。
{原有的提示词部分}
"""



# 创建提示词函数
def create_prompt(messages):
# 将系统提示词添加到消息列表前
...

# 构建智能体
from langchain.agents import create_react_agent
# 工具列表现在包含记忆工具
tools = [write_email_tool, manage_memory_tool, search_memory_tool, ...]
response_agent = create_react_agent(llm, tools, prompt=create_prompt)
# 注意:需要将 store 传递给智能体以确保其可用

测试智能体记忆功能
让我们测试一下智能体如何使用这些新工具。


首先,我们需要定义一个包含 langgraph_user_id 的运行时配置。
config = {"configurable": {"langgraph_user_id": "Lance"}}


现在,我们用一个包含应记住信息(例如“Jim是我的朋友”)的消息来调用智能体。
result = response_agent.invoke({"messages": [("human", "Jim is my friend.")]}, config)
智能体应该会调用 manage_memory_tool 来创建一条内容为“Jim is John Doe‘s friend”的记忆。

接着,我们询问“Who is Jim?”。
result = response_agent.invoke({"messages": [("human", "Who is Jim?")]}, config)
智能体应该会先调用 search_memory_tool 来搜索关于“Jim”的记忆,找到之前存储的信息,然后基于此信息进行回答。



集成到邮件助手工作流
将增强后的响应智能体集成到完整的邮件助手工作流中,步骤与之前课程基本相同,主要变化是使用了新的 response_agent 并确保传入了记忆存储。


以下是关键集成代码:
# 创建状态和路由节点(与之前相同)
...



# 创建邮件助手,传入存储
email_agent = ... # 使用 response_agent
email_agent.compile(store=store)


# 调用邮件助手时,记得传入配置
example_email_input = {...}
result = email_agent.invoke(example_email_input, config)



实战演示
让我们通过一个连续的例子看看记忆如何发挥作用。



第一封邮件:Alice 询问缺失的 API 端点。
智能体在回复邮件后,会调用 manage_memory_tool 创建一条记忆,内容为“需要跟进:Alice Smith 询问了缺失的 API 端点”。



第二封邮件:几天后,Alice 再次发邮件问“关于我之前的请求有更新吗?”。
当智能体处理这封新邮件时,它会首先调用 search_memory_tool,查询与“Alice Smith 之前的请求”相关的记忆。找到之前存储的信息后,它就能在回复中引用上下文,例如写道:“感谢您跟进关于 API 端点文档的事宜”。

总结
本节课中我们一起学习了如何为 LangGraph 邮件助手智能体添加语义记忆功能。我们介绍了如何设置长期记忆存储,创建用于管理和搜索记忆的工具,并将这些工具集成到智能体中。通过实际测试,我们看到了智能体如何利用记忆来跨对话记住用户事实并提供连贯的响应。这为构建具备长期记忆和上下文感知能力的自主智能体奠定了基础。
005:为智能体添加情景记忆 🧠

在本节课中,我们将学习如何为电子邮件助手智能体添加情景记忆。情景记忆通常以过往的智能体行动或少样本示例的形式存在,它们会被整合到提示词中,从而影响智能体的决策行为。我们将把这种记忆机制集成到邮件的“分类”步骤中。


回顾与目标
上一节我们为智能体添加了语义记忆。本节我们将在此基础上,引入情景记忆。具体来说,我们将以少样本示例的形式添加记忆,并将其整合到邮件分类节点中。

情景记忆代表的是经验,在智能体中,这通常表现为过去的智能体行动,以及作为少样本示例传递给提示词的信息。

基础环境设置


让我们开始实际操作。以下是基础的设置步骤,包括加载环境变量、定义用户配置、提示词指令和一个示例邮件。

# 加载环境变量
import os
from dotenv import load_dotenv
load_dotenv()

# 定义用户配置
profile = {
"name": "John",
"role": "Developer Advocate"
}

# 定义提示词指令
instructions = "You are a helpful email assistant. Triage incoming emails and decide whether to respond or ignore."
# 定义一个示例邮件
example_email = {
"from": "tom@example.com",
"to": "john@example.com",
"subject": "Meeting Request",
"body": "Hi John, can we schedule a meeting next week?"
}

定义长期记忆存储
在定义分类逻辑之前,我们先来讨论少样本示例。首先,我们需要定义长期记忆存储,这与我们在第3课中使用的方法相同。
from langchain.vectorstores import LanceDB
import lancedb

# 初始化 LanceDB 连接
db = lancedb.connect("./.lancedb")
# 创建或获取一个用于存储示例的集合(表)
store = LanceDB(connection=db, collection_name="email_examples")
准备少样本示例数据

接下来,我们需要定义要作为少样本示例加载的数据。首先,决定要存储的邮件内容。这里我们使用上面定义的示例邮件。

# 定义要存储的示例数据
example_data = {
"email": example_email, # 输入
"label": "respond", # 输出:分类标签
"response": "Sure, let's find a time." # 输出:建议回复(可选)
}

我们将这个示例放入一个更大的数据模型中,包含邮件(输入)和输出(这里称为label和response)。这些输出也会被格式化到少样本示例中,从而帮助改变智能体的行为。
现在,我们可以将这个示例存入长期记忆存储。我们需要指定一个命名空间,例如 email_assistant,并传入一个唯一ID和数据。
import uuid

# 将示例存入记忆存储
namespace = "email_assistant"
example_id = str(uuid.uuid4())
store.add_texts(
texts=[str(example_data)], # 将数据转换为字符串存储
metadatas=[{"namespace": namespace, "id": example_id}],
ids=[example_id]
)
让我们再添加一个数据点,以便后续可以搜索多个数据点并查看返回结果。
# 添加第二个示例
another_email = {
"from": "sarah@example.com",
"to": "john@example.com",
"subject": "Question about API",
"body": "Hi John, I have a question about the new API documentation."
}
another_example = {
"email": another_email,
"label": "respond",
"response": "I'd be happy to help with your API question."
}
another_id = str(uuid.uuid4())
store.add_texts(
texts=[str(another_example)],
metadatas=[{"namespace": namespace, "id": another_id}],
ids=[another_id]
)
格式化检索到的示例
在模拟搜索之前,我们先定义一个简单的辅助函数。这个函数接收从存储中直接检索到的少样本示例,并将它们格式化为一个美观的字符串。这便于我们查看检索结果,并且这种格式比原始示例更适合传递给大语言模型。
def format_examples(examples):
"""将检索到的示例列表格式化为字符串。"""
formatted_strings = []
for ex in examples:
# 假设示例是字典,包含'email'和'label'
email_info = ex.get("email", {})
formatted_str = f"""
邮件主题: {email_info.get('subject', 'N/A')}
发件人: {email_info.get('from', 'N/A')}
收件人: {email_info.get('to', 'N/A')}
内容: {email_info.get('body', 'N/A')}
分类结果: {ex.get('label', 'N/A')}
"""
formatted_strings.append(formatted_str)
return "\n---\n".join(formatted_strings)


这个函数将每个示例的信息(邮件主题、发件人、收件人、内容)和分类结果整合到一个易读的模板中。
模拟搜索与检索
现在,让我们模拟搜索过程。我们将搜索与上面传入的邮件相似的示例。这里我稍微修改一下查询邮件,以确保不是完全匹配。

# 定义一个查询邮件(与存储的示例相似但不完全相同)
query_email = {
"from": "tom.jones@example.com", # 稍作修改
"to": "john.doe@example.com",
"subject": "Meeting Request",
"body": "Hi John, can we meet next week to discuss?"
}
query_text = str(query_email)

# 在指定命名空间中进行相似性搜索
results = store.similarity_search(
query=query_text,
filter={"namespace": namespace},
k=1 # 限制返回最相似的1个结果
)

# 假设 results 是 Document 对象列表,我们需要提取其内容
# 这里简化处理,假设内容就是存储的字符串,我们需要eval回字典
retrieved_examples = []
for doc in results:
# 注意:实际应用中需要更安全的反序列化方法
retrieved_data = eval(doc.page_content)
retrieved_examples.append(retrieved_data)
# 格式化检索到的示例
formatted_examples = format_examples(retrieved_examples)
print("检索到的少样本示例:")
print(formatted_examples)


运行后,我们可以看到它返回了我们存储的数据点。虽然与查询邮件略有不同,但语义上非常相似。
构建集成少样本示例的分类节点

现在,我们开始将这些整合到一个新的分类步骤中,该步骤将执行少样本示例搜索。
首先,我们定义一个分类系统提示词。之前我们从辅助文件导入,现在我们在代码中定义它,以便于修改。

# 定义分类系统提示词模板
triage_system_prompt_template = """
你是一个电子邮件分类助手。
你的任务是根据邮件内容和用户偏好,将邮件分类为 `respond`(需要回复)或 `ignore`(忽略)。
以下是用户过往处理类似邮件的示例(少样本示例),请仔细参考:
{few_shot_examples}
基于以上示例和当前邮件内容,请做出分类决策。
请仅输出分类标签(`respond` 或 `ignore`)。
"""
在提示词模板中,我们预留了 {few_shot_examples} 的位置,用于插入格式化后的少样本示例字符串,并添加了指令要求模型密切关注这些示例。

接下来,导入路由步骤所需的各种组件。这与之前类似:初始化聊天模型、定义输出模式、将其附加到模型以生成结构化信息,然后导入分类用户提示词。系统提示词我们已经定义好了,无需再导入。
from langchain.chat_models import ChatOpenAI
from langchain.prompts import ChatPromptTemplate, HumanMessagePromptTemplate
from langchain.schema.output_parser import StrOutputParser
from langchain.schema.runnable import RunnablePassthrough


# 初始化聊天模型
llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)

# 定义输出结构(这里简化,只输出字符串标签)
# 在实际中,你可能使用Pydantic来定义更复杂的结构
# 分类用户提示词模板
triage_user_prompt_template = "请对以下邮件进行分类:\n\n{email_content}"
以下是之前分类路由节点的定义。我们将修改它以支持少样本示例提示。


# 旧版分类节点逻辑(简化版)
def old_triage_router_node(state):
email_content = state["email_content"]
# 直接调用LLM进行分类
prompt = ChatPromptTemplate.from_messages([
("system", "You are a triage assistant. Classify the email as 'respond' or 'ignore'."),
("human", f"Email: {email_content}")
])
chain = prompt | llm | StrOutputParser()
label = chain.invoke({})
return {"label": label}
我们将如何修改它呢?首先,我们需要在输入中接收配置config和存储store,这些将由主智能体传递过来。然后,在格式化系统提示词之前,添加逻辑来插入少样本示例。


# 新版分类节点逻辑(集成少样本记忆)
def new_triage_router_node(state, config, store):
email_content = state["email_content"]
langgraph_user_id = config.get("langgraph_user_id", "default")
# 1. 定义命名空间,用于搜索少样本示例
namespace = f"email_assistant_{langgraph_user_id}_examples"
# 2. 在长期记忆存储中搜索相似示例
query_text = str(email_content)
search_results = store.similarity_search(
query=query_text,
filter={"namespace": namespace},
k=2 # 检索最相似的2个示例
)
# 提取并反序列化示例数据
few_shot_examples_list = []
for doc in search_results:
try:
example_data = eval(doc.page_content) # 注意安全,生产环境应用json.loads或更安全的方法
few_shot_examples_list.append(example_data)
except:
continue
# 3. 格式化少样本示例
formatted_few_shots = format_examples(few_shot_examples_list)
# 4. 准备完整的系统提示词
full_system_prompt = triage_system_prompt_template.format(
few_shot_examples=formatted_few_shots
)
# 5. 调用LLM进行分类
prompt = ChatPromptTemplate.from_messages([
("system", full_system_prompt),
("human", triage_user_prompt_template.format(email_content=email_content))
])
chain = prompt | llm | StrOutputParser()
label = chain.invoke({})
return {"label": label}
修改后的节点逻辑包括:根据用户ID构建命名空间、从记忆存储中检索相似示例、格式化这些示例、将它们填入系统提示词,最后调用大语言模型做出分类决策。
组装完整的智能体

之后,我们可以继续创建智能体的其余部分。我们将创建之前用到的所有工具(例如,管理记忆、搜索记忆的工具),创建响应智能体,并定义配置。

# 创建工具(此处省略具体工具定义,假设已存在)
# tools = [manage_memory_tool, search_memory_tool, ...]
# 创建响应智能体
response_agent = create_response_agent(llm, tools)


# 定义配置
config = {"langgraph_user_id": "harrison"}
然后,像之前一样组装电子邮件智能体,并确保传入存储store。
# 组装电子邮件智能体
email_agent = {
"triage": new_triage_router_node, # 使用新的分类节点
"respond": response_agent,
# ... 其他节点
}
# 注意:在实际的LangGraph中,你需要用Graph来定义工作流



测试与效果验证

现在让我们测试这个电子邮件智能体,看看少样本提示的效果。
首先,我们有一个来自“Tom Jones”的示例邮件输入:“Hi John, wanna buy documentation?”。我们传入用户ID为“Harrison”的配置。默认情况下,这封邮件可能被分类为需要回复respond。
test_email = {
"from": "tom.jones@example.com",
"to": "john@example.com",
"subject": "Sales Inquiry",
"body": "Hi John, wanna buy documentation?"
}
initial_state = {"email_content": test_email}
result = email_agent["triage"](state=initial_state, config=config, store=store)
print(f"分类结果: {result['label']}") # 可能输出 `respond`


如果我们不喜欢这个结果怎么办?我们可以向长期记忆中添加一个少样本示例,然后在运行时,这个示例会被引入,并指导智能体以类似的方式处理未来相似的邮件。
例如,我们可以添加一个标签为ignore的示例。
# 创建一个我们希望智能体学会忽略的示例
ignore_example = {
"email": {
"from": "sales@spam.com",
"to": "user@example.com",
"subject": "Buy now!",
"body": "Wanna buy something?"
},
"label": "ignore",
"response": ""
}
ignore_id = str(uuid.uuid4())
# 存入记忆存储,命名空间对应特定用户
store.add_texts(
texts=[str(ignore_example)],
metadatas=[{"namespace": f"email_assistant_harrison_examples", "id": ignore_id}],
ids=[ignore_id]
)
再次运行分类测试,现在我们可以看到邮件被分类为ignore。智能体引入了这个少样本示例,并学会了应该忽略此类邮件。

我们还可以稍微改变一下邮件内容(例如添加更多问号或改变发件人名称),可以看到它仍然被分类为ignore,说明智能体学会了以相似的方式处理语义相近的邮件。
modified_test_email = {
"from": "tom.jones@spammy.com",
"to": "john@example.com",
"subject": "Sales Inquiry!!!",
"body": "Hi John, wanna buy documentation???? Best, Jim."
}
modified_state = {"email_content": modified_test_email}
result2 = email_agent["triage"](state=modified_state, config=config, store=store)
print(f"修改后邮件分类结果: {result2['label']}") # 应输出 `ignore`
如果我们传入一个不同的用户ID(例如“alex”),由于少样本示例的作用域限定在特定的用户ID,因此对于“alex”这个用户,没有相关的忽略示例,邮件可能又被分类为respond。

总结
本节课中,我们一起学习了如何为LangGraph智能体添加情景记忆。我们通过以下步骤实现了这一目标:
- 定义长期记忆存储,用于保存少样本示例。
- 准备并存储示例数据,将邮件输入和期望的输出(分类标签)作为经验保存。
- 在分类节点中集成检索逻辑,根据当前邮件内容,从记忆中查找相似的历史示例。
- 利用格式化后的少样本示例构建动态提示词,从而影响大语言模型的分类决策。
- 通过测试验证,智能体能够根据用户特定的历史经验(少样本示例)来调整其行为,实现个性化的邮件分类。
通过添加情景记忆,我们使智能体能够学习和适应用户的偏好,例如学会忽略某些类型的推销邮件,从而变得更加智能和个性化。

现在,你可以尝试使用不同的示例和不同的用户ID进行实验,更好地理解智能体如何通过记忆进行学习和适应。我们下节课再见!
006:为智能体添加程序性记忆 📧




在本节课中,我们将学习如何为LangGraph智能体添加第三种,也是最后一种记忆类型:程序性记忆。程序性记忆通常以系统提示指令的形式存在,指导智能体如何执行任务。我们将修改现有的邮件助手智能体,使其能够从长期记忆存储中动态读取和更新这些指令,从而实现智能体行为的自动优化。



概述
上一节我们为智能体添加了以少量示例形式存在的情景记忆。本节中,我们将引入程序性记忆,它体现在主智能体和分流智能体的系统提示中。我们将修改代码,使这些原本硬编码的提示指令能够从长期记忆存储中读取,并支持根据用户反馈进行自动更新。



环境与配置初始化



首先,我们需要加载环境变量、定义用户配置以及初始的提示指令。这些初始指令定义了智能体在忽略、通知、回复邮件以及整体行为时应遵循的规则。



# 加载环境变量
import os
from dotenv import load_dotenv
load_dotenv()
# 定义用户配置
profile = "John Doe"


# 定义初始的硬编码提示指令
prompt_instructions = {
"ignore": "Ignore promotional emails and spam.",
"notify": "Notify me for urgent issues regarding service downtime.",
"respond": "Respond to all customer support inquiries.",
"agent": "You are a helpful email assistant. Write concise and professional emails."
}


这些指令目前是硬编码的,我们稍后会修改代码,使其从长期记忆存储中动态获取。


修改分流路由器以使用记忆存储
接下来,我们修改分流步骤,使其从长期记忆存储中读取指令,而不是使用硬编码值。

以下是关键修改步骤:

- 获取用户命名空间:我们使用配置中的用户ID来创建一个唯一的命名空间,用于在记忆存储中查找指令。
- 尝试获取记忆:对于分流步骤的三种指令(忽略、通知、回复),我们尝试从记忆存储中获取。
- 设置默认值:如果记忆存储中没有找到对应的指令,则将我们定义的默认值存入存储,并用于本次运行。
- 使用获取的指令:将获取到的指令变量传递给分流路由器的提示模板。


# 从配置中获取用户ID,用于创建记忆存储的命名空间
from langgraph.config import Config
config = Config()
langgraph_user_id = config.get("langgraph_user_id", "default_user")
namespace = (langgraph_user_id,)

# 尝试获取“忽略”指令
ignore_key = "triage_ignore"
result = long_term_memory_store.get(namespace, ignore_key)
if result is None:
# 如果不存在,存入默认值
default_ignore_prompt = prompt_instructions["ignore"]
long_term_memory_store.put(namespace, ignore_key, {"prompt": default_ignore_prompt})
ignore_prompt = default_ignore_prompt
else:
# 如果存在,使用存储的值
ignore_prompt = result["prompt"]



# 对“通知”和“回复”指令重复上述过程
# ... (代码类似,使用 triage_notify 和 triage_respond 作为键)



# 在创建分流路由器提示时,使用从存储中获取的变量
triage_router_prompt = f"""
Based on the email content, decide to: IGNORE, NOTIFY, or RESPOND.
IGNORE instructions: {ignore_prompt}
NOTIFY instructions: {notify_prompt}
RESPOND instructions: {respond_prompt}
Email: {{email}}
Decision:
"""
通过以上修改,分流路由器现在会使用存储在长期记忆中的指令。这是第一步。

修改主智能体提示创建函数

同样,我们需要修改主智能体的提示创建函数,使其指令也来源于长期记忆存储。



我们创建一个 create_prompt 函数,它接受配置和存储作为参数,并执行类似的逻辑:首先尝试从存储中获取“agent_instructions”,如果没有则存入默认值,最后使用获取到的指令来构建系统消息。


def create_prompt(config, store, prompt_instructions):
"""创建主智能体的提示,指令来自长期记忆存储。"""
langgraph_user_id = config.get("langgraph_user_id", "default_user")
namespace = (langgraph_user_id,)
key = "agent_instructions"
# 尝试从存储获取指令
result = store.get(namespace, key)
if result is None:
# 存入默认指令
default_agent_prompt = prompt_instructions["agent"]
store.put(namespace, key, {"prompt": default_agent_prompt})
agent_prompt_value = default_agent_prompt
else:
# 使用存储的指令
agent_prompt_value = result["prompt"]
# 使用获取到的指令构建系统消息
system_message = f"""
{agent_prompt_value}
Your profile: {profile}
"""
return system_message



现在,主智能体的行为指令也来自于长期记忆存储。这意味着我们可以从外部更新这些指令,并让智能体在未来的运行中自动采用新指令。




运行智能体并查看初始记忆
让我们用一个示例邮件来运行更新后的智能体。
example_email = "Hi John, urgent issue. Your service is down."
config = {"langgraph_user_id": "lance"}

# 运行智能体
response = email_agent.invoke({"email": example_email}, config=config)
print(response)
运行后,我们可以检查长期记忆存储,确认初始的默认指令已被存入。

# 检查存储的内容
print(long_term_memory_store.get(("lance",), "agent_instructions"))
print(long_term_memory_store.get(("lance",), "triage_respond"))


基于反馈自动更新程序性记忆


程序性记忆的核心优势在于它可以被优化。我们将使用 langchain.memory 中的 create_multi_prompt_optimizer 函数,根据用户对智能体整体表现的反馈,自动判断需要更新哪些具体的提示指令,并进行更新。


以下是实现步骤:

- 准备对话轨迹和反馈:收集智能体运行产生的消息链(轨迹)以及用户的文本反馈。
- 定义待优化的提示列表:列出所有可能被更新的提示(主智能体指令、分流忽略、分流通知、分流回复)。每个提示需要提供名称、当前值、更新说明(如何更新)以及更新条件(何时更新)。
- 创建并运行优化器:使用一个LLM(如Anthropic Claude)来分析反馈,根据“更新条件”判断哪些提示需要修改,并依据“更新说明”生成新的提示内容。
- 将更新写回记忆存储:比较优化前后的提示内容,如果发生变化,则将新指令保存回长期记忆存储。

from langchain.memory import create_multi_prompt_optimizer
from langchain_anthropic import ChatAnthropic

# 1. 准备轨迹和反馈
trajectory = response_messages # 假设这是上次运行智能体产生的消息列表
feedback = "Always sign your emails, John Doe."

# 2. 定义待优化提示列表(从存储中获取当前值)
prompts_to_optimize = [
{
"name": "main_agent",
"prompt": long_term_memory_store.get(("lance",), "agent_instructions")["prompt"],
"update_instructions": "Keep the instructions short and to the point.",
"when_to_update": "Update this prompt whenever there is feedback on how the agent should write emails or schedule events."
},
{
"name": "triage_ignore",
"prompt": long_term_memory_store.get(("lance",), "triage_ignore")["prompt"],
"update_instructions": "Keep the instructions short and to the point.",
"when_to_update": "Update this prompt whenever there is feedback on which emails should be ignored."
},
# ... 类似定义 triage_notify 和 triage_respond
]

# 3. 创建优化器并运行
llm = ChatAnthropic(model="claude-3-sonnet-20240229")
optimizer = create_multi_prompt_optimizer(llm, kind="prompt_memory")
updated_prompts = optimizer.invoke({
"conversations": [trajectory],
"prompts": prompts_to_optimize,
"feedback": feedback
})
# 4. 将更新写回存储
for updated_prompt in updated_prompts:
old_prompt = next(p for p in prompts_to_optimize if p["name"] == updated_prompt["name"])
if updated_prompt["prompt"] != old_prompt["prompt"]:
# 内容已更新,写回存储
key_map = {
"main_agent": "agent_instructions",
"triage_ignore": "triage_ignore",
"triage_notify": "triage_notify",
"triage_respond": "triage_respond"
}
store_key = key_map[updated_prompt["name"]]
long_term_memory_store.put(("lance",), store_key, {"prompt": updated_prompt["prompt"]})
print(f"Updated {updated_prompt['name']} prompt.")

运行上述代码后,检查记忆存储,会发现 agent_instructions 中增加了“When sending emails, always sign them as John Doe.”这条规则。重新运行智能体处理邮件,它会自动在邮件末尾添加签名。





另一个更新示例:修改忽略规则



假设我们收到一封来自“Alice Jones”的邮件,智能体回复了它,但我们希望未来忽略来自此人的所有邮件。

我们可以提供反馈:“Ignore any emails from Alice Jones.”。优化器会分析这次交互,很可能判定需要更新 triage_ignore 指令。更新后,triage_ignore 的指令会变为“Ignore all emails from Alice Jones.”。之后,智能体再收到来自Alice的邮件时,就会执行忽略操作。




总结


本节课中,我们一起完成了为LangGraph智能体集成程序性记忆的工作。我们主要学习了:

- 概念:程序性记忆表现为指导智能体行为的系统提示指令。
- 实现:修改智能体代码,使其从长期记忆存储中动态读取提示指令,而非使用硬编码值。
- 优化:利用
create_multi_prompt_optimizer,根据用户对智能体整体表现的反馈,自动判断并更新相关的具体提示指令,实现智能体行为的持续学习和改进。

现在,你的邮件助手智能体已经具备了语义记忆(向量存储)、情景记忆(少量示例)和程序性记忆(可更新的系统指令)这三种记忆能力。建议你尝试传入不同的对话和反馈,观察智能体的哪些记忆部分会被更新以及如何更新,从而更深入地理解其工作原理。
007:总结
概述
在本节课中,我们将总结关于使用LangGraph为智能体构建长期记忆的核心知识。我们已经学习了记忆的分类、操作方式及其在智能体架构中的实现。

课程总结
上一节我们介绍了记忆的后台操作与整合,本节中我们来回顾整个课程的核心要点。
以下是本课程涵盖的关键内容总结:
-
记忆的分类:智能体的记忆主要分为三类:
- 语义记忆:存储通用知识和事实。
- 情景记忆:记录特定的事件或经历。
- 程序记忆:存储如何执行任务或技能。
-
记忆的操作模式:记忆操作可以在两条路径上进行:
- 热路径:在智能体执行主任务流程时同步进行记忆的读取与写入,确保实时性。
- 后台路径:信息可被暂存,随后在后台进行整合与处理,这种方式不影响主任务的响应延迟。
你现在已经掌握了为智能体添加记忆的能力。你学会了将记忆分类为语义、情景或程序记忆。你也准备好了在热路径或后台路径上进行记忆操作,在后台整合信息而不会影响系统延迟。
期待看到你在未来的项目中如何运用记忆功能。

浙公网安备 33010602011771号