DLAI-AutoGen-智能体设计模式笔记-全-
DLAI AutoGen 智能体设计模式笔记(全)
001:课程介绍


欢迎来到与微软合作构建的《利用AutoGen的人工智能智能体设计模式》课程。
本课程的讲师是微软的资深研究员朱旺和宾夕法尼亚州立大学的助理教授吴涛,他们两位都是AutoGen的共同创造者。
感谢吴恩达。很高兴来到这里。谢谢。很高兴与您一起,吴恩达。
课程概述
在本课程中,我们将介绍AutoGen。AutoGen是一个多智能体对话框架,它使您能够快速创建具有不同角色、个性和能力的多个智能体,以使用不同的AI智能体设计模式来实现复杂的AI应用。
让我们假设您对分析金融数据感兴趣。这项任务可能需要编写代码来收集和分析股价,然后将您的发现综合成一份报告。这可能需要一个人花费数天时间进行研究、编码和写作。
一个多智能体系统可以通过让您创建并“雇佣”智能体为您工作来简化这个过程,例如研究员、数据收集员、代码编写员和执行员。您的智能体还可以迭代地审查、批评和改进结果,直到它达到您的标准。
这只是多智能体框架众多实际应用中的一个例子。朱旺和吴涛将引导您完成六个课程,每个课程都展示其独特的设计流程和用例。
课程内容
在本课程中,您将通过探索构建智能体和可对话智能体来学习AutoGen的所有核心组件。您将创建并定制两个单口喜剧演员智能体之间的生动对话,同时探索它们的交互能力。
接下来,您将学习一种称为顺序聊天的多智能体交互模式。这使您能够构建按步骤执行一系列任务的对话智能体。我们将用一个客户入职应用程序来说明这一点。
您还将在两个实际场景中探索智能体反思框架。第一个场景将使用多个智能体来生成一篇写得很好的博客文章。第二个场景则为智能体添加工具,以创建一个对话式国际象棋游戏。在这两个例子中,您将学习另一种称为嵌套聊天的交互模式。
嵌套聊天意味着当一个智能体被赋予一项任务时,它会召集一批其他智能体,并与它们进行一段时间的迭代,然后才返回结果。如果我们把您(开发者)想象成几个智能体的经理,那么嵌套聊天就对应于让您管理的一个智能体也去管理它们自己的智能体。
您还将学习一个非常强大的功能:工具使用。您可以为智能体提供一个用户定义的函数,例如一个检查特定棋步是否合法的函数,并让智能体使用它。
对于那些您可能没有预定义函数或代码可提供的应用,您还将学习编码和代码执行。这意味着要求智能体编写它需要的代码,在检查正确性之后,它可以在沙箱环境中执行自己编写的代码,以计算任务所需的结果。我们将在金融分析示例中同时说明工具使用以及代码编写和执行。
最后,您将学习构建自定义多智能体群聊的最佳实践。我们将通过生成详细报告的示例来说明这一点。这项复杂任务需要规划,换句话说,智能体必须决定要采取的一系列行动,例如获取数据、分析、写作和修订。您将看到我们如何将规划智能体添加到群聊中,并控制工作如何从一个智能体流向另一个智能体,以便以正确的顺序执行所需的各个任务。
第一课预告
许多人已经完成了这门课程,我要感谢微软的每一位成员以及DeepLearning.AI的团队。


在第一课中,您将从构建第一个AutoGen智能体开始,编写一个基本的双智能体对话程序,并欣赏一场单口喜剧表演。这听起来很棒。让我们进入下一个视频,开始学习吧。😊
002:多智能体对话与单口喜剧示例


在本节课中,我们将学习AutoGen中可对话智能体的基本概念,并构建一个由两个单口喜剧演员智能体相互对话的趣味应用。我们将从智能体的基础定义开始,逐步学习如何创建智能体、配置其行为,并管理它们之间的多轮对话。
智能体基础概念


上一节我们介绍了本课程的目标,本节中我们来看看AutoGen中智能体的核心概念。
在AutoGen中,一个智能体是一个可以代表人类意图行事的实体。它可以发送消息、接收消息、执行操作、生成回复并与其他智能体互动。
AutoGen提供了一个内置的智能体类,称为 ConversableAgent。它在同一个编程抽象中统一了不同类型的智能体,并附带了许多内置功能。
以下是ConversableAgent的一些核心功能:
- 你可以使用LLM配置列表来生成回复。
- 你可以执行代码或函数调用。
- 它提供了让人类参与循环以及检查是否停止响应的组件。
- 你可以根据应用需求,开启、关闭或自定义这些组件。
利用这些不同的能力,你可以使用相同的接口创建具有不同角色的智能体。
创建你的第一个智能体
了解了智能体的概念后,我们开始动手创建第一个智能体。
首先,我们需要从环境变量中导入OpenAI API密钥,并定义语言模型配置。
# 导入获取API密钥的工具函数
from autogen import get_openai_api_key
# 运行函数获取密钥
openai_api_key = get_openai_api_key()
# 定义LLM配置,本课程使用GPT-3.5 Turbo模型
llm_config = {
"model": "gpt-3.5-turbo",
}
接下来,我们从AutoGen导入ConversableAgent类,并创建第一个智能体对象。
# 导入ConversableAgent类
from autogen import ConversableAgent
# 创建第一个可对话智能体,命名为“chatbot”
chatbot = ConversableAgent(
name="chatbot",
llm_config=llm_config, # 传入上面定义的LLM配置
human_input_mode="NEVER", # 设置人类输入模式为“从不”
)
我们向ConversableAgent传递了LLM配置,这样智能体就能使用大语言模型来生成回复。human_input_mode设置为"NEVER"意味着智能体永远不会寻求人类输入,它只会使用大语言模型来生成回复。通常,你可以将此模式切换到其他设置,例如"ALWAYS",那么智能体在尝试自行生成回复前总会先询问人类输入。
这只是智能体的基本设置。通常,你还可以添加代码执行配置、函数调用等其他设置,但让我们从这个简单设置开始。
与智能体进行单次对话
创建好智能体后,我们可以使用generate_reply方法让它对一个提问生成回复。
# 调用智能体的generate_reply函数,并提供一个消息列表
reply = chatbot.generate_reply(
messages=[
{
"content": "Tell me a joke.",
"role": "user",
}
]
)
print(reply)
运行这段代码,你应该能从智能体得到一个回复。智能体可能会说:“当然,给你讲个笑话:为什么稻草人会获奖?因为他在他的领域里很出色。”
这是通过提问并从智能体获得回复所能做的最基本的事情。
现在,如果你再次调用这个函数会发生什么?假设我们再次调用generate_reply函数,这次将内容替换为“重复那个笑话”。我们期望智能体重复那个笑话吗?实际上不会。因为当我们调用generate_reply函数时,它不会改变智能体的内部状态。所以当我们再次调用时,它并不知道之前已经生成过回复,这相当于一次全新的调用,它会生成一个新的回复,而不知道之前回复过。
如果你希望生成不同的回复,当然可以在应用中使用这种方式。但如果你想保持状态、维护状态并让它执行一系列任务,我们需要一种不同的方法。
构建多智能体对话:单口喜剧示例
在下一部分,让我们看看如何创建多个智能体之间的对话,我们将做一个单口喜剧的例子。
我们想创建一个应用,让两个单口喜剧演员智能体相互交谈并取笑对方。首先,我们创建一个名为Cassy的可对话智能体。
# 创建第一个喜剧演员智能体 Cassy
cassy = ConversableAgent(
name="Cassy",
system_message="Your name is Cassy and you are a standup comedian.",
llm_config=llm_config,
human_input_mode="NEVER",
)
在这个例子中,我们通过system_message让智能体知道“你的名字是Cassy,你是一个单口喜剧演员”。我们传递了相同的LLM配置和相同的人类输入模式。如果你不指定系统消息,那么智能体会有一个空的系统消息,并表现为一个通用助手智能体。使用系统消息,我们可以定制智能体的行为。
接下来,我们创建另一个智能体。
# 创建第二个喜剧演员智能体 Joe
joe = ConversableAgent(
name="Joe",
system_message="Your name is Joe and you are a standup comedian. Start the next joke from the punch line of the previous joke.",
llm_config=llm_config,
human_input_mode="NEVER",
)
我们创建了另一个名为Joe的可对话智能体,并给出了系统消息:“你的名字是Joe,你是一个单口喜剧演员。” 之后,我们添加了另一条指令:“从上一个笑话的包袱开始下一个笑话。” 这为我们提供了关于如何延续对话的更具体指示。
发起并管理智能体对话
现在,我们有两个喜剧演员智能体准备就绪,可以开始工作了。发起对话的方式是调用其中一个智能体的initiate_chat函数。
例如,如果我们想让Joe开始对话,就调用Joe的initiate_chat函数。
# 让 Joe 发起与 Cassy 的对话
chat_result = joe.initiate_chat(
recipient=cassy,
message="I'm Joe, Cassy. Let's keep the jokes rolling.",
max_turns=2, # 设置对话轮数为2
)
我们将收件人设置为Cassy,并给出初始消息:“我是Joe,Cassy。让我们继续讲笑话吧。” 我们设置max_turns为2,所以将进行两轮对话然后结束。
运行后,我们可以看到对话过程。第一条消息就是我们设置的“I‘m Joe, Cassy. Let’s keep the jokes rolling.”。下一条消息来自Cassy:“嘿Joe,很高兴见到另一位喜剧爱好者,让我们用一些笑话开始吧。为什么数学书看起来很伤心?因为它有太多问题。” 下一轮,Joe说:“好吧,Cassy,至少现在我们知道为什么数学书总是那么消极了。” 你可以看到Joe遵循了我们之前的指示,从上一个笑话的包袱开始了下一个笑话。Cassy接着说:“哈哈,没错,它只是无法从书页中减去悲伤。” 这延续了那个笑话,并最终提出了另一个笑话。经过这两轮交流,对话停止了。
对话结束后,我们可以检查chat_result中的聊天记录。
# 导入 pprint 库以便美观地打印聊天记录
import pprint
pprint.pprint(chat_result.chat_history)
你可以看到所有交换的消息:第一条来自Joe,第二条来自Cassy,第三条来自Joe,第四条再次来自Cassy。
你还可以检查聊天结果中的令牌使用情况。
# 查看对话的成本(令牌使用量和费用)
print(chat_result.cost)
我们会看到我们使用了GPT-3.5 Turbo模型,消耗了97个完成令牌和219个提示令牌,总令牌数为316,总成本为若干美元。
通常,你可以用不同的方式定义对话。你也可以通过调用chat_result.summary函数来检查聊天结果的摘要。默认情况下,我们使用最后一条消息作为聊天结果的摘要。在这个例子中,我们看到那是来自Cassy的最后一条消息。如果你想改变摘要方法,我们可以用不同的摘要方法进行配置。
高级对话控制:终止条件
你可能会注意到,我使用了max_turns=2来控制这次对话中发生多少轮。如果你在对话结束前不知道正确的轮数,该怎么办?我们可以通过提供名为is_termination_msg的额外配置来改变终止条件。这是一个布尔函数,它接收一条消息作为输入,并返回True或False,表示该消息是否意味着对话应该终止。
例如,你会注意到我改变了这里的消息,说“当你准备好结束对话时,说‘I gotta go’”。我们也传递这个停止条件,检查“I gotta go”是否在消息中。如果我们检测到“I gotta go”这个短语,我们将认为对话结束,并将此条件给予每个智能体。因此,每个智能体都会检查从另一个智能体收到的消息中的条件,如果他们在收到的消息中看到“I gotta go”内容,他们将停止回复。
让我们用新的终止条件再次运行对话,看看会发生什么。
# 定义终止条件函数
def is_termination_msg(content):
return "I gotta go" in content["content"]
# 再次发起对话,这次包含终止条件
chat_result_2 = joe.initiate_chat(
recipient=cassy,
message="I'm Joe, Cassy. Let's keep the jokes rolling.",
max_turns=10, # 设置一个较大的轮数上限,实际由终止条件控制
is_termination_msg=is_termination_msg,
)
前几条消息类似。但这次,你可以看到他们有更多轮的对话。Cassy讲了个笑话,Joe回应了。Cassy问Joe另一个不同的笑话,Joe用其他笑话回应,最终Joe的最后一条消息是:“很高兴你喜欢,Cassy。披萨总是领先一步。谢谢你的笑声,我得走了。” 是的,它以“I gotta go”结束了对话,Cassy看到了,所以它停止了回复。这是一种更灵活的停止对话的方式。
继续对话与状态保持
对话结束后,如果你想继续对话,或者想看看这次智能体是否能保持状态,我们可以测试一下。我们可以用之前类似的提问来测试。接下来,我们将让Cassy发送另一条消息。
# 让 Cassy 继续与 Joe 对话
continuation_result = cassy.send(
recipient=joe,
message="What's the last joke we talked about?",
)
这次,他们会记得最后一个笑话是什么吗?让我们检查一下。成功了!Joe回应道:“我们讨论的最后一个笑话是关于稻草人因为在他的领域里很出色而获奖。” 并且,他们也遵循了相同的终止条件。所以我们看到这次Cassy说“I gotta go”。Joe也知道这是停止对话的信号,它将停止回复。
这演示了让智能体进行对话、开始对话、继续对话以及记住对话历史的方法。
总结

本节课中我们一起学习了AutoGen中可对话智能体的核心概念。我们从定义智能体开始,学习了如何创建和配置单个智能体,并使用generate_reply方法进行单次交互。接着,我们深入探讨了多智能体对话,构建了两个单口喜剧演员智能体相互调侃的趣味示例。我们掌握了使用initiate_chat发起对话、用max_turns控制对话轮数、以及通过自定义is_termination_msg函数实现更灵活的对话终止条件。最后,我们还验证了智能体在对话中保持状态和记忆的能力。这仅仅是使用ConversableAgent构建两个智能体对话的非常基础的演示,在接下来的课程中,我们将学习许多其他对话模式和智能体设计模式。
003:顺序对话与客户引导

在本节课中,我们将学习如何利用多智能体设计来完成一个包含多个步骤的任务。我们将构建一个由多个智能体参与的对话序列,它们将协作完成一个产品的客户引导流程。我们还将体验人类如何无缝地参与到AI系统的循环中。
概述
客户引导是一个典型的多步骤流程。通常,我们需要先收集客户信息,然后调查客户兴趣,最后根据收集到的信息与客户进行互动。因此,将客户引导任务分解为三个子任务是一个好主意,包括:信息收集、兴趣调查和客户互动。



上一节我们介绍了多智能体的基本概念,本节中我们来看看如何通过顺序对话来完成这一系列任务。

构建智能体
我们将使用AutoGen中的ConversableAgent类来实现这些智能体。首先,我们需要导入这个类。
from autogen import ConversableAgent
1. 创建信息收集智能体
第一个智能体负责询问个人信息。我们通过设置系统消息来定义其行为。
onboarding_info_agent = ConversableAgent(
name="OnboardingInfoAgent",
system_message="你是一个客户引导智能体。你的任务是向客户询问其个人信息,例如姓名和所在地。",
llm_config={"config_list": [{"model": "gpt-4"}]},
human_input_mode="NEVER"
)
这里,我们将human_input_mode设置为"NEVER",因为我们使用大语言模型来生成该智能体的响应。
2. 创建兴趣调查智能体
第二个智能体负责询问客户感兴趣的话题。
onboarding_topic_agent = ConversableAgent(
name="OnboardingTopicAgent",
system_message="你是一个客户引导智能体。你的任务是询问客户在阅读方面感兴趣的话题。",
llm_config={"config_list": [{"model": "gpt-4"}]},
human_input_mode="NEVER"
)
3. 创建客户互动智能体

第三个智能体将根据客户的个人信息和话题偏好,提供有趣的事实、笑话或故事。

engagement_agent = ConversableAgent(
name="EngagementAgent",
system_message="你是一个客户互动智能体。你的任务是根据客户的个人信息(如姓名、所在地)和他们感兴趣的话题,提供有趣的事实、笑话或故事来吸引客户。",
llm_config={"config_list": [{"model": "gpt-4"}]},
human_input_mode="NEVER"
)
4. 创建客户代理智能体
我们需要一个代理智能体来代表真实的客户。注意,这里我们将human_input_mode设置为"ALWAYS",以便该代理智能体能够随时向真实客户征求输入。
customer_proxy_agent = ConversableAgent(
name="CustomerProxyAgent",
system_message="你是一个客户代理。",
llm_config=False,
human_input_mode="ALWAYS"
)
设计顺序对话流程
构建好智能体后,我们现在可以设计一个顺序对话来完成引导流程。在这个具体例子中,每次对话本质上是某个引导智能体与客户代理智能体之间的双人对话。
在每次对话中,发送方智能体会向接收方发送一条初始消息以开启对话,然后他们会进行来回交流,直到达到最大轮次或收到终止消息。
以下是实现顺序对话的关键步骤:
第一段对话:收集个人信息
在第一段对话中,我们设置最大对话轮次为2,以确保对话简洁。
# 启动第一段对话
result_info = onboarding_info_agent.initiate_chat(
recipient=customer_proxy_agent,
message="你好!欢迎使用我们的服务。首先,可以告诉我你的姓名和所在地吗?",
max_turns=2
)
在顺序对话场景中,任务通常是相互依赖的。因此,我们可能需要总结前一段对话的信息,以供下一段对话使用。在这个例子中,我们使用reflection作为总结方法。
# 总结第一段对话的客户信息
summary_info = result_info.summary(method="reflection")
我们还可以提供一个总结提示,指导大语言模型如何进行总结。
summary_prompt = "请将客户信息总结为一个JSON对象,格式为:{'name': '客户姓名', 'location': '客户所在地'}"
summary_info = result_info.summary(method="reflection", summary_prompt=summary_prompt)
第二段对话:调查兴趣话题
第二段对话由话题偏好引导智能体发起。
# 启动第二段对话
result_topic = onboarding_topic_agent.initiate_chat(
recipient=customer_proxy_agent,
message="太好了!接下来,能告诉我你对哪些阅读话题感兴趣吗?",
max_turns=1,
summary_method="reflection"
)
这次我们没有提供自定义的总结提示,因此将使用内置的默认提示。max_turns设置为1,因为我们只想用一轮对话来了解客户的兴趣话题。你可以根据需求将其改为多轮。
第三段对话:进行客户互动
第三段对话在客户代理智能体和客户互动智能体之间进行。这次由客户代理智能体发起对话。
# 启动第三段对话
result_engagement = customer_proxy_agent.initiate_chat(
recipient=engagement_agent,
message="让我们找点东西来读吧。😊"
)
运行引导流程
配置好所有对话会话后,我们就可以通过执行initiate_chat来启动整个引导流程了。在这个例子中,你将扮演客户。当系统提示你输入答案时,请键入你的响应并按回车键。
以下是运行示例:
- 第一段对话开始:信息收集智能体询问:“你好!欢迎使用我们的服务。首先,可以告诉我你的姓名和所在地吗?”
- 客户输入:
Alice - 智能体询问:“你的所在地是哪里?”
- 客户输入:
New York
- 客户输入:
- 第二段对话开始:兴趣调查智能体询问:“太好了!接下来,能告诉我你对哪些阅读话题感兴趣吗?”
- 客户输入:
dogs
- 客户输入:
- 第三段对话开始:客户互动智能体根据收集到的信息(姓名:Alice,所在地:New York,兴趣:dogs)生成一条互动消息。
你可以随时暂停并尝试提供自己的偏好信息。
检查结果与成本
对话完成后,你可以检查顺序对话中每个会话的结果。

# 检查第一段对话的总结信息
print("客户个人信息总结:", summary_info)

# 检查第二段对话的总结信息
summary_topic = result_topic.summary(method="reflection")
print("客户兴趣总结:", summary_topic)
# 检查第三段对话的最后一条消息
print("互动消息:", result_engagement.last_message()["content"])

同样,你也可以查看与每次对话相关的成本。

# 查看总成本信息
print("总成本详情:", result_info.cost, result_topic.cost, result_engagement.cost)

成本信息通常包括总成本、提示词令牌成本和补全令牌成本。

总结
在本节课中,我们一起学习了如何使用顺序对话来完成一系列相互依赖的任务。我们构建了三个专门的智能体来分别处理信息收集、兴趣调查和客户互动,并通过一个客户代理智能体将人类用户引入循环。我们还了解了如何总结前序对话的信息并将其传递到后续对话中。

在下一节课中,你将学习如何利用嵌套对话来实现反思智能体设计模式,即在一个智能体的内部独白中,将一段或一系列对话嵌套在另一段对话里。
004:反思与博客写作 📝


在本节课中,我们将学习智能体反思框架,并利用其能力来创建高质量的博客文章。你将学习如何使用嵌套聊天(Nested Chat)模式来实现一个复杂的反思过程。

我们将构建一个系统,其中一组评审员智能体被嵌套在一个评论家智能体内部,作为其“内心独白”,来反思由作者智能体撰写的博客文章。

概述
上一节我们介绍了基础的智能体对话模式。本节中,我们将深入探讨一个更高级的模式——反思模式,并将其应用于一个具体的任务:撰写一篇关于深度学习的博客文章。我们将从一个简单的作者智能体开始,逐步引入评论家智能体,并最终构建一个包含多个专业评审员的复杂反思工作流。
构建基础作者智能体

首先,我们考虑一个博客写作任务。假设我们需要撰写一篇关于深度学习AI的简洁而引人入胜的博客文章,并且要确保文章在100字以内。
根据第一课所学,我们首先想到的是构建一个作者智能体来完成这个任务。让我们快速实现它。

我们可以导入AutoGen库,并构建一个基于大语言模型的助手智能体作为作者。
import autogen


# 构建作者智能体
writer = autogen.AssistantAgent(
name="Writer",
llm_config={"config_list": [{"model": "gpt-4", "api_key": "your_key"}]}
)


当然,你可以直接调用作者智能体的 generate_reply 函数,并提供任务作为输入,让它开始写作。然后,你可以检查这个函数返回的结果。


现在,我们得到了一篇关于深度学习AI的博客文章。这很好,但我们希望它能变得更好。



引入反思模式与评论家智能体

一个改进的思路是使用“反思”,这是一个显著且有效的智能体设计模式。实现反思的一种方法是引入另一个智能体来反思工作并帮助改进它。
遵循这个思路,我们创建一个评论家智能体来反思作者智能体的工作。

# 构建评论家智能体
critic = autogen.AssistantAgent(
name="Critic",
system_message="你是一位严格的编辑。请仔细审查作者提供的博客文章,并提供具体的改进反馈。",
llm_config={"config_list": [{"model": "gpt-4", "api_key": "your_key"}]}
)

同样,我们使用AutoGen的助手智能体卡片来创建这个基于大语言模型的智能体。在这里,我们通过系统提示词引导这位评论家去调查作者的写作并提供反馈。

有了这两个智能体,我们就能发起它们之间的对话,让评论家和作者进行来回交流。

以下是对话流程的示例:
- 作者给出博客文章的初稿。
- 评论家提供一些反馈。
- 作者根据反馈给出另一个版本的文章。

从这个结果中,我们可以看到智能体正在反思工作并提出建议。这很好,但建议仍然比较笼统。


实现复杂的嵌套反思工作流

在许多情况下,我们可能希望实现一个更复杂的反思工作流,作为评论家智能体的“内心独白”。例如,我们可能希望确保评论家智能体从特定角度对工作进行批评,比如内容在搜索引擎中的排名是否良好、是否能吸引自然流量、是否存在法律和伦理问题等。


现在,让我们看看如何使用嵌套聊天(Nested Chat)来处理所有这些需求。嵌套聊天本质上是一个注册为智能体内部独白的聊天会话。

首先,在这个特定的博客写作任务中定义嵌套聊天。假设我们想在评论家智能体内部创建一组评审员,来仔细检查写作的不同方面。


以下是我们要定义的评审员智能体列表:
- SEO评审员:负责优化内容以适配搜索引擎,确保其排名良好并吸引自然流量。我们通过设置该评审员智能体的系统消息来实现这一点。
- 法律评审员:负责确保内容在法律上是合规的。
- 伦理评审员:负责确保内容在伦理上是合理的,并且没有任何潜在的伦理问题。


请注意,在所有这三个评审员智能体中,我们都指示其提供建议,并且我们希望评审员以特定角色开始评审,以便稍后我们可以将这些评审员的评论汇总成最终评审。
最后,为了模拟真实评审过程的工作方式,我们可能还需要一个元评审员来汇总所有评审并给出最终建议。为此,我们创建一个元评审员,它将汇总来自所有评审员的评论。
配置嵌套聊天会话


下一步是定义要注册的聊天会话。这里我们进一步使用上一课学到的顺序聊天对话模式,在评论家和各个评审员之间构建一系列聊天会话。
我们有效地拥有一个包含四个聊天会话的列表,每个会话都涉及一个特定的评审员作为接收者。

稍后,我们将把这个聊天列表注册到评论家智能体。因此,评论家智能体将默认用作发送者,我们不需要在这里进一步指定发送者。

此外,对于前三个聊天会话,我们还使用大语言模型按照所需格式进行总结,这就是为什么将 summary_method 设置为 reflection_with_llm,并且我们提供了一个总结提示词,以便每个评审都能以JSON格式返回评审结果,该格式包括“评审员”和“评审内容”字段。
在所有聊天会话中,我们将最大对话轮数设置为1。

另一个值得注意的事情是,我们需要正确设置初始消息,以便嵌套的评审员能够获得要评审的内容。一种常用的方法是从外部聊天会话的总结中获取内容。这就是为什么我们将初始消息定义为一个函数 reflection_message。这个函数的职责是从外部聊天会话的智能体中获取总结,并以此作为初始消息。

注册并运行嵌套聊天

最后,我们可以将评审聊天会话作为嵌套聊天注册到评论家智能体。在 register_nested_chat 函数中,我们还通过设置 trigger 为作者智能体。这样,每当评论家智能体收到来自作者智能体的消息时,它会自动将消息路由到这个嵌套聊天会话中进行仔细反思。


完成此注册步骤后,我们现在可以发起评论家和作者之间的聊天。
请注意,这一步基本上与我们之前进行评论家和作者智能体之间的反思时所做的相同。不同之处在于,这次我们在评论家智能体之上注册了这个嵌套聊天会话,以进行更仔细的反思。
现在,我们启动聊天会话。我们可以看到,第一步是作者生成博客文章的初稿。这与我们之前看到的基本相同。





接下来,评论家智能体没有直接给出反馈,而是将消息路由到嵌套聊天中。例如,我们可以看到评论家智能体和SEO评审员之间开始了一个新的聊天,SEO评审员将调查此内容,并就如何通过合并相关关键词(如“AI课程”、“深度学习AI”等)来优化内容以适配搜索引擎提出建议。

然后,我们转到法律评审员,法律评审员将调查潜在的法律风险。例如,这位法律评审员建议审查标题。



接着,我们转到伦理评审员。在调查了博客文章后,伦理评审员调查了潜在的伦理问题,并得出结论:没有提出关于伦理影响的修改或担忧。
最后,我们转到元评审员。基本上,元评审员将调查我们从先前评审员那里收到的所有评论。



因为我们指示聊天会话总结聊天内容,所以每个聊天会话都以JSON格式提供了清晰的评审结果。最后,元评审员通过评论家智能体向作者智能体给出最终的汇总建议。



作者智能体收到评论家智能体的建议后,将相应地完善博客文章。

检查最终结果


现在,让我们进一步检查这个聊天会话的结果。我们调用 summary 来查看结果。


在这里,你可以看到它返回了博客文章的最终完善版本。
你可以随时暂停,尝试你自己的任务。例如,你可以让智能体就你感兴趣的主题撰写一篇博客文章。


总结
本节课中,我们一起学习了如何使用嵌套聊天来实现反思智能体设计模式。我们从构建一个简单的作者智能体开始,然后引入了评论家智能体进行基础反思。为了获得更专业、多维度的反馈,我们进一步构建了一个复杂的嵌套反思工作流,其中评论家智能体内部包含了SEO、法律、伦理评审员以及一个负责汇总的元评审员。通过嵌套聊天模式,我们实现了智能体之间层次化的协作与反思,从而显著提升了最终输出内容的质量。

在下一课中,我们将学习如何进一步利用嵌套聊天来升级智能体设计模式。敬请期待。
005:工具使用与会话式国际象棋 🏆


在本节课中,我们将学习如何在智能体之间的消息聊天中使用工具。你将构建一个会话式国际象棋游戏,两个AI棋手都可以调用工具,并在棋盘上做出合法的移动。

概述
上一节我们介绍了如何在AutoGen中使用可对话智能体进行对话。本节中,我们将学习一项新能力——工具使用。我们将继续使用嵌套聊天作为一种对话模式,使智能体能够使用工具完成任务,并通过构建一个会话式国际象棋游戏来展示这个例子。
我们将使用GPT-4模型,因为它在国际象棋方面比GPT-3表现更好。
第一步:创建国际象棋游戏所需的工具
首先,我们需要导入一些库。我们导入chess库,它为我们提供了以编程方式运行游戏的方法。
import chess
我们将初始化棋盘。为了让棋盘了解当前状态,我们需要一个变量来跟踪是否已经走了一步棋。因此,我们将该变量初始化为False。
board = chess.Board()
move_made = False
现在,我们准备好定义智能体所需的工具了。
首先,我们定义一个名为get_legal_moves的工具。这是一个Python函数,它返回一个字符串,内容是UCI格式的合法移动列表。UCI格式是一种供智能体理解所走棋步的语言格式。
def get_legal_moves() -> str:
legal_moves = list(board.legal_moves)
return f"Possible moves are: {legal_moves}"
这个带注释的部分非常重要,它将允许智能体理解返回内容的格式及其含义。
接下来,我们还需要定义一个在棋盘上走棋的工具。这个函数有一个名为move的输入参数。当智能体决定走一步棋时,它可以调用这个函数并传入一个特定的字符串,这个移动也必须是UCI格式。该函数的返回值应该是一个表示移动结果的字符串。
def make_move(move: str) -> str:
global move_made
chess_move = chess.Move.from_uci(move)
board.push(chess_move)
move_made = True
board_display = str(board)
piece = board.piece_at(chess_move.from_square)
piece_symbol = piece.symbol()
piece_name = chess.piece_name(piece.piece_type)
if piece_symbol.isupper():
piece_name = "white " + piece_name
else:
piece_name = "black " + piece_name
return f"We moved the {piece_name} from {chess_move.from_square} to {chess_move.to_square}."
在返回信息给智能体之前,我们想获取刚刚所走棋步的信息,从移动的原始点获取棋子,获取该棋子的符号,并使用chess库的函数创建棋子名称并进行规范化。最后,我们返回一个字符串,说明我们将一个棋子从一个原始位置移动到了一个新位置。这样就完成了工具的定义。
第二步:创建智能体
现在,我们准备好创建智能体了。我们将从AutoGen导入ConversableAgent。
from autogen import ConversableAgent
首先,我们创建一个玩家智能体player_white,并给它一个系统消息:“你是一名国际象棋棋手,你执白棋”。我们还需要让智能体知道有哪些工具可用,所以告诉它首先调用get_legal_moves来获取合法移动列表,然后调用make_move来走一步棋。我们将让智能体知道这两个可用的函数。我们还需要向它传递一个大语言模型配置。
player_white = ConversableAgent(
name="player_white",
system_message="You are a chess player and you play as white. First call get_legal_moves to get a list of legal moves, then call make_move to make a move.",
llm_config={"config_list": [{"model": "gpt-4", "api_key": os.environ.get("OPENAI_API_KEY")}]},
)
另一个玩家智能体呢?唯一的区别是名称和系统消息。
player_black = ConversableAgent(
name="player_black",
system_message="You are a chess player and you play as black. First call get_legal_moves to get a list of legal moves, then call make_move to make a move.",
llm_config={"config_list": [{"model": "gpt-4", "api_key": os.environ.get("OPENAI_API_KEY")}]},
)
现在创建了这两个玩家智能体。需要记住的一点是,尽管我们在系统消息中告诉了这两个智能体它们可以使用的两个函数,但我们必须给它们具体的定义。因此,稍后我们将需要注册工具。
在此之前,让我们创建一个存储智能体,称为board_agent。board_agent扮演着与玩家检查棋步直到棋步合法,并更新棋盘状态的角色。为了定义这个board_agent,我们还需要为智能体设置一个停止条件。
def check_move_made():
return move_made
board_agent = ConversableAgent(
name="board_proxy",
llm_config=False, # 这个智能体不会使用大语言模型
is_termination_msg=check_move_made,
human_input_mode="NEVER",
)
我们传递了终止条件函数check_move_made。当一步棋已经走完时,这个智能体将停止与其他智能体的对话;否则,它会继续询问其他智能体并说“请走一步棋”,这是它唯一的回复。我们还将human_input_mode设置为NEVER。
第三步:向智能体注册工具

现在我们已经定义了所有智能体,可以向智能体注册工具了。为此,我们将从autogen导入一个名为register_function的函数。
from autogen import register_function
我们需要为两个玩家都注册,所以我们将在这两个玩家智能体之间使用一个for循环。
for player in [player_white, player_black]:
register_function(
get_legal_moves,
caller=player,
executor=board_agent,
name="get_legal_moves",
description="Get the list of legal moves in UCI format.",
)
register_function(
make_move,
caller=player,
executor=board_agent,
name="make_move",
description="Make a move on the chessboard. Input should be a move in UCI format.",
)
在这些函数注册之后,调用者(即其中一个玩家智能体)将能够提议一个函数调用,而执行者(即board_proxy智能体)将能够执行该工具。
我们实际上可以检查工具注册情况,例如,我们可以检查player_black当前的llm_config部分。我们看到一些字段,如类型、函数描述、参数,都自动填充到了这个工具定义中。这就是AutoGen的register_function所做的。因此,用户不需要手动编写所有这些遵循OpenAI格式的定义。AutoGen库会为你完成。用户需要做的就是定义函数,并以这种方式提供必要的注释。除此之外,它只是一个你正在学习的普通Python函数,智能体使用这些工具不需要额外的工作。
第四步:设置嵌套聊天模式
注册工具后,我们还希望创建这个嵌套聊天对话模式,因为我们希望当两个玩家彼此对弈时,他们在走棋之前能与board_agent进行嵌套聊天,以确保棋步是合法的。为此,我们首先为player_white注册一个嵌套聊天。这个嵌套聊天的触发者是player_black。当player_white收到来自player_black的消息时,这个嵌套聊天将被触发,并且这个嵌套聊天将在它回复player_black之前执行。
player_white.register_nested_chats(
trigger=player_black,
chat_queue=[{"sender": board_agent, "recipient": player_white, "summary_method": "last_msg"}],
)
这里我们使用一个只有一个聊天的聊天队列。在嵌套聊天中,聊天将由代理board_proxy发起,接收者将是player_white。我们将使用最后一条消息作为这个聊天的总结方法。在player_white回复player_black之前,它将首先在board_proxy和player_white之间发起这个嵌套聊天,直到player_white选择了一步棋,然后它再回复player_black关于这步棋。
类似地,我们也可以为player_black注册一个嵌套聊天。这次的触发者是player_white,内部聊天是在board_proxy和player_black之间。


player_black.register_nested_chats(
trigger=player_white,
chat_queue=[{"sender": board_agent, "recipient": player_black, "summary_method": "last_msg"}],
)

第五步:运行游戏
好了,这就是我们需要做的所有工作。让我们准备好开始游戏。我们将首先调用chess.Board()函数创建一个新棋盘,然后调用player_black.initiate_chat方法,并附带初始消息“让我们下棋吧,该你走了”。我们将clear_history设置为True。


board = chess.Board()
move_made = False
player_black.initiate_chat(
player_white,
message="Let's play chess. Your move.",
clear_history=True,
)
当我们运行代码时,我们看到player_black发起了消息。现在player_white收到消息,它将进入这个自动回复,并进行必要的嵌套聊天。这个嵌套聊天是在board_proxy和player_white之间。我们注意到player_white首先建议调用工具get_legal_moves来从棋盘获取合法移动。board_proxy返回工具执行结果,返回所有可能的移动。然后player_white使用make_move工具,参数为e2e4。这就是player_white选择的移动。board_agent现在执行该移动并返回消息:“我们将兵从e2移动到e4”。这就是嵌套聊天的结束,因为一步棋已经走完。
在那个嵌套聊天结束后,player_white将回复返回给player_black。这是从player_white发送给player_black的实际回复。
现在,board_proxy和player_black之间会发生同样的嵌套聊天。player_black也会调用get_legal_moves工具并建议移动,使用make_move。player_black选择的移动是g8f6。现在board_proxy返回确认:“我们将马从g8移动到f6”。所以来自player_black的最终消息是“我已将我的马从g8移动到f6,该你了”。那个内部嵌套聊天结束,所以player_black将总结发送回player_white。
同样的模式将再次发生,player_white将获取合法移动并选择它将走的另一步棋。这就是这两个回合对话的结束。

第六步:增加趣味性
我们如何让这个游戏更有趣呢?我们注意到在这个游戏中,玩家只谈论他们正在走的棋步。我们如何增加一点乐趣,让聊天不仅仅局限于这些棋步呢?

我们可以修改关于这些玩家智能体的定义。现在我们可以更改这条消息,说在走了一步棋之后,闲聊一下让游戏变得有趣。这是我们添加的一个额外指令。对player_black也做同样的操作。我们将需要重复关于工具使用的注册,以及那个聊天。
player_white = ConversableAgent(
name="player_white",
system_message="""You are a chess player and you play as white. First call get_legal_moves to get a list of legal moves, then call make_move to make a move.
After a move is made, chat to make the game fun.""",
llm_config={"config_list": [{"model": "gpt-4", "api_key": os.environ.get("OPENAI_API_KEY")}]},
)


player_black = ConversableAgent(
name="player_black",
system_message="""You are a chess player and you play as black. First call get_legal_moves to get a list of legal moves, then call make_move to make a move.
After a move is made, chat to make the game fun.""",
llm_config={"config_list": [{"model": "gpt-4", "api_key": os.environ.get("OPENAI_API_KEY")}]},
)


最后,我们将重新创建棋盘并重新开始对话。顺便说一下,我们还在嵌套聊天中添加了一个标志summary_method="last_msg",所以这次我们不需要检查内部对话,我们只想看到两个玩家智能体之间的外部对话。
让我们看看。这次我们看到player_white走了第一步棋,不仅走了棋,player_white还说:“让我们看看你如何应对这个经典开局。”这次player_black走了兵并说:“我将我的兵移动到c5,选择西西里防御,该你了。你在这场国际象棋战斗中计划下一步采取什么策略?”他们开始互相挑战。player_white回应道:“我刚刚将我的兵移动到d4,将游戏推入了西西里防御的公开争夺领域。中心正在升温,我很好奇你将如何回应。”他们继续制造乐趣并进行下去。

你可以自己尝试这个例子,并自行修改,让它们表现不同。
总结

在本节课中,我们一起学习了如何在AutoGen中使用工具。我们构建了一个会话式国际象棋游戏,演示了在嵌套聊天中使用工具的一种特定方式。这并非唯一的方式,但一般来说,当你使用工具时,你需要一个能够提议使用工具的智能体,和另一个能够执行该工具的智能体。如果你想将这种提议和使用工具隐藏在嵌套聊天内部,你可以以类似的方式进行。当我们这样做时,从外部看,我们不一定需要知道涉及两个智能体,我们可以简单地创建一个智能体的体验,该智能体能够利用工具并在其他应用程序中执行某些功能。在其他应用程序中,你可能希望明确展示这种工具提议和工具执行的分离。

在下一课中,我们还将演示其他使用工具的方式,例如,使用自由格式代码,或让智能体建议代码,或让智能体在更自由的编码风格中消费用户定义的函数。
006:代码编写与金融分析 📈💻



在本节课中,我们将学习如何为智能体添加代码编写能力,并利用生成的代码来完成一项金融分析任务。我们将构建两个智能体系统,让智能体在请求人类反馈的同时协作解决手头的任务。

概述
上一节我们介绍了如何使用工具,但许多模型不具备相同的功能,或者有时我们希望智能体能够创造性地编写自由风格的代码,而不仅仅是使用某些预定义的函数。本节中,我们将学习如何进行自由形式的代码编写,包括如何创建能够生成代码以完成金融分析任务的智能体,以及如何构建两个智能体协作并请求人类反馈的系统。
我们将创建两个这样的系统。第一个系统将要求大语言模型智能体完全自主生成代码。第二个系统则会利用用户提供的代码。我们将使用GPT-4 Turbo来完成这个示例。
创建代码执行器
首先,我们需要创建一个代码执行器。我们从AutoGen导入一个名为LocalCommandLineCodeExecutor的代码执行器。
from autogen.coding import LocalCommandLineCodeExecutor
AutoGen中有几种不同类型的代码执行器,包括基于Docker的执行器和基于Jupyter节点的执行器。在本例中,我们将简单地使用本地命令行代码执行器。
我们将创建一个执行器,并设置一些配置。例如,我们将超时时间限制为60秒,并将工作目录设置为“coding”。这样会在“coding”文件夹中创建一个工作目录,所有生成的代码和中间结果都可以在该文件夹中找到。
executor = LocalCommandLineCodeExecutor(
timeout=60,
work_dir="coding"
)
创建智能体
接下来,我们将创建智能体。我们从AutoGen导入ConversableAgent及其派生类AssistantAgent。


from autogen import ConversableAgent, AssistantAgent
首先,我们创建一个能够执行代码的智能体。我们使用一个名为code_executor_agent的ConversableAgent。这个智能体不使用大语言模型,但会使用代码执行器来执行代码。我们将其human_input_mode设置为“ALWAYS”,这样它在执行代码之前总会请求人类输入确认。如果这个智能体在收到的消息中没有找到任何代码,它会回复“请继续,如果一切完成,请回复TERMINATE”。
code_executor_agent = ConversableAgent(
name="code_executor_agent",
llm_config=False,
code_execution_config={"executor": executor},
human_input_mode="ALWAYS",
default_auto_reply="请继续,如果一切完成,请回复TERMINATE。"
)

接下来,我们创建一个能够编写代码的智能体。我们创建一个AssistantAgent,它是ConversableAgent的一个特殊类,带有一个默认的系统消息,说明如何编写代码以及如何处理不同条件来解决任务。我们将其命名为code_writer_agent,并为其提供大语言模型配置。我们设置code_execution_config为False,因此这个智能体只会提议代码而不会执行它们。我们将其human_input_mode设置为“NEVER”,因此这个智能体永远不会请求人类输入。


code_writer_agent = AssistantAgent(
name="code_writer_agent",
llm_config={"config_list": [{"model": "gpt-4-turbo"}]},
code_execution_config=False,
human_input_mode="NEVER"
)

如果你对默认的系统消息感到好奇,我们可以打印出来查看。这是一个相当长的系统消息,包含了关于如何使用编码和语言技能解决任务的全面指导。它建议了几种应该建议使用Python代码或Shell脚本的情况,例如当智能体需要收集信息时,可以使用代码来获取信息;当智能体需要使用代码执行某些任务时,我们要求智能体使用代码来完成任务并输出结果。我们还提供了关于建议计划的指导,并明确哪些步骤使用代码,哪些步骤使用语言技能。我们指导了在使用代码时需要遵循的特定格式,并且让智能体知道用户只会执行代码。智能体应检查执行结果,如果结果指示有错误,则修复错误并再次输出代码。最后,我们要求智能体在一切完成时回复TERMINATE。这是AssistantAgent的默认系统消息,你当然可以自定义它,但我们发现这个默认消息在许多编码任务中效果很好。

定义任务并启动对话


现在,我们继续前进。我们已经准备好了这两个智能体,可以让它们处理一些任务了。让我们尝试定义一个关于金融分析的任务。我们导入datetime库,获取今天的日期,并将任务定义为“创建一个图表,显示英伟达和特斯拉年初至今的股票涨幅,并确保将图表保存到文件中”。
import datetime
today = datetime.date.today()
task = f"今天是 {today},创建一个图表,显示英伟达和特斯拉年初至今的股票涨幅。确保代码在Markdown代码块中,并将图形保存到文件。"
现在,我们可以从代码执行器智能体开始对话,代码执行器智能体将向代码编写器智能体发送初始消息。
chat_result = code_executor_agent.initiate_chat(
code_writer_agent,
message=task
)
代码编写器智能体使用大语言模型(我们使用的是GPT-4 Turbo)来生成响应。以下是回复:代码编写器智能体建议了如何使用代码解决此任务的步骤,包括使用yfinance获取股票价格数据、计算股票涨幅、使用matplotlib绘制股票涨幅并将图表保存到文件。代码按照该计划建议,包含了所有步骤。现在,用户可以验证代码,如果没有问题,我们可以简单地按Enter键跳过反馈并使用自动回复。
现在,代码执行器将执行该函数,生成输出,并将该输出发送回代码编写器智能体。代码脚本成功执行并生成了图表。我们可以检查文件是否确实已生成。由于代码执行器设置了TERMINATE,我可以输入“exit”来停止对话。让我们查看图表。
from IPython.display import Image
Image(filename="coding/stock_gain_plot.png")
这是由两个智能体生成的图表文件。看起来不错。这是一种让智能体自由编写代码的方法,我们只需要AutoGen的默认助手智能体和另一个代码执行智能体来执行此任务。
使用用户自定义代码
如果你想对如何完成此任务有更多控制,你可以提供一些已经编写的代码,并让智能体使用它们,而不是完全由自己编写代码。接下来,我们将演示如何做到这一点。
假设我们已经有一个关于获取股票价格的函数,我们可以定义它。例如,我们可以导入yfinance并下载数据,然后返回数据。我们还希望为此函数添加文档字符串,以便智能体更好地了解如何使用此函数。
def get_stock_prices(ticker, start_date, end_date):
"""
获取指定股票代码在给定日期范围内的股票价格数据。
参数:
ticker (str): 股票代码,例如 'NVDA' 或 'TSLA'。
start_date (str): 开始日期,格式为 'YYYY-MM-DD'。
end_date (str): 结束日期,格式为 'YYYY-MM-DD'。
返回:
pandas.DataFrame: 包含股票价格数据的DataFrame。
"""
import yfinance as yf
data = yf.download(ticker, start=start_date, end=end_date)
return data
假设我们还希望提供关于如何绘制股票价格的函数。我们可以再次提供该函数,并附带文档字符串和函数定义。


def plot_stock_prices(data_dict, save_path="stock_prices_plot.png"):
"""
绘制多个股票的价格走势图并保存。
参数:
data_dict (dict): 一个字典,键为股票名称,值为对应的pandas.DataFrame价格数据。
save_path (str): 保存图表的文件路径。
"""
import matplotlib.pyplot as plt
plt.figure(figsize=(10, 6))
for name, data in data_dict.items():
plt.plot(data.index, data['Close'], label=name)
plt.title('Stock Prices Year-to-Date')
plt.xlabel('Date')
plt.ylabel('Price (USD)')
plt.legend()
plt.grid(True)
plt.savefig(save_path)
plt.close()

现在,假设我们希望智能体仅使用这两个用户定义的函数来执行任务。我们如何让智能体知道这一点呢?我们可以通过添加functions参数来更改本地命令行代码执行器的创建,将这两个函数以列表形式提供给配置。代码执行器将能够执行这些函数。同时,我们需要让代码编写器智能体知道这两个函数的存在。我们通过修改代码编写器智能体的系统消息来实现,即在原有消息后附加一些包含这两个函数知识的额外提示。

# 更新执行器配置,包含用户定义的函数
executor_with_funcs = LocalCommandLineCodeExecutor(
timeout=60,
work_dir="coding",
functions=[get_stock_prices, plot_stock_prices]
)

# 获取格式化后的函数提示,并将其添加到助手智能体的系统消息中
func_prompt = executor_with_funcs.format_functions_for_prompt([get_stock_prices, plot_stock_prices])
new_system_message = code_writer_agent.system_message + "\n\n" + func_prompt




# 使用新的系统消息重新创建代码编写器智能体
code_writer_agent_with_funcs = AssistantAgent(
name="code_writer_agent_with_funcs",
llm_config={"config_list": [{"model": "gpt-4-turbo"}]},
system_message=new_system_message,
code_execution_config=False,
human_input_mode="NEVER"
)
# 使用更新后的执行器重新创建代码执行器智能体
code_executor_agent_with_funcs = ConversableAgent(
name="code_executor_agent_with_funcs",
llm_config=False,
code_execution_config={"executor": executor_with_funcs},
human_input_mode="ALWAYS",
default_auto_reply="请继续,如果一切完成,请回复TERMINATE。"
)
执行后,我们看到系统消息的第一部分与之前相同,但之后我们添加了一些关于“您可以访问以下用户定义函数”的知识。它们可以通过函数名从名为functions的模块中访问。例如,如果有一个名为get_stock_prices的函数,您可以通过编写from functions import get_stock_prices来导入它。现在,我们将具体的函数签名和文档字符串添加到了系统消息中。当你调用executor.format_functions_for_prompt时,这会根据函数定义自动完成,因此用户无需额外工作,只需以正常方式编写Python函数即可。通过此命令,我们将自动将该系统消息注册到原始系统消息中。
现在,我们可以使用新的系统消息再次创建代码编写器智能体。这次它将包含关于这两个用户定义函数的说明。我们还将使用更新后的代码执行器再次创建代码执行器智能体。
现在,如果我们再次尝试相同的任务,这次我们给出了相同的指令,只是将图形保存到不同的文件名。
task2 = f"今天是 {today},创建一个图表,显示英伟达和特斯拉年初至今的股票价格。确保代码在Markdown代码块中,并将图形保存到文件 'stock_prices_custom.png'。"
chat_result2 = code_executor_agent_with_funcs.initiate_chat(
code_writer_agent_with_funcs,
message=task2
)
当我们再次启动两个智能体聊天时,我们使用的是更新后的、可以访问用户提供函数的代码编写器和代码执行器。这次,我们应该期望代码编写器智能体利用用户定义的函数。让我们检查一下:我们可以先检查计划,代码编写器说我们首先要确定当前年份的开始日期,然后利用函数get_stock_prices下载数据,这是用户提供的函数。收集数据后,我们使用函数plot_stock_prices来创建图表。这也是用户提供的函数。因此,我们可以看到,在计划中,编写器智能体确实建议使用用户提供的两个函数。然后,让我们检查这次生成的代码:它调用from functions import get_stock_prices, plot_stock_prices,并基于这两个函数编写代码。这个functions模块的创建也是自动完成的。在这种情况下,代码要短得多,因为它直接使用用户提供的函数来完成这两个步骤,代码也更简洁。
我们将按Enter键跳过人工输入并使用自动回复。这次,代码执行器将再次执行代码。让我们结束对话并再次检查生成的图表。
Image(filename="coding/stock_prices_custom.png")
这次我们使用提供的股票价格绘图函数来绘制价格,而不是这两只股票的百分比变化。还要注意,我们不需要任何函数调用或底层模型的函数调用功能。因此,即使你使用的模型不是基于OpenAI或不支持函数调用功能,也能够使用用户定义的函数。
注意事项
我想提几点注意事项。首先,当我们检查Python代码块时,我们看到有一行关于文件名的内容。当这一行存在时,Python代码建议它将保存在指定编码目录下具有此文件名的文件中。但如果这一行不存在,代码将在执行后自动删除。
另一个注意事项是,如果我们发现代码不符合预期,并希望建议对代码进行一些更改,我们可以在代码执行器提示人工输入时提供该输入。例如,在这里,只有当我们跳过时,代码执行器才会执行代码。
代码执行与函数调用的比较
如果我们比较代码执行和函数调用,存在一些差异。代码执行是一种更灵活的方式,可以编写智能体能够编写的任何代码。它还具有使用用户定义代码的灵活性,类似于用户定义的工具或函数。但这种代码执行能力不要求底层模型具有函数调用功能。这两种不同的方式各有优缺点:对于函数调用,限制更多,但也更确定,因为你知道只有提供的函数会被执行,没有其他内容。对于代码执行,情况并非总是如此,这也意味着如果你希望智能体能够超越提供的函数,你可以使用代码执行来实现。总的来说,代码执行能力对于许多不同的任务非常有用。
总结

本节课中,我们一起学习了AutoGen智能体进行代码编写的两种方式:一种基于默认的AssistantAgent,另一种基于额外的用户提供函数。代码编写能力是一种非常通用的能力,可用于执行许多不同的任务。你可以探索更多关于不同类型的代码执行器,或者为不同的语言或不同的执行环境定义自己的代码执行器。我们还学习了人类有机会在代码执行前提供反馈,并且可以为这些智能体使用不同的人工输入模式。你可以让它们完全自动化,也可以让人参与其中。在下一课中,我们还将学习一个更复杂的示例,使用编码智能体、规划智能体和执行智能体,通过群聊(这也是我们将要学习的一种新的对话模式)来完成任务。
007:规划与股票报告生成 📈

在本节课中,我们将深入学习多智能体群聊,并构建一个自定义的群聊系统。该系统将协作生成一份关于过去一个月股票表现的详细报告。完成这个复杂任务需要引入“规划”这一设计模式,通过在群聊中加入一个规划智能体来实现。我们还将学习如何自定义群聊中的发言者转换顺序。


概述


上一节我们介绍了顺序对话模式。本节我们将探索一种更动态的协作模式——群聊。在这种模式下,多个智能体可以自主交互,共同解决复杂任务,而无需开发者预先设计每一步的具体执行顺序。我们将通过一个生成股票报告的例子来演示如何创建包含规划者、工程师、执行者和写手在内的多智能体群聊系统。
代码实现
让我们开始编写代码。首先,定义模型配置,我们将使用GPT-4 Turbo。
config_list = [{"model": "gpt-4-turbo", "api_key": "YOUR_API_KEY"}]
接下来,定义我们的任务。这是一个相当复杂的任务:生成一份关于过去一个月股票表现的详细博客文章。
task = "Generate a detailed report on the stock performance of Tesla (TSLA) over the past month, formatted as a blog post."
在之前的课程中,我们学习了顺序对话模式,它可以执行多步骤任务,但这需要人工设计具体的步骤和每个步骤涉及的智能体。如果我们想简化问题呢?如果我们只想定义智能体,并让它们协同工作来解决这个任务,而不为每一步提供非常具体的详细指令,该怎么做?在本课中,让我们尝试一种名为“群聊”的新对话模式,它不需要手动设计的步骤。
首先,导入AutoGen库。
import autogen
然后,尝试创建此任务所需的几个智能体。让我们思考一下需要什么。
首先,我们需要一个用户代理智能体,它可以向其他智能体发送关于初始任务的信息。
user_proxy = autogen.UserProxyAgent(
name="Admin",
system_message="Give the task and send instructions to writer to refine the blog post.",
code_execution_config=False,
llm_config={"config_list": config_list},
human_input_mode="ALWAYS"
)
我们设置human_input_mode为ALWAYS,因此当轮到该智能体发言时,它将始终请求人工输入。如果人工跳过输入,它将代表用户向写手智能体发送指令以完善博客文章。
考虑到这是一个复杂的任务,我们可能需要一个规划智能体来帮助我们将任务分解为更简单、更小的子任务,并将这些子任务发送给其他智能体去解决。
那么,我们创建一个规划者智能体。这是一个可对话的智能体,名为“Planner”。我们给它一些指令。
planner = autogen.AssistantAgent(
name="Planner",
system_message="""Given a task, please determine what information is needed to complete the task. Please note that all information will be retrieved using Python code. Only suggest information that can be retrieved using Python code. After each step is done by others, check the progress and instruct the remaining steps. If a step fails, try to work around.""",
description="Planner. Given a task, determine what information is needed. After each step is done, check progress and instruct remaining steps.",
llm_config={"config_list": config_list}
)
你看到我添加了一个名为description的字段。description和system_message有什么区别?system_message是我告诉智能体的指令,只有这个智能体需要知道。而description是我用来让其他智能体了解这个智能体的角色的。例如,管理者可以根据描述来决定何时使用这个智能体。所以描述是:“规划者。给定一个任务,确定完成任务所需的信息。在每个步骤完成后,检查进度并指导剩余步骤。” 这更简略、更高层,并且从第三人称的角度,我们可以了解这个智能体是做什么的。
规划者可能会建议一些需要使用Python代码的任务,因此我们可能需要一些能够编写Python代码的智能体。
让我们定义一个名为“Engineer”的智能体。我们将使用AutoGen中的默认助手智能体作为这个智能体的类。这个默认的助手智能体将有一个关于编写代码的详细指令的默认系统消息,因此我们不需要提供额外的系统消息,但我们会添加一个描述。
engineer = autogen.AssistantAgent(
name="Engineer",
description="Engineer. Writes code based on the plan provided by the planner.",
llm_config={"config_list": config_list}
)
在工程师编写代码之后,我们需要一些智能体来执行代码,因此我们可能需要一个执行者。
executor = autogen.UserProxyAgent(
name="Executor",
system_message="Executor. Execute code.",
code_execution_config={
"last_n_messages": 3,
"work_dir": "coding",
"use_docker": False
},
human_input_mode="NEVER",
llm_config=False
)
我们将last_n_messages设置为3,因此这个智能体将回顾对话历史,并按从后往前的顺序查找第一条包含代码的消息,并执行该消息中的代码。我们设置工作目录为“coding”,并设置use_docker为False。
别忘了任务是关于撰写博客文章的。因此我们还需要一个负责写作的智能体。定义这个写手智能体。
writer = autogen.AssistantAgent(
name="Writer",
system_message="Please write blog in markdown format with relevant titles and put content in the markdown code block. Take feedback from the admin and refine the blog.",
description="Writer. Writes blog posts in markdown format.",
llm_config={"config_list": config_list}
)
现在我们已经创建了这些智能体。你可以看到,当我创建每个智能体时,我主要考虑这个智能体需要扮演什么角色,以及为了启动这个任务,我需要创建哪些角色的智能体。但我不需要提供关于哪个智能体应该先做什么的详细指令。我只需将它们放在群聊中。
在这里,我使用刚刚创建的智能体创建一个群聊。
groupchat = autogen.GroupChat(
agents=[user_proxy, planner, engineer, executor, writer],
messages=[],
max_round=10
)
有五个智能体。我提供了一个空的初始消息列表,并将最大轮数设置为10。



在我让这些智能体工作之前,我还需要创建一个群聊管理器。这是AutoGen中管理群聊的特殊智能体。这个群聊管理器也需要大语言模型配置。



manager = autogen.GroupChatManager(
groupchat=groupchat,
llm_config={"config_list": config_list}
)
我只需定义这些智能体并将它们放入群聊中,然后我就可以通过启动用户代理和管理器之间的聊天来开始对话,因为我希望第一条消息来自用户代理,消息内容将是我们之前定义的任务。
user_proxy.initiate_chat(
manager,
message=task
)

在我开始对话之后,所有这些智能体将自动协同工作,无需开发者指定具体的发言者顺序。
第一条消息将从用户代理发送给聊天管理器,内容是关于任务的。聊天管理器会将此消息广播给其他每个智能体,因此群聊中的每个智能体都会看到这条消息。然后,聊天管理器将使用其大语言模型选择一个智能体作为下一个发言者。在决定谁下一个发言时,聊天管理器会查看当前的对话历史和每个智能体的角色,并决定哪个是最佳选择。

我们看到规划者被选为第一个发言的智能体。规划者建议了几个步骤,包括:检索股票数据、分析股票数据、研究相关事件、起草博客文章,最后重复第一步并建议一些操作。


接下来,聊天管理器需要选择下一个发言者,它选择了工程师。因此,工程师将遵循规划者对第一步的建议,并建议一段用于检索股票数据的代码。
然后,执行者被聊天管理器选中来执行代码并输出数据。现在,在第一步完成后,规划者再次被聊天管理器选中。它建议了接下来的步骤:分析股票数据、研究相关事件等。它实际上建议了一个Python代码示例来开始第二步,因此工程师将完成该Python代码。

执行者被聊天管理器选中来运行代码并输出结果。你不仅可以看到股票价格输出,还可以看到每日变化。



当前两步完成后,我们看到写手被聊天管理器选中开始起草博客文章,因为写手已经有足够的信息开始写作。它以Markdown格式撰写博客文章,包含引言部分、股价分析、股票表现部分、重大事件及其影响以及结论。
现在,管理员被聊天管理器选中。这个管理员将征求用户的反馈。如果我对这篇博客文章不满意,我可以在这里提供我的输入。我打算跳过我的反馈,让这个用户代理智能体代表我提出一些修改建议。
现在,这个用户代理智能体正在使用大语言模型来生成回复。这个回复应该包含一些关于这篇博客文章需要改进的建议。

因此,管理员提出了一些改进建议,包括添加可视化、详细分析、互动元素等。之后,写手应该接受该反馈并自动提出修改后的博客文章。
在这次对话中,我们看到所有智能体都由群聊管理器自动触发,并遵循预定义的角色和描述。通常,角色和描述需要精心设计,以确保它们能够遵循正确的顺序。
现在我们看到写手已经对博客文章进行了一些修订,它还总结了博客文章中增强的元素。如果我们增加群聊的轮数,可能会有更多的对话。对话在这里停止,因为我们将最大轮数设置为10。
如果我们回顾这次对话,我们会发现规划者确实扮演了建议初始计划的角色,并且在某些步骤完成后,它会检查进度并建议后续步骤。但并非所有步骤都被完全遵循。在前两步之后,我认为写手跳过了第三步,直接开始撰写博客文章。这是使用大语言模型决定发言顺序的一个缺点。

但是,有几种方法可以为对话添加更多控制。例如,你可以设置关于发言者顺序的约束。让我展示如何做到这一点。
首先,重复相同的智能体定义。
# 重新定义相同的智能体
user_proxy = autogen.UserProxyAgent(...)
planner = autogen.AssistantAgent(...)
engineer = autogen.AssistantAgent(...)
executor = autogen.UserProxyAgent(...)
writer = autogen.AssistantAgent(...)
我将演示如何在发言顺序中添加一些约束。

在这个例子中,我创建一个带有额外约束的群聊。这是一个名为allowed_or_disallowed_speaker_transitions的字典。对于每个智能体,你可以指定允许在该特定智能体之后发言的智能体。
例如,我们限制工程师之后的发言者只能是用户代理或执行者。我们限制执行者之后的发言者只能是用户代理、工程师或规划者。


allowed_transitions = {
engineer: [user_proxy, executor],
executor: [user_proxy, engineer, planner],
# 其他智能体可以自由发言,或根据需要添加更多约束
}
groupchat_with_constraints = autogen.GroupChat(
agents=[user_proxy, planner, engineer, executor, writer],
messages=[],
max_round=10,
allowed_or_disallowed_speaker_transitions=allowed_transitions,
speaker_transitions_type="allowed" # 指定这是允许的转换列表
)

在我们施加这个约束之后,我们不应该看到写手在工程师或执行者之后立即发言。这给了规划者在写手开始撰写博客文章之前审查计划的机会。你可以指定允许或禁止的发言者转换。在这种情况下,我们只设置允许的转换。
在定义之后,我们应该看到一个更符合我们期望的发言顺序。前几个步骤是相同的。但现在,在执行者完成第二步之后,我们应该看到规划者接管。它审查之前的步骤并建议接下来的步骤。这修复了我之前提到的问题。



你看到,尽管我们添加了一些约束,但整体任务完成仍然是非线性的。它仍然保持了智能体在需要时来回切换和插话的灵活性。因此,这仍然比顺序聊天具有更多的灵活性。
这种指定发言者转换的方式可以模拟群聊中的有限状态机转换,因此它是允许开发者添加更多控制的高级功能。你也可以在智能体的描述中添加一些自然语言指令,这样你不仅可以指定约束,还可以获得关于何时转换到哪个智能体的更多细节。
此外,你还可以使用编程语言定义精确的转换顺序。本节课不涵盖这一点,但你可以在AutoGen的网站上找到相关信息。
总结
本节课我们一起学习了“规划”这一智能体设计模式,并探索了“群聊”这一新的对话模式。

- 群聊模式:提供了一种更动态的方式,让多个智能体无需人类开发者设计非常详细的执行计划即可协同解决任务。
- 规划智能体:可以在群聊中引入,以帮助制定计划和任务分解。
- 发言控制:通过
allowed_or_disallowed_speaker_transitions参数,我们可以为群聊添加约束,模拟状态机转换,从而在保持灵活性的同时增加对对话流程的控制。

总的来说,任务分解和规划可以有多种不同的方式,我们只展示了一个特定的例子。关于AutoGen的基本介绍,这就是我们涵盖的所有课程内容。AutoGen还有许多其他高级功能,例如教智能体随时间改进、多模态、理解图像的视觉能力、使用开源模型作为智能体的后端等等。我们还有基于智能体的评估工具和基准测试工具,以及其他新的有趣研究,例如帮助用户为特定任务设计智能体。请从我们的网站查找这些高级新功能或正在进行的研究,并阅读博客文章。希望你喜欢到目前为止的课程,并能在自己的实践中探索更多有趣的用例。
008:总结与展望 🎯
在本课程中,我们学习了利用AutoGen框架构建人工智能智能体的几种核心设计模式。现在,让我们对所学内容进行回顾,并了解如何进一步探索。
课程内容回顾
上一节我们介绍了各种智能体协作模式,本节中我们来对整个课程进行总结。
在本节课中,我们学习了AutoGen中的几种智能体设计模式。
- 多智能体协作:多个智能体通过对话与合作共同完成任务。
- 反思:智能体对自身或他人的输出进行审查与改进。
- 工具使用:智能体调用外部工具(如代码执行器、搜索引擎)来获取信息或执行操作。
- 代码生成:智能体根据需求生成可执行的代码。
- 规划:智能体将复杂任务分解为可执行的子步骤序列。

你可以将这些基础构建模块组合起来,构建富有创意的应用程序,以解决非常复杂的任务。
进阶学习与社区
以下是进一步学习和获取支持的途径。

- 访问官方网站:查看我们的网站,以了解本课程未涵盖的更多高级功能。
- 感谢开源社区:感谢出色的开源社区让这一切成为可能。
- 加入Discord社区:加入我们拥有超过16,000名成员的Discord社区,我期待看到你构建的作品。
本节课中我们一起学习了AutoGen的核心智能体设计模式,包括多智能体协作、反思、工具使用、代码生成和规划。掌握这些模式是构建强大AI应用的基础。请利用提供的资源继续你的探索与构建之旅。


浙公网安备 33010602011771号