DLAI-大模型操作系统笔记-全-
DLAI 大模型操作系统笔记(全)
001:课程介绍 🧠

在本课程中,我们将探索和构建AI智能体的记忆系统。我们还将探讨一个核心观点:大型语言模型智能体可以管理自己的输入上下文窗口,从而在AI应用中扮演操作系统的角色。
本课程由《MGPT:将LLMs视为操作系统》论文的两位作者——Charles Pecker和Sarah Withs——担任讲师。如果你学习过我们之前关于提示工程和LLM API的课程,你可能已经熟悉一个概念:尽管LLMs能完成惊人的任务,但它们不具备持久记忆,你需要显式地管理其记忆。
理解上下文窗口与记忆

上一节我们提到了LLM缺乏持久记忆的问题,本节中我们来看看如何通过上下文窗口为其提供信息。
通过将信息包含在提示(也称为输入上下文)中,你可以为模型提供生成输出所需的额外信息。输入上下文中的内容决定了LLM及其应用的行为。例如:
- 聊天机器人应用拥有对话记忆,用于存储对话中早期的交流。
- 你可能希望长期追踪个人事实或姓名。
- 你可能需要跟踪任务进度。
- 你可能希望在不同智能体之间共享信息。
在RAG应用中,你从外部数据源检索相关信息,并将其引入LLM的上下文中。因此,LLM可以利用输入上下文中包含的任何信息来生成响应。
管理上下文的挑战与机遇

然而,输入上下文窗口的可用空间是有限的,使用更长的输入上下文也会增加成本并导致处理速度变慢。因此,管理这些上下文信息——决定在输入上下文中包含什么——至关重要。
MGPT论文描述了一种新颖的方法:让LLM自己来管理上下文。如果你熟悉计算机系统中的虚拟内存概念,MGPT使用了一个很好的类比来解释这个想法。如果不熟悉,也无需担心,课程内容依然清晰且实用。以下是Charles对这个类比的解释:
我们可以将上下文窗口类比为计算机上的虚拟内存。你的计算机认为自己拥有一个非常大的内存(虚拟内存),远大于其实际的物理内存。当它试图引用一个不在物理内存中的虚拟地址时,操作系统会首先通过将物理内存中的一个信息块移出到磁盘(保存该块中的任何更改)来腾出空间,然后将具有引用地址的新信息块从磁盘取回物理内存。
类似地,你可以将LLM的上下文窗口视为我们系统中的物理内存。一个LLM智能体扮演操作系统的角色,决定哪些信息应该被包含在上下文窗口中。AI可以使用LLM进行规划、使用工具并做出决策(例如决定停止或继续任务)。类似的方法也让AI智能体能够管理其记忆以支持记忆管理。

课程实践内容概述
接下来,我们将概述本课程中你将动手实践的内容。
智能体会被分配一部分上下文窗口作为长期记忆,它可以写入其中。同时,智能体被赋予访问外部存储(如数据库)的工具,以创建更大的记忆存储。通过结合写入其上下文内存和外部内存的工具,以及搜索外部内存并将结果放入LLM上下文的工具,智能体可以有效地管理记忆。
在本课程中,你将把这些想法付诸实践:
- 构建基础智能体:在这些课程中,你将从头开始构建一个能够编辑自身记忆的LLM智能体。这将让你深入理解LLM记忆管理的基本思想。
- 学习核心概念:接下来,你将超越基础,学习MGPT论文中的关键概念。
- 使用Lettra框架:然后我们将介绍Lettra——一个开源智能体框架。在该框架中,智能体拥有管理上下文窗口所需的工具和信息。你不仅可以使用Lettra构建如原论文所述的MGPT智能体,还可以超越研究论文,构建具有更高级记忆类型的智能体。
- 深入探索与实践:在多个课程中,你将创建智能体并探索记忆构建的细节。你将通过构建自定义任务记忆来实践这些知识。
- 应用所学知识:你将把这些知识应用于各种应用,包括构建你自己的研究智能体和一个HR多智能体应用,其中智能体们共享记忆。
我们要感谢帮助创建本课程的一些人:加州大学伯克利分校教授兼Lettra顾问Joseph Gonzalez、Lettra团队,以及来自DeepLearning.AI的Jeff Ladwig。
总结与展望
本节课中,我们一起学习了让AI智能体管理自身记忆这一新颖而强大的技术。它提供了一个强大的基础设施,可以在此基础上构建许多应用程序。这里的想法非常令人兴奋。

让我们进入下一个视频,开始深入学习这些内容。
002:从零实现自编辑记忆
在本节课中,我们将学习如何为智能体构建一个能够自我更新的长期记忆系统。我们将从零开始,逐步实现一个具备自编辑记忆功能的基础智能体。


概述

自编辑记忆指的是允许大语言模型随时间更新其自身长期或持久记忆的概念。这是构建能够持续学习和改进的智能体的关键方面。我们将通过代码实践,理解如何将记忆整合到智能体的推理循环中,并使其能够自主更新这些记忆。
智能体与聊天机器人的区别
上一节我们介绍了自编辑记忆的概念,本节中我们来看看是什么让智能体不同于普通的聊天机器人。
大多数聊天机器人底层使用大语言模型来生成对话回复。例如,用户说“你好”,我们将其附加到对话历史中,然后请求大语言模型生成对话中的下一条消息。模型生成一个回复,比如机器人说“嘿,你好”。
那么,智能体有何不同?智能体通过在一个自主循环中采取多步行动来实现自主行为。对于一个用户消息,智能体实际上可能会多次运行大语言模型。例如,它可能先进行内部思考:“我刚收到一条消息,我知道是谁发送的吗?”然后,智能体可能会运行一个记忆搜索工具。之后,智能体最终可能决定发送回复:“嘿,莎拉,再次见到你真好,希望你今天生日过得愉快。”当智能体运行记忆搜索工具时,它能够找到关于用户的额外信息,甚至发现用户的生日就是今天。
在这个例子中,我们看到一条用户消息触发了三次不同的大语言模型调用。这就是我们所说的智能体循环。
实现多步推理
为了实现大语言模型的多步推理,需要一个能够更新状态的推理循环。
我们有一个智能体状态,它被放入上下文窗口中。这个上下文窗口是大语言模型推理的输入,而大语言模型推理的结果则用于更新智能体状态。我们称这个循环中的单个推理步骤为智能体步。
为智能体添加记忆
那么,我们如何为智能体添加记忆呢?长期记忆是智能体状态的一部分,它会被编译到上下文窗口中。我们称这个过程为上下文编译。
让我们想象一个智能体没有任何长期记忆的情况。此时,上下文窗口可能非常简单,例如:“你是一个乐于助人的助手”。但如果智能体确实有一些记忆呢?例如,用户名叫莎拉,生日是2001年1月1日。上下文编译指的是我们需要以某种方式将这些智能体状态放入上下文窗口。
一种方法,就像这个例子一样,是将字典数据转换成某种句子形式。

使记忆可编辑
现在我们已经理解了如何为智能体添加记忆。但如何让这个记忆变得可编辑呢?
我们可以通过使用特殊的记忆工具,使智能体记忆能够自我编辑。例如,假设人类说“你好”,智能体基于记忆认为人类的名字是莎拉,于是回复“嗨,莎拉”。然后人类说“我的名字其实是查尔斯,不是莎拉”。这时,智能体可能会思考一下:“哦,不,我的记忆肯定出错了,需要修正它。”接着,它会调用一个专门更新智能体状态中人类名字的特殊函数。然后,因为智能体可以运行多次,它会回复:“抱歉,查尔斯。”
实践:从零构建自编辑记忆智能体
让我们将这些知识付诸实践,从零开始构建一个具有自编辑记忆的智能体。我们将指导你如何使用OpenAI的工具调用功能来实现一些基本的记忆管理特性。
第一步:设置环境与模型
首先,我们需要设置OpenAI。接下来,我们需要选择一个模型,这里我们使用gpt-4o-mini,它在速度、成本和人类可理解性之间取得了良好的平衡。
第二步:定义系统提示词
我们需要定义系统提示词。目前我们保持非常简单,只使用“你是一个聊天机器人”。
第三步:进行大语言模型请求
我们消息请求的格式是将系统提示词放在前面,然后是聊天历史。在这个例子中,我们有一个空的聊天记录,所以唯一的消息是用户询问“我的名字是什么?”。
我们期望发生什么?由于没有任何关于名字的上下文,正如预期,智能体回复:“抱歉,我不知道你的名字。我今天能如何帮助您?”
第四步:向上下文窗口添加记忆
在Python中,我们可以拥有的最基本记忆形式是一个Python字典。
让我们设置一个智能体记忆字典,它有一个字段human,用于保存所有与人类相关的记忆。目前,我们唯一的记忆是:人类的名字是鲍勃。
请注意,我们实际上必须更新我们的系统提示词。我们的系统提示词不再仅仅是“你是一个聊天机器人”,它还包含了额外的信息,明确告诉大语言模型它拥有某种记忆。
我们告诉大语言模型,它的上下文中有一个名为“记忆”的部分,其中包含与对话相关的信息。我们明确指示大语言模型使用其记忆来个性化对话。
接下来,让我们实际发送这个请求到OpenAI。你会注意到这里的主要区别是,我们现在使用了新的系统提示词,并且手动将记忆注入到了系统提示词中。
和之前一样,聊天历史只有一条消息:“我的名字是什么?”。我们期望这里会发生什么?在这种情况下,我们实际上提供了关于人类名字的信息,因此我们期望聊天机器人返回类似“你的名字是鲍勃”的内容。很好,一切如预期运行。
第五步:实现记忆编辑工具
在本节中,我们将介绍如何使这个记忆对象能够被智能体编辑。
因为我们在Python中表示记忆(它只是一个Python字典),定义记忆编辑工具的方法就是简单地编写一个Python函数。
让我们创建一个简单的函数,名为core_memory_save。core_memory_save接受两个参数:section(部分)和memory(记忆)。core_memory_save将索引到相应的部分,并将记忆追加到该部分。
让我们看看它在实践中如何工作。当前记忆是空的,它有两个字段,但都是空字符串。现在,让我们尝试自己运行这个记忆编辑工具。我们运行了core_memory_save,并添加了一些关于人类的信息:人类的名字是查尔斯。所以,它应该已经更新了记忆。很好,现在我们知道我们的记忆编辑工具按预期工作了。
第六步:向大语言模型描述工具
下一步是找到一种方法,向大语言模型描述它应该如何与OpenAI的API一起使用这个工具。这需要你做两件事:
- 你必须提供函数的描述。在我们的例子中,描述是:“保存关于你(智能体)或你正在聊天的人类的重要信息”。
- 接下来,你需要提供某种JSON模式。这基本上是以编程方式描述函数的参数是什么以及参数的类型是什么。此外,我们指定这两个参数都是必需的。
现在我们有了工具编辑描述和模式,我们可以将这些元数据传递给OpenAI。OpenAI将使用这些信息来告知大语言模型它可以访问这些工具。
第七步:执行工具调用
让我们快速回顾一下这个调用。我们传入了系统提示词,然后也传入了记忆,最后传入了聊天历史。同样,这里的聊天历史是一条消息:“我的名字是鲍勃”。
那么,我们期望会发生什么?用户说“我的名字是鲍勃”,但记忆被初始化为空状态。因此,我们希望这里的智能体会决定使用core_memory_save函数来保存关于用户名字是鲍勃的信息。
让我们看看发生了什么。我们可以看到finish_reason是tool_calls,这意味着智能体或大语言模型正在尝试调用一个工具。我们可以看到智能体试图调用的工具是core_memory_save,并且我们可以看到函数的参数是:section: human, memory: the human's name is Bob。太棒了!这正是我们想要的。大语言模型在看到聊天历史中的信息后,正试图保存“人类的名字是鲍勃”这个信息。
然而,OpenAI实际上不会为你执行这个工具,这需要你自己来完成。
执行工具的第一步是加载参数。OpenAI的响应将参数作为字符串化的JSON传递。这意味着我们可以使用json.loads函数将参数加载回字典。现在我们有了字典形式的参数,我们只需要将它们传递给实际的函数。我们可以使用**语法来实现这一点。
一旦我们有了字典形式的参数,我们需要做的就是运行这个函数。运行函数后,我们可以检查我们的记忆。正如预期的那样,它被更新了。
第八步:测试更新后的记忆
让我们再次运行智能体,看看在记忆更新后它的响应有何不同。在这个请求中,我们包含了系统提示词、更新后的记忆对象和一个问题:“我的名字是什么?”。正如预期,智能体回复:“你的名字是鲍勃”。
恭喜!你现在已经实现了一个记忆编辑功能。
第九步:实现多步推理循环
在我们当前的实现中,智能体一次只能执行一个步骤:要么编辑记忆,要么回复用户。
然而,如果我们希望智能体支持多步推理,以便它能将多个动作组合在一起,我们可以通过在一个while循环中调用聊天补全功能来实现一个智能体循环,并允许智能体决定是继续其推理步骤还是跳出循环。为简单起见,我们假设如果智能体的响应不是工具调用,我们就跳出循环;如果是工具调用,我们就保持在循环内。
让我们从重置智能体记忆开始。接下来,我们将对系统提示词进行一些修改。系统提示词包含了关于智能体应如何使用工具的额外信息。我们让智能体知道,它要么需要调用一个工具,要么需要向用户写一个回复。我们还告诉智能体不要多次执行相同的动作。最后,我们还告诉智能体,当它学到新信息时,应该总是调用core_memory_save工具。这些只是额外的指令,帮助智能体理解如何使用我们提供给它的工具。
我们的基本智能体步函数将只接受一个参数:user_message。接下来,我们需要准备大语言模型调用的输入。在我们的例子中,我们有系统提示词、记忆,并且还包括了新的用户消息。
现在,让我们开始构建循环。循环以调用OpenAI的聊天补全API开始。记住,我们需要向API传递关于如何使用这个工具的信息。
一旦我们从API得到响应,我们将这条消息追加到消息列表中。
现在这是重要的部分:如果智能体没有调用工具,那么我们希望通过返回来跳出循环。另一方面,如果智能体正在调用一个工具,我们将执行那个工具并继续循环。
我们做的第一件事是打印工具调用信息,以便我们可以看到它。接下来,我们需要将参数加载到Python字典中。一旦我们将参数加载为Python字典,我们就可以实际执行core_memory_save函数。最后,一旦我们执行了工具调用,我们还需要将工具调用的响应注入到消息历史中。这是OpenAI期望你遵循的标准风格。
现在,让我们尝试用一个简单的消息“我的名字是鲍勃”来运行这个智能体生成循环。
很好!我们可以看到智能体做了两件事。它首先调用了一个工具,你可以看到它用新的记忆“用户的名字是鲍勃”更新了名为human的记忆部分,正如我们所期望的那样。然后,智能体有一个后续消息。
我们可以看到,智能体能够在一个步骤中既编辑其记忆,又生成一个使用更新后记忆的用户回复。虽然在这个例子中,我们只支持两个动作(一个工具调用和回复用户),但相同的结构可以用来实现更复杂的推理循环,结合许多不同的工具。
在MemGPT中,所有动作,甚至是对用户的回复,都是一个工具。一些工具,比如发送消息,被设计为中断推理循环;而其他工具,比如搜索档案记忆和编辑记忆,则被设计为不中断循环。
总结

在本节课中,我们一起学习了如何从零开始实现一个具有自编辑记忆和多步推理能力的智能体。我们探讨了智能体与聊天机器人的核心区别,理解了智能体循环和上下文编译的概念,并逐步实现了记忆的存储、编辑以及与大语言模型的集成。通过构建一个简单的循环,我们让智能体能够自主决定何时更新记忆、何时回复用户。在下一节中,我们将更深入地探讨MemGPT智能体的实际工作原理。
003:MemGPT 🧠
在本节课中,我们将学习MemGPT的核心思想。MemGPT通过让大语言模型(LLM)管理自身的上下文窗口,从而增强了智能体的能力。我们将探讨自编辑记忆、内心独白、心跳机制、上下文编译和虚拟上下文等关键概念。
什么是MemGPT?🤔

MemGPT研究论文提出了一种构建智能体的新思路。控制LLM智能体行为的主要方式是改变其输入或上下文窗口。然而,为LLM,特别是复杂的LLM智能体,构建一个最优的上下文窗口通常并不简单。

上下文窗口需要包含智能体完成任务所需的一切信息,这包括来自各种外部数据源的信息、用户数据、先前的消息、工具调用结果以及之前的推理步骤或思维链。
MemGPT研究论文展示了如何为你的LLM构建一个管理上下文窗口的操作系统,换句话说,一个执行内存管理的LLM操作系统。在MemGPT中,这个操作系统本身也是一个LLM智能体,因此内存管理是自动完成的。
MemGPT的核心思想 💡
MemGPT研究论文背后有几个关键思想。第一个是自编辑记忆的概念。这指的是LLM编辑自身记忆的能力。在许多LLM应用中,系统指令或LLM的个性化信息是固定的。在MemGPT中,智能体可以根据聊天中学到的东西更新自己的指令或个性化信息。
第二个关键思想是内心独白。在MemGPT中,智能体总是在进行内部思考,即使它们不直接回复用户。在MemGPT中,智能体总是调用工具。例如,当智能体想与用户通信时,它必须调用send_message工具。智能体唯一不调用工具的时候,是当它在输出内心独白时。
MemGPT智能体被设计为在单次用户输入后运行多个LLM步骤。例如,如果用户要求智能体执行一项复杂任务,我们期望智能体运行多个步骤,因为它将问题的不同方面分解为子任务。
在MemGPT中,智能体能够通过心跳机制进行循环。每当MemGPT智能体调用一个工具时,它可以在任何工具中添加一个特殊的心跳请求,这将触发后续调用。
总而言之,这些能力——自编辑记忆、内心独白、工具输出、通过心跳循环——使得MemGPT智能体能够自主和自我改进。我们称MemGPT智能体为自主的,因为它们可以在循环中自行采取行动。我们称MemGPT智能体为自我改进的,因为它们可以随着时间的推移编辑自己的长期记忆。
MemGPT智能体步骤示例 🔄
让我们通过一个MemGPT智能体步骤的示例。在这里,智能体收到用户的新消息:“My name is Sarah.”
MemGPT智能体首先生成一些内心独白:“The human shared their name. That seems like important information to remember. I agree.”
接下来,你可以看到智能体调用一个记忆函数,将这个新事实保存到其永久记忆库中。
然而,如果我们只允许智能体执行这一步,用户可能会感到困惑,因为智能体从未向用户发送任何回复。这里,Sarah说:“Hello, is anyone there?”

为了允许多步推理,MemGPT智能体可以使用特殊的心跳功能。通过在其调用的函数中添加一个特殊的请求心跳参数,MemGPT智能体就是在请求后续执行,基本上是循环。通过请求心跳,智能体可以运行多个步骤。现在它可以先编辑其长期记忆,然后通过调用send_message函数跟进对用户的回复。
这样用户就不会感到困惑了。
智能体状态与上下文编译 🗃️
构成我们智能体的所有不同数据统称为智能体状态。当我们在循环中运行LLM智能体时,循环中的每一步都在修改智能体状态。在大多数智能体框架中,这个智能体状态只是由程序内存中保存的不同Python变量组成。在MemGPT中,这个状态保存在数据库内部,因此智能体可以随着时间的推移而持久存在。例如,你可以关闭Python脚本并重新运行同一个智能体,你的MemGPT智能体会记住你上次运行它时的一切。
每次我们想让智能体执行一步时,我们必须决定如何将我们的智能体状态转换为将输入LLM的提示。归根结底,LLM只是一个接收令牌并输出令牌的机器。我们将从智能体状态到提示的过程称为上下文编译。
我们执行上下文编译的方式会极大地影响智能体的行为。例如,如果我们拥有的消息数量超过了提示或上下文窗口的容量,我们应该如何决定哪些消息应该省略,哪些消息应该包含?这就是MemGPT的全部意义所在。这些是LLM操作系统可以自动为你做出的重要决策。
上下文窗口的构成 🪟
让我们详细分解LLM提示或上下文窗口中的内容。在最流行的LLM API中,LLM输入分为两个部分:系统提示和聊天历史。
系统提示是指令,可以定制或改变LLM的行为,使其与基础LLM不同。在这个例子中,我们的系统提示非常简单:“You are a helpful assistant that answers questions.”
聊天历史包含用户与助手或智能体之间的消息列表。你可以将LLM的工作视为简单地根据当前聊天历史生成新的回复。一旦LLM生成新的回复,我们就可以将其添加到聊天历史的末尾。
在MemGPT中,我们在上下文窗口中创建了一个特殊部分,称为核心记忆。核心记忆用于存储关于用户的重要信息,以个性化智能体。想想你与朋友的对话如何不同于与陌生人的对话。这是因为你了解关于朋友的信息,这些信息调节或个性化对话。
在MemGPT中,系统提示还包括关于如何编辑核心记忆的信息。因此,MemGPT智能体既能看到上下文中的一个特殊保留部分用于长期记忆,也能理解如果它认为合适,它有权编辑这个记忆。
在这个简单的例子中,我们可以看到用户纠正了智能体记忆中的一个错误事实。用户问:“Who am I?” 智能体说:“I know your name is Charles and that you do AI research.” 然后用户说:“My name is actually Sarah. 😊” MemGPT智能体然后可以使用其核心记忆替换工具立即纠正其核心记忆中的这个错误事实。
核心记忆的定制与重要性 🎯
核心记忆可以定制。例如,我们可以将其分成不同的部分:一部分存储关于用户或人类的信息,另一部分存储关于智能体的信息。在本课程后面,你将学习如何创建定制的记忆模块。根据你希望智能体做什么,你可以使这个记忆模块尽可能简单或复杂。可能性是无限的。
MemGPT中的核心记忆赋予了智能体随时间学习的能力。请记住,核心记忆不仅仅是聊天历史中的另一条消息,它是上下文窗口的一个特殊保留部分,无论何时都对智能体始终可见。
为什么我们希望我们的智能体具有随时间学习的能力?随时间学习的能力是人类如此有用的部分原因。当我们犯错或接受额外训练时,我们会调整行为以改进。
你可能希望智能体具有持久记忆的一个原因是它们更具吸引力。例如,想象一下,如果用户问聊天机器人最喜欢哪种冰淇淋。这里,聊天机器人回复说它最喜欢的冰淇淋口味是香草。在大多数LLM聊天机器人中,没有持久记忆的概念,所以聊天机器人最终会忘记它说过自己最喜欢的口味是香草。因此,当用户后来提到聊天机器人早先的陈述时,聊天机器人会说一些完全不同的话。这只是长期记忆缺失如何完全破坏LLM应用沉浸感的一个简单例子。MemGPT智能体能够识别它陈述了一个偏好,并将这个偏好提交到长期记忆中。因此,如果用户问同样的问题,MemGPT智能体可以回复一个更真实的响应。
处理上下文溢出:召回记忆与归档记忆 📚
那么,当聊天历史空间不足时会发生什么?上下文窗口是有限的。因此,无论基础LLM的上下文窗口有多大,如果你的对话持续足够长,你最终都会在MemGPT中耗尽空间。当空间不足时,我们首先刷新或逐出聊天历史中的一大块消息,并用一个递归摘要替换它们。😊
我们称之为递归摘要,因为它总结了所有被逐出的消息,而这些消息本身可能包含先前生成的摘要。
许多智能体框架都有类似的技术,通过截断或删除聊天历史中的消息来处理上下文溢出,但这些消息通常是永久删除的。在MemGPT中,我们从不删除任何消息。相反,所有从上下文窗口逐出的消息都被插入到一个持久数据库中,我们称之为召回记忆。通过将旧消息移出聊天历史并放入召回记忆,我们可以释放聊天历史中的空间,同时确保完整的对话历史在需要时始终可供智能体使用。
与核心记忆使用工具来运作类似,召回记忆也使用工具。如果智能体想从召回记忆中检索一条消息,它可以使用conversation_search工具,该工具将搜索旧消息的数据库。可以将其想象成Facebook Messenger等聊天应用中的搜索工具。当聊天记录太长时,你可能使用搜索工具来查找旧消息,而不是滚动,因为消息太多无法滚动浏览。
在这个例子中,用户提到“Timber bit me”。因此,智能体使用搜索工具尝试查找与Timber相关的旧消息。搜索工具在数据库中执行,并将结果返回到聊天历史中。根据搜索结果,智能体能够推断出Timber是一只狗,并且Timber之前咬过用户。😊 智能体可以使用这些信息来制作一个引人入胜的回复,说:“I can’t believe your dog bit you again.”
核心记忆的限制与归档记忆 🗄️
核心记忆的大小也是有限的,类似于聊天历史。核心记忆的每个部分都有一个相关的字符限制。在这里,我们可以看到用户和智能体字段都有2000个字符的限制。作为开发者,你可以将此限制更改为你想要的任何值。基本的约束是,组合的系统提示、核心记忆、摘要和聊天历史必须全部一起适应你正在使用的基础LLM的上下文窗口。
你可能想知道,如果智能体在核心记忆中空间不足会发生什么?例如,这里用户表达了一个新事实,智能体希望将其保存到核心记忆的用户部分,但该部分空间不足。别担心,类似于聊天历史有一个称为召回记忆的无限二级存储,核心记忆也有一个无限的二级存储。我们称之为归档记忆。
MemGPT智能体决定哪些信息最重要,需要保留在核心记忆中,哪些应该存储在上下文窗口之外的归档记忆中。在这个例子中,智能体认为该信息不够重要,不足以放入核心记忆。因此,它将信息放入归档记忆中。你可以想象一个场景,智能体可能认为该信息足够重要,可以放入核心记忆。在那种情况下,智能体会首先通过将信息移入归档记忆来从核心记忆中逐出信息。😊 然后,在释放空间后,它会将其添加到核心记忆中。
你可以将归档记忆视为MemGPT智能体的通用数据存储。它是所有不够重要、无法始终固定在上下文窗口内的核心记忆中的一般信息。因为归档记忆作为一个通用概念,意味着你可以将其用于许多不同的事情。例如,你甚至可以使用归档记忆来存储代码或PDF文档。😊
在这个例子中,用户正在询问关于公司手册的问题。公司手册太大,无法存储在核心记忆中。因此,智能体决定将其放入归档记忆中。为了回答用户的问题,智能体将首先搜索归档记忆。智能体从手册中得到一个结果:“The company handbook says that the user can take unlimited vacation days. Good news, it’s unlimited!”
外部记忆统计与信息检索 🔍
因为归档记忆和召回记忆是外部存储,智能体实际上无法看到它们的内容,除非它明确使用其中一个搜索工具来获取信息。如果宝贵的信息存储在外部存储中,但所有外部存储都在上下文窗口之外,这就产生了一个问题:智能体首先如何知道去哪里寻找?
这就是外部记忆统计发挥作用的地方。在MemGPT中,上下文窗口有一个特殊部分,提供关于外部存储中内容的统计信息。例如,如果用户问了一个没有被核心记忆或聊天历史明确定义的问题,如果智能体看到有很多外部记忆,它将首先检查外部记忆,看看是否有相关信息。这里,记忆统计显示归档和召回都有数百个条目,因此当用户测试智能体关于他们最喜欢的狗时,智能体首先寻找相关数据并找到它。作为反例,如果记忆统计显示外部存储中没有数据,那么MemGPT智能体就根本不需要搜索外部存储,可以直接回复。

总结 📝

让我们总结一下到目前为止学到的内容。在MemGPT中,记忆有两个通用层级:在上下文窗口内的记忆和不在上下文窗口内的记忆。
在上下文之外的记忆中,我们区分两种类型的记忆:召回记忆,指的是消息历史;以及归档记忆,这是一个通用数据存储。
智能体由智能体状态组成,这包括智能体的记忆、工具和完整消息。在MemGPT中,这个智能体状态存储在数据库中,因此你的智能体可以永久持久存在。当我们运行LLM推理时,我们必须将这个智能体状态转换为提示,我们称这一步为上下文编译。

恭喜你,现在你知道了构建一个基本LLM操作系统所需的技术,它可以赋予你的LLM智能体长期持久记忆和高级多步推理能力。你学到的MemGPT背后的关键概念将使你能够构建需要智能体能够随时间记忆和学习的应用程序。在本课程的其余部分,你将学习如何将这些基础概念提升到新的水平,构建更复杂的记忆系统,以及编排多智能体系统,其中每个智能体都有自己的长期记忆系统。
004:4. 构建具有记忆的智能体 🧠
在本节课中,我们将学习如何使用 Lettra 框架创建和与 MGPT 智能体进行交互。我们还将深入了解智能体的状态,包括系统提示、工具和记忆。最后,我们会学习如何查看和编辑智能体的归档记忆。


智能体状态与记忆概览
你可以使用 Lettra 来创建 MGPT 智能体。MGPT 智能体是有状态的,并且能显式地管理其上下文窗口中的特定部分。
在本节中,我们将介绍智能体状态的不同部分,以及归档记忆和回忆记忆。我们还将探讨如何通过控制以下几个“旋钮”来设计智能体:
- 提示:包括系统提示和定义智能体行为的人设。
- 智能体的工具。
- 智能体管理和组织记忆的方式。
- 智能体记忆的内容,包括核心记忆和归档记忆。
这些“旋钮”定义了在每一步中放入 LLM 上下文的内容,从而决定了智能体的行为。
导入辅助函数与设置客户端
首先,我们将导入一个辅助函数,它能让 MGPT 的响应打印输出更易读。
from utils import print_messages
接下来,创建一个 Lettra 客户端。这个客户端也可以连接到 Lettra 服务器,但在这个例子中,我们将使用一个本地 Lettra 客户端的示例,它将在本地运行智能体推理。
from lettra import LettraClient
client = LettraClient()
我们还将为此客户端设置默认配置,在本实验中我们使用 gpt-4o-mini 模型。
client.set_default_model("gpt-4o-mini")
创建基础 MGPT 智能体
现在,让我们开始使用 Lettra 创建一个基础的 MGPT 智能体。
我们将这个智能体命名为 simple_agent,你也可以将其更改为你喜欢的任何名称。
首先,调用客户端的 create_agent 函数来创建智能体。这需要传入一个智能体名称,以及一个 ChatMemory 类的实例。
ChatMemory 类基本上代表了我们之前学到的核心记忆。在这里,我们传入一个 human 字符串来代表“人类”部分的初始核心记忆,以及一个 persona 字符串来代表“人设”部分的初始核心记忆。
- 对于
human部分,我输入了“我的名字是 Sarah”,但你可以将其更改为你自己的名字,或者包含关于你自己的额外信息。 - 对于
persona部分,我们告诉智能体:“你是一个乐于助人的助手,喜欢使用表情符号。” 这将定义智能体的个性,但由于它在记忆部分,所以仍然是可编辑的。
from lettra import ChatMemory

agent_state = client.create_agent(
agent_name="simple_agent",
memory=ChatMemory(
human="我的名字是 Sarah",
persona="你是一个乐于助人的助手,喜欢使用表情符号。"
)
)
创建智能体后,我们现在可以向它发送消息。我们先发送一个非常简单的消息:“你好”。
response = client.step(agent_state.agent_id, "你好")
这个响应会生成两个部分:使用情况统计和智能体返回的实际消息。
usage统计对象显示生成响应所使用的完成令牌和提示令牌的数量。- 我们可以使用导入的
print_messages函数来打印响应消息。
print_messages(response.messages)
你会注意到,智能体生成了一个解释其行为的内部独白。你可以利用这个独白来理解智能体为何如此行事。这个独白也有助于智能体花更多时间思考,以生成更好的回应。
我们还可以看到,MGPT 智能体实际上是在使用一个工具进行通信:通过 send_message 工具将消息发送回用户。这实际上允许智能体通过不同的媒介进行通信(例如,如果你想让它生成短信)。同时,这也允许智能体区分哪些信息发送给最终用户(即消息内容),哪些信息留给自己(如内部独白)。

理解智能体状态的不同部分
智能体状态是我们创建智能体时返回的对象。它包含很多内容,但我们将逐步分解。
1. 系统提示
第一部分是智能体系统提示。它也定义了智能体的行为,类似于人设,但智能体无法编辑它。这是一个我们设计的非常长的系统提示,旨在让 MGPT 发挥最佳性能。
它包含诸如尝试覆盖智能体创建者信息(因为许多 LLM 有自己的系统提示,会说“我是由 OpenAI 或 Anthropic 创建的”)、控制流信息、不同用户事件、基本功能(如内部思考、发送消息)的描述,以及关于记忆编辑(应如何进行及其指令)、回忆记忆、对话历史、核心记忆及其不同区块、归档记忆等的描述。
这是一个非常长的系统提示,有时为了真正优化智能体的行为,编辑系统提示可能很重要。
print(agent_state.system)
2. 工具列表
智能体状态的另一个部分是智能体可以访问的工具列表。默认工具包括:
- 发送消息 (
send_message) - 暂停心跳以停止智能体循环 (
pause_heartbeat) - 搜索对话历史 (
conversation_search) - 插入到归档记忆 (
archival_memory_insert) - 搜索归档记忆 (
archival_memory_search) - 追加和替换核心记忆 (
core_memory_append,core_memory_replace)
通过共同使用这些工具,MGPT 智能体能够控制它们的记忆。
print(agent_state.tools)
3. 核心记忆
我们还可以查看智能体核心记忆中的内容,通过访问智能体状态中的 memory 字段来实现。
print(agent_state.memory)
这将返回一个内存对象,其中包含多个区块。我们将在后面的课程中更详细地介绍这些区块的具体含义。
查看归档记忆与回忆记忆
除了智能体状态,我们还可以使用智能体的 ID 来获取其归档记忆的摘要。
目前,我们还没有向归档记忆中放入任何内容,智能体也没有,所以它是空的。
archival_memory = client.get_archival_memory(agent_state.agent_id)
print(archival_memory)
同样,我们也可以获取回忆记忆的摘要。因为我们已经与智能体交换了几条消息,所以回忆记忆中已经有一些消息了。
recall_memory = client.get_recall_memory(agent_state.agent_id, count=10)
print(recall_memory)
我们还可以获取智能体的原始消息历史记录,查看其中的单个消息,以准确理解智能体的执行轨迹。

message_history = client.get_message_history(agent_state.agent_id)
print(message_history)
编辑核心记忆
核心记忆是智能体的上下文内记忆。MGPT 的独特之处在于,它实际上可以使用工具编辑自己的核心记忆。

以下是一个示例:我们可以使用智能体的 ID 向其发送一条消息,告诉它“我的名字实际上是 Bob”,尽管我之前告诉它我的名字是 Sarah。或者,你也可以让智能体添加你最初未包含在 human 字符串中的关于你的额外信息。
response = client.step(agent_state.agent_id, "我的名字实际上是 Bob。")
print_messages(response.messages)
智能体应该更新记忆以反映这一点。你可以看到它调用了 core_memory_replace 函数,知道需要更新记忆的 human 部分,将内容从“我的名字是 Sarah”替换为“我的名字是 Bob”。完成此函数调用后,它意识到已成功更新记忆,然后知道要发送给用户的消息:“明白了,Bob。我们今天聊点什么?” 当然,还包含一个表情符号。
人设记忆部分和系统提示在定义我们希望智能体表现出的行为方面非常相似。这意味着我们实际上可以让智能体编辑其关于自己应该做什么的记忆。
到目前为止,我们有一个非常友好的智能体,在其消息中使用了很多表情符号。但在我们给出以下反馈后,我们期望它在未来可能发送的任何消息中永远不再使用表情符号。

response = client.step(agent_state.agent_id, "请不要在消息中使用表情符号。")
print_messages(response.messages)
我们可以看到,智能体意识到用户更喜欢在以后的互动中不再使用表情符号。与上一个例子类似,我们看到它调用了 core_memory_replace,根据 Bob 的偏好,将“喜欢使用表情符号”替换为“不使用表情符号”。最后,它发送了这条消息:“明白了,Bob。以后不会再使用表情符号了。”
这真的很酷,因为我们可以随着时间的推移调整智能体的行为。当人类用户向智能体提供反馈时,智能体实际上可以利用这些反馈进行改进。这些反馈也将包含在将来所有发送给 LLM 的消息中,因为它位于每次请求都会发送的记忆中。
我们可以使用智能体 ID 检索智能体的当前记忆,然后获取 persona 区块,以查看智能体记忆中字符串的确切值。
current_memory = client.get_memory(agent_state.agent_id)
print(current_memory.persona) # 输出:你是一个乐于助人的助手,不使用表情符号。
这基本上反映了我们之前看到的变化。

深入归档记忆
MGPT 智能体拥有长期记忆,即归档记忆,它将数据持久化到外部数据库中。这意味着智能体可以在其大小受限的上下文窗口之外,拥有额外的空间来写入信息。
我们可以通过调用 get_archival_memory 来查看智能体归档记忆中的内容。目前它是空的。智能体会根据需要随时间添加归档记忆,但我们也可以明确建议智能体应该向归档记忆中添加一些内容。
在这里,我们告诉智能体:“将信息‘Bob 爱猫’保存到归档记忆中。” 你也可以将字符串“Bob 爱猫”更改为与你自己相关的特定内容。
response = client.step(agent_state.agent_id, "请将信息‘Bob 爱猫’保存到归档记忆中。")
print_messages(response.messages)
与前面的例子一样,智能体再次进行了内部独白,意识到需要调用 archival_memory_insert 工具,并这样做了,插入了“Bob 爱猫”的数据。完成后,它向用户确认已进行了此调整,并跟进了一些相关的对话:“你喜欢什么样的猫?”
现在,如果我们获取归档记忆,应该会看到里面有一些内容。

archival_memory = client.get_archival_memory(agent_state.agent_id)
print(archival_memory)
我们也可以只获取文本。
archival_memory_text = client.get_archival_memory_text(agent_state.agent_id)
print(archival_memory_text)
现在我们看到,归档记忆中的第一行文本是“Bob 爱猫”,数据已成功添加。

手动操作归档记忆与基于记忆的响应
我们刚刚看到了智能体如何插入到其归档记忆中的示例。但作为用户,我们也可以手动将记忆插入到智能体的归档记忆中。
使用智能体 ID,我们可以插入“Bob 爱波士顿梗犬”到其归档记忆中。
inserted_passage = client.insert_archival_memory(agent_state.agent_id, "Bob 爱波士顿梗犬。")
print(inserted_passage)
这将返回插入的段落,包含文本、使用的嵌入信息、添加日期和其他一些内容。
现在,智能体的归档记忆中应该有两个记忆。我们可以尝试问:“我喜欢什么样的动物?”
response = client.step(agent_state.agent_id, "我喜欢什么样的动物?")
print_messages(response.messages)
根据归档记忆搜索的结果,智能体进行了一些关于根据这些结果继续对话的内部独白,然后发回消息:“你爱猫和波士顿梗犬。” 接着它还试图通过提问“你更喜欢哪一个呢?”来保持对话的吸引力。
这是一个示例,展示了智能体如何利用其归档记忆中的内容,向用户生成信息更丰富的回应。


总结 🎉
在本节课中,我们一起学习了如何构建一个具有记忆的 MGPT 智能体。
- 创建智能体:我们使用 Lettra 框架和
ChatMemory初始化了一个基础的 MGPT 智能体,并设置了其初始人设和用户信息。 - 理解智能体状态:我们剖析了智能体状态的关键组成部分,包括不可编辑的系统提示、可用的工具集以及可编辑的核心记忆。
- 与记忆交互:
- 核心记忆:我们实践了如何通过对话让智能体动态编辑其核心记忆(如更新用户姓名、修改行为偏好),使其行为能适应用户反馈。
- 归档记忆:我们探索了智能体的长期存储。我们既指导智能体自动添加重要信息到归档记忆,也学会了如何手动插入记忆。最后,我们看到了智能体如何检索并利用归档记忆中的信息来生成更准确、更相关的回答。

恭喜!你现在已经能够创建一个具备自我管理记忆能力的 MGPT 智能体了。在未来的课程中,我们还将介绍如何为核心记忆实现更高级的功能,以及如何扩展 MGPT 智能体的 RAG(检索增强生成)能力。
005:编程智能体记忆 🧠


在本节课中,我们将深入学习核心记忆的设计与实现方式。我们将通过一个具体示例,展示如何通过自定义记忆块和工具来定制核心记忆。
概述
上一节我们介绍了智能体记忆的基本概念。本节中,我们将深入探讨核心记忆的具体构成,并学习如何通过编程方式扩展其功能,使其能更好地服务于特定应用场景。
核心记忆的构成
核心记忆由记忆块和记忆工具共同定义。在上下文窗口内,核心记忆被划分为多个块,每个块对应一个字符限制,这决定了该块能占用多少上下文窗口空间。
每个记忆块都有一个标签(例如“human”或“persona”),用于引用该块。最后,每个块都有一个值,这是实际放入上下文窗口的数据。例如,一个“human”块的值可能是“名字是Sarah”。
除了这些块,核心记忆还关联了用于操作记忆的工具。例如,core_memory_replace工具可以指定要替换的块标签(如“human”)、旧内容(“Sarah”)和新内容(“Bob”)。
在推理时,数据被编译到上下文窗口中,构成核心记忆的上下文。例如,对于“human”块,编译后的字符串会显示“human”标签、已使用的字符数以及块的实际值。
记忆块会被同步到数据库,并拥有唯一ID,因此它们可以通过将块值同步到多个智能体的上下文窗口,实现跨智能体共享。
探索默认记忆类
为了开始实践,我们将导入与上节课相同的辅助函数,并创建我们的LiteLLM客户端。我们将确保在本实验中使用GPT-4o-mini模型。
之前创建智能体时,我们使用了ChatMemory类。现在,我们将深入了解这个类。创建这个记忆类后,底层实际上会创建多个记忆块。我们可以通过列出ChatMemory中的块名称来查看这一点。
以下是查看记忆块的方法:
- 我们可以列出
chat_memory中的块名称,例如“persona”和“human”。 - 我们可以通过
get_block(‘human’)查看chat_memory中的实际块。该块包含当前存储在其中的值、字符限制以及块的名称。
ChatMemory类还包含两个用于编辑记忆的默认函数:core_memory_append和core_memory_replace。我们可以查看这些函数的源代码。
core_memory_append函数在创建带有ChatMemory记忆类的智能体时,会作为一个工具被添加到智能体中。该函数接收要编辑的记忆块名称以及要追加的内容。它还需要一个文档字符串,用于向智能体描述该工具的用途、需要提供的参数以及预期的响应。
该函数的执行逻辑是:获取块的值,将数据追加到该值,然后在智能体的记忆中更新块的值。
我们还可以查看ChatMemory使用的提示模板。这个模板定义了ChatMemory应如何被编译到上下文窗口中。在编译字符串中,对于每个块,我们都会显示块名称、已使用的字符数以及块的实际值。这就是放入LLM上下文窗口核心记忆部分的内容。
自定义记忆类
智能体所使用的记忆类可以根据不同的应用程序进行定制。你可以通过以下方式自定义记忆:
- 定义自定义块:这些是除了“human”和“persona”之外,可能专属于你的智能体的额外块。
- 定义自定义记忆工具:除了
core_memory_append或core_memory_replace,你可以定义不同的或额外的工具来编辑块。 - 编辑记忆模板:改变
memory.compile将记忆表示为字符串格式的方式。
在本实验中,我们将实现一个自定义记忆类:TaskQueueMemory。TaskQueueMemory将扩展ChatMemory,使其不仅拥有“human”和“persona”块,还拥有一个“tasks”块,用于跟踪智能体当前应处理的任务。我们还将添加两个额外的自定义记忆工具:task_queue_push(将新任务推送到任务队列)和task_queue_pop。
首先,我们导入ChatMemory和Block类。然后定义TaskQueueMemory的初始化函数。TaskQueueMemory将简单地扩展ChatMemory。初始化函数将接收与ChatMemory相同的“human”和“persona”字符串,同时接收一个任务列表。对于“human”和“persona”,我们将参数传递给父类。对于“tasks”,我们将创建一个名为“tasks”的新块,设置2000个字符的限制,并将任务列表JSON序列化后作为块的值。
接下来,我们将定义两个自定义的记忆编辑函数。第一个是task_queue_push,用于推送任务描述。请注意,这里的self参数实际上是agent,而不是TaskQueueMemory。这有点令人困惑,但当这个工具实际执行时,该函数会被附加到智能体类上,以便它能访问其记忆。该函数将通过将任务描述追加到任务列表中,来更新存储在核心记忆中的任务队列。
我们使用json.loads和json.dumps是因为我们只支持字符串类型,所以必须确保以字符串格式存储块的值。
我们还将定义task_queue_pop函数。该函数将从任务队列中获取下一个任务,打印该任务,并更新当前块的值,移除刚刚弹出的任务。
创建并使用自定义记忆智能体
现在,我们可以创建一个拥有这个自定义TaskQueueMemory类的智能体。在系统提示词之外,我们还传入一个TaskQueueMemory类的实例。我们传入“human”信息(例如“我的名字是Sarah”,但你应该更新为关于你自己的信息)、“persona”信息(“你是一个必须清空其任务的智能体”),以及一个空的任务列表。
我们希望这个智能体向其任务队列添加任务。我们可以发送这样的消息:“首先,开始叫我Charles,并告诉我一个关于我名字的俳句,作为两个独立的任务。”
我们将此消息发送给任务智能体。除了实际的响应打印,我们还可以看到服务器打印出了当前任务,因为我们已将打印语句添加到了task_queue_pop函数中。
回顾这个过程,我们首先看到智能体的内部独白:它意识到应该向任务队列添加任务。因此,它首先对“开始叫我Charles”调用task_queue_push,然后对“告诉我一个关于名字Charles的俳句”也调用task_queue_push。
我们在系统提示词和“persona”中都指定,这个智能体应始终确保任务队列为空。现在它已经向任务队列添加了两个任务,它可以在其核心记忆中看到有待完成的任务。因此,它没有立即响应用户,而是开始从任务队列中弹出这些任务。

它调用一次task_queue_pop,获取任务“开始叫我Charles”,然后执行这个任务(调用core_memory_append)。接着,它再次看到记忆中还有更多任务,于是第二次调用task_queue_pop,获取第二个任务“告诉我一个关于名字Charles的俳句”。最后,它创作了俳句。
此时,它看到任务队列在记忆中已为空,因此知道终于可以向用户发送回复消息了,其中包含关于名字Charles的俳句。
这是一个很长的序列,但如果你正在学习本笔记本,我鼓励你仔细查看每个步骤,以真正理解智能体在每一步是如何做出决策的。这也希望向你展示,这些模型在进行多步推理时有多么强大——我们能够运行大量步骤来完成相当复杂的任务。
有可能你没有得到和我完全相同的结果,你的智能体可能“偷懒”了,没有在应该的时候执行所有任务。如果发生这种情况,我建议你提示智能体去实际完成它的任务。

最后,我们可以通过查看其核心记忆并获取“tasks”块,来确认智能体是否正确清空了整个任务队列。调用后返回的块确实是空的,这意味着我们的智能体正确地清除了它的任务。
记忆的持久化
最后我想提到的是,正如幻灯片中讨论的,所有这些块实际上都持久化到了数据库中。我们可以调用client.get_block()并粘贴块ID。这将返回完全相同的块,因为它被持久化在数据库中。因此,即使你在不同的服务器上,甚至从不同的智能体或笔记本访问,你仍然可以访问相同的块数据。
总结

本节课中,我们一起学习了核心记忆的详细构成,并动手实现了一个自定义记忆类TaskQueueMemory。你现在已经实现了一个可以控制智能体管理记忆方式的自定义记忆类。你可以利用这个知识,来实现更高级的智能体,以特定于你自己应用程序的方式来控制上下文。
006:6.L5 Agentic RAG 与外部内存 🧠


在本节课中,我们将解释外部内存与RAG(检索增强生成)之间的关系,并通过两种方式实现Agentic RAG。首先,我们将数据直接复制到智能体的归档内存中;其次,我们将为智能体提供一个LangChain工具来查询网络。我们将构建一个研究智能体来演示这些概念。
概述
上一节我们介绍了智能体如何使用外部内存。本节中,我们来看看如何利用这些内存来实现更智能的检索增强生成(Agentic RAG)。与传统的RAG不同,Agentic RAG允许智能体自主决定何时以及如何检索数据,例如,它能够自行决定向搜索功能发送什么样的查询。
实现方式一:加载数据到归档内存
首先,我们将学习如何以用户身份手动将数据加载到智能体的归档内存中。
以下是实现步骤:
-
导入必要的库并设置环境:我们需要导入常用工具并设置环境变量,以便使用Tavily搜索。同时创建LiteLLM客户端并设置默认语言模型。
import os from litellm import LiteLLM # 设置环境变量和客户端 client = LiteLLM() client.set_default_model('gpt-4o-mini') -
创建数据源:使用
client.create_source函数创建一个数据源,例如命名为“员工手册”。数据源会配置一个嵌入模型,用于为源内数据生成向量。source = client.create_source(name="employee handbook") -
加载文件到数据源:将一个文件(如
handbook.pdf)加载到刚创建的数据源中。数据会被预处理(分块并生成嵌入)。client.load_into_source(source_id=source.id, file_path="handbook.pdf") -
创建智能体并附加数据源:创建一个基础的MCPT智能体,然后将数据源附加到该智能体。附加操作会将源中的数据复制到智能体的归档内存中。
agent = client.create_agent(name="HR Assistant") client.attach_source(agent_id=agent.id, source_id=source.id)

- 进行查询:现在可以向智能体提问,例如询问公司休假政策。智能体会意识到需要从归档内存中搜索信息,并自动执行检索。
智能体的内部思考过程会决定查询词(如“vacation policies”),检索相关数据片段,并基于检索到的信息生成回答。response = agent.chat("搜索归档内存:公司的休假政策是什么?") print(response)
实现方式二:通过自定义工具连接外部数据


除了使用归档内存,我们还可以为智能体添加自定义工具来直接查询外部数据管道。
以下是具体方法:
-
创建自定义查询函数:例如,我们创建一个模拟的生日数据库查询函数。
birthday_db = {"Andrew": "March 6, 1997"} def query_birthday_database(name: str) -> str: """根据姓名查找生日。""" return birthday_db.get(name, "未找到该姓名") -
将函数注册为工具:使用
client.create_tool函数,LiteLLM会自动解析函数的文档字符串,为其生成OpenAI JSON格式的模式定义。birthday_tool = client.create_tool(func=query_birthday_database) -
创建能使用该工具的智能体:在创建智能体时,指定它可以使用的工具,并为其设定相应的角色描述。
persona = "你是一个可以访问生日数据库的助手,可以帮用户查询生日信息。" birthday_agent = client.create_agent( name="Birthday Agent", tools=[birthday_tool], memory_config=ChatMemory(persona=persona) )

- 进行查询:当用户询问“我的生日是什么时候?”时,智能体会决定调用
query_birthday_database工具,获取结果后生成回复。response = birthday_agent.chat("我的生日是什么时候?") print(response)

实现方式三:集成LangChain工具进行网络搜索
LiteLLM支持集成LangChain和LlamaIndex的工具。接下来,我们创建一个能进行网络搜索并引用来源的研究智能体。
以下是操作步骤:
-
设置Tavily搜索工具:导入Tavily API密钥和LangChain的搜索工具。
from langchain_community.tools import TavilySearchResults os.environ["TAVILY_API_KEY"] = "your_api_key" search_tool = TavilySearchResults() -
将LangChain工具转换为LiteLLM工具:使用
Tool.from_langchain方法进行转换,并持久化该工具。from litellm.tools import Tool lite_tool = Tool.from_langchain(search_tool) lite_tool.persist() -
创建研究智能体:定义智能体的角色,指示它使用搜索工具并引用来源。
research_persona = """ 你是一个研究助手。你可以使用名为‘tavily_search’的工具来搜索网络以回答问题。 在你的回答中,请务必提供引用来源的链接。 """ research_agent = client.create_agent( name="Research Agent", tools=[lite_tool], memory_config=ChatMemory(persona=research_persona) )


- 进行查询:向智能体提问,例如“谁创立了OpenAI?”。智能体会调用搜索工具,获取网络信息,并生成带有引用的回答。LiteLLM框架会自动处理过长的工具返回结果。
response = research_agent.chat("谁创立了OpenAI?") print(response)


- (可选)使用更强大的模型:如果默认模型(如GPT-4o-mini)效果不佳,可以指定使用GPT-4等模型,但需注意其成本和延迟更高。
from litellm import LLMConfig gpt4_config = LLMConfig(model="gpt-4") gpt4_agent = client.create_agent( name="GPT-4 Search Agent", tools=[lite_tool], llm_config=gpt4_config, memory_config=ChatMemory(persona=research_persona) )
总结
本节课中,我们一起学习了Agentic RAG与外部内存的三种集成方式:


- 数据加载到归档内存:用户可以将文件数据预加载到智能体的归档内存中,智能体在需要时自动检索。
- 自定义工具连接数据库:通过创建自定义函数工具,智能体可以直接查询外部数据库或API。
- 集成现有框架工具:利用LiteLLM对LangChain等框架工具的支持,可以快速为智能体赋予网络搜索等强大能力,同时享受MCPT智能体的记忆功能。

这些方法提供了灵活性,允许你根据已有的数据管道和工具栈,构建出功能丰富、能够自主决策检索行为的智能体。
007:多智能体编排实验

在本节课中,我们将学习如何在Let框架中实现多智能体协作。我们将通过使用工具进行跨智能体通信以及共享内存块,来构建一个模拟的招聘工作流。这个工作流将包含三个不同的智能体:招聘专员、评估专员和外联专员。


概述
在Let框架中,智能体被设计为以服务的形式运行,以便真实的应用可以通过REST API与它们通信。例如,一个使用聊天机器人的移动应用可以向由数据库支持的Let服务器发送REST API请求,然后服务器返回响应供移动应用使用。
那么,当智能体作为独立服务运行时,我们如何实现它们之间的协调与通信呢?一种解决方案是让智能体之间互相发送消息。另一种方案是使用共享内存块,即在一个共享的持久化数据存储中放置内存块,让不同服务中的智能体同步这些内存块。
在本实验中,我们将通过一个包含三个智能体的招聘工作流,来实践这两种协作方式。
实验设置
首先,我们需要进行基础设置,包括导入必要的库和创建Let客户端。
# 导入必要的库并创建Let客户端
import notebook_print
from letter import LetterClient
client = LetterClient()
client.update_lm('gpt-4o-mini')
创建共享组织内存
为了实现多智能体协作,我们需要让智能体既拥有自己的记忆,也共享一部分记忆。共享内存将包含所有智能体所属组织的信息。关键之处在于,当一个智能体更新了共享内存块时,这个变更需要传播到所有其他智能体的记忆中。
我们将创建一个名为“公司块”的共享内存块。
# 初始化组织描述
organization_description = "公司名为AgentOS,正在构建AI工具,以便更轻松地创建和部署LLM智能体。"
# 创建一个共享内存块
company_block = client.create_block(
block_name="company_info", # 在编译到上下文时使用的标签
value=organization_description
)
为了便于使用,我们将创建一个自定义的记忆对象。这个对象将继承一个基础的块记忆类,并同时包含私有的“角色”块和共享的“组织”块。
from letter.memory import BasicBlockMemory
class OrgMemory(BasicBlockMemory):
def __init__(self, persona_string, org_block):
# 创建私有角色块
persona_block = client.create_block(block_name="persona", value=persona_string)
# 初始化父类,传入包含私有块和共享块的列表
super().__init__(blocks=[persona_block, org_block])
创建智能体
现在,我们将创建三个智能体:评估专员、外联专员和招聘专员。
- 评估专员:负责根据简历评估候选人。
- 外联专员:负责向合适的候选人撰写并发送邮件。
- 招聘专员:负责从数据库中生成潜在候选人名单,并将名单传递给其他专员。
就像人类一样,这些智能体将通过互相发送消息进行沟通。我们通过赋予智能体能够向其他智能体发送消息的工具来实现这一点。
1. 评估专员
评估专员将拥有两个工具:
- 读取简历工具:查找并读取指定文件路径的简历。
- 提交评估工具:将评估结果(是否联系、简历数据、理由)发送给外联专员。
提交评估工具将使用Let客户端向外联专员发送消息。
# 定义工具函数
def read_resume(file_path):
# 从数据文件夹读取简历文件
with open(f"./data/{file_path}", 'r') as f:
return f.read()
def submit_evaluation(candidate_name, should_reach_out, resume_data, justification):
if should_reach_out:
# 向外联专员发送消息
message = f"请联系候选人 {candidate_name}。简历摘要:{resume_data[:200]}... 理由:{justification}"
client.send_message(agent_id=outreach_agent.id, content=message)
print(f"已提交候选人 {candidate_name} 供外联。")
else:
print(f"决定不联系 {candidate_name}。理由:{justification}")
# 使用客户端创建工具
read_resume_tool = client.create_tool(read_resume)
submit_eval_tool = client.create_tool(submit_evaluation)
# 定义评估专员的角色描述
eval_persona = """
你是一名评估专员。你的技能包括:分析技术简历、评估与职位的匹配度、做出数据驱动的决策。
你的职责是评估候选人,并使用‘提交评估’工具将结果发送给外联团队。
"""
# 创建评估专员智能体
eval_agent = client.create_agent(
name="Evaluator",
persona=eval_persona,
memory=OrgMemory(eval_persona, company_block), # 使用包含共享块的自定义记忆
tools=[read_resume_tool, submit_eval_tool]
)
2. 外联专员
外联专员负责向候选人发送定制化邮件。为了简化,我们这里只是打印出邮件内容。

def email_candidate(candidate_name, email_body):
# 模拟发送邮件,实际只是打印
print(f"\n--- 发送给 {candidate_name} 的邮件 ---\n")
print(email_body)
print(f"\n--- 邮件结束 ---\n")
# 创建邮件工具
email_tool = client.create_tool(email_candidate)


# 定义外联专员的角色和邮件模板
outreach_persona = """
你是一名外联专员。你负责向评估通过的候选人发送个性化的外联邮件。
请使用以下模板,并融入公司和候选人的具体信息:
【邮件模板】
主题:关于您在[公司名]的潜在机会
尊敬的[候选人姓名],
我们在[公司名]注意到您在[候选人技能/领域]方面的杰出经验。我们正在[公司当前业务描述],相信您的背景会非常契合。
我们期待与您进一步交流。
诚挚问候,
[公司名] 招聘团队
"""
# 创建外联专员智能体
outreach_agent = client.create_agent(
name="Outreach",
persona=outreach_persona,
memory=OrgMemory(outreach_persona, company_block), # 同样使用共享组织记忆
tools=[email_tool]
)

现在,我们有了两个通过消息发送和共享组织记忆连接起来的智能体。

测试智能体协作
让我们通过用户直接与评估专员对话来启动流程。评估专员拥有触发其他智能体的能力,因为它可以向其他智能体发送消息。

# 用户告诉评估专员评估候选人“Tony Stark”
user_message = "请评估候选人 Tony Stark。他的简历文件是 ‘tony_stark_resume.txt‘。"
response = client.send_message(agent_id=eval_agent.id, content=user_message)
print(response)
执行流程如下:
- 评估专员调用
读取简历工具,获取Tony Stark的简历内容。 - 评估专员分析简历,认为这是一个合适的人选。
- 评估专员调用
提交评估工具,其中should_reach_out=True。 提交评估工具内部会向外联专员发送一条消息,包含候选人姓名、简历摘要和评估理由。- 外联专员收到消息后,调用其
发送邮件工具,生成并“发送”(打印)一封个性化的邮件。邮件中会引用共享内存中的公司信息(此时是“AgentOS”)。
值得注意的是,评估专员本身并没有发送邮件的工具。邮件是由外联专员在收到评估专员的消息后发送的。这展示了智能体间的任务分工与协作。
测试共享内存的更新与同步
共享内存的一个强大特性是更新同步。我们可以向其中一个智能体提供反馈,更新共享的公司信息,并观察这个更新是否自动同步到其他智能体。
例如,我们告诉评估专员公司信息发生了变更。
update_message = """
我们公司的业务方向已转向基础模型训练。此外,公司已更名为 Foundation AI。
请更新你的公司记忆部分。
"""
response = client.send_message(agent_id=eval_agent.id, content=update_message)
print(response)
评估专员会更新其核心记忆中“公司信息”块的内容。由于这是一个共享内存块,外联专员记忆中的同一部分也应该被更新。
为了验证这一点,我们让评估专员评估另一个候选人。
new_eval_message = "请评估候选人 Spongebob Squarepants。他的简历文件是 ‘spongebob_resume.txt‘。"
response = client.send_message(agent_id=eval_agent.id, content=new_eval_message)
print(response)
如果外联专员随后为Spongebob生成邮件,我们应当看到邮件中引用了新的公司名“Foundation AI”和新的业务方向“基础模型训练”,尽管我们从未直接告诉外联专员这些信息。这证明了共享内存块的同步是有效的。
引入第三个智能体:招聘专员
到目前为止,我们都是手动触发评估专员。现在,我们引入招聘专员来自动化生成候选人名单并启动评估流程。
首先,我们重置并重新创建评估和外联专员(以确保使用最新的共享内存块)。然后创建招聘专员。
招聘专员将拥有两个工具:
- 搜索候选人数据库:模拟从数据库分页获取候选人名单(例如:Tony Stark, Spongebob Squarepants, Galtung Vang)。
- 考虑候选人:这个工具会创建一个Let客户端,并向评估专员发送消息,触发其对指定候选人的评估流程。
# 模拟的候选人数据库
candidate_database = {
0: ["Tony Stark"],
1: ["Spongebob Squarepants"],
2: ["Galtung Vang"],
3: [] # 空页表示结束
}

def search_candidate_db(page_number):
return candidate_database.get(page_number, [])
def consider_candidate(candidate_name):
# 自动化之前手动执行的步骤:向评估专员发送消息
message = f"请评估候选人 {candidate_name}。他的简历文件是 ‘{candidate_name.lower().replace(‘ ‘, ‘_‘)}_resume.txt‘。"
client.send_message(agent_id=eval_agent.id, content=message)
print(f"招聘专员已提交候选人 {candidate_name} 供评估。")
# 创建工具
search_tool = client.create_tool(search_candidate_db)
consider_tool = client.create_tool(consider_candidate)
# 定义招聘专员的角色
recruiter_persona = """
你是一名招聘专员。你的任务是从候选人数据库中持续提取候选人,直到没有更多候选人为止。
对于每个提取到的候选人,请调用‘考虑候选人’工具将其提交给评估团队。
请持续轮询数据库,直到返回空页。
"""
# 创建招聘专员智能体
recruiter_agent = client.create_agent(
name="Recruiter",
persona=recruiter_persona,
memory=OrgMemory(recruiter_persona, company_block), # 连接到同一个共享组织块
tools=[search_tool, consider_tool]
)
现在,我们只需要启动招聘专员,它就会自动运行整个多智能体工作流。
# 启动招聘专员
start_message = "开始从候选人数据库中寻找并评估候选人。"
response = client.send_message(agent_id=recruiter_agent.id, content=start_message)
print(response)
工作流执行过程:
- 招聘专员调用
搜索数据库工具,获取第一页候选人(Tony Stark)。 - 招聘专员为第一个候选人调用
考虑候选人工具。 考虑候选人工具向评估专员发送消息。- 评估专员和外联专员协作,完成对Tony Stark的评估和邮件发送(如果合适)。
- 招聘专员继续获取第二页(Spongebob)、第三页(Galtung)的候选人,并重复步骤2-4。
- 当招聘专员获取到空页(第3页)时,它意识到所有候选人都已评估完毕,于是向用户发送最终消息,告知流程结束。
在这个过程中,评估专员可能会拒绝某些候选人(例如,Galtung的简历过于专注于园艺,与公司需求不匹配),这时提交评估工具会打印拒绝理由,而不会触发外联专员。
总结
本节课中,我们一起学习并实现了一个复杂的多智能体编排系统。我们主要掌握了以下核心概念:
- 智能体即服务:智能体可以作为独立服务运行,通过API进行交互。
- 跨智能体通信:通过为智能体提供消息发送工具,可以实现智能体间的直接协作与任务传递。
- 共享内存:通过创建共享内存块,可以让多个智能体访问和更新同一份上下文信息(如公司详情),并且更新会自动同步。
- 工作流编排:通过设计不同角色的智能体(招聘、评估、外联)并利用上述通信和内存共享机制,可以构建出自动化的多步骤业务流程。
这个例子展示了智能体如何像人类团队一样分工合作:招聘专员负责寻找资源,评估专员负责筛选,外联专员负责沟通。它们共享公司背景信息,并能通过消息传递工作项。随着LLM变得更智能、更经济,这类多智能体工作流将变得越来越强大和实用。

恭喜你完成了这个内容详实的实验!这是本课程的最后一个实验,你已成功掌握了使用共享内存块和工具消息来实现多智能体协调的新方法。
008:课程总结 🎓
在本节课中,我们将一起回顾并总结关于如何将大型语言模型(LLM)构建为具备复杂记忆系统的智能体(Agent)的核心知识。我们学习了如何突破LLM有限的上下文窗口,利用LMOS(LLM作为操作系统)框架来创建能够利用虚拟上下文和扩展记忆的应用程序。
上一节我们探讨了智能体记忆系统的具体实现,本节中,我们来进行全面的课程总结。
恭喜你完成本课程的学习。你已经掌握了如何将一个简单的文本生成器——即大型语言模型(LLM)——通过LMOS框架,用于创建复杂的智能体记忆系统。
你现在已经拥有了工具,就像你的智能体一样,可以构建能够利用虚拟上下文的LLM应用程序。这种能力将记忆扩展到远超出LLM自身有限的上下文窗口。
我们期待看到你独立构建出的成果。


本节课中我们一起学习了将LLM视为操作系统的核心理念,特别是如何为其设计和集成记忆系统。关键点在于利用外部存储和检索机制来突破模型自身的上下文限制,从而构建出更强大、更持久的智能体应用。


浙公网安备 33010602011771号