这篇文章瞄准的是AutoGen框架官方教程中的 Tutorial
章节中的 Teams
小节,主要介绍了如何构建一个多Agent系统并在恰当时间暂停、恢复、终止整个Team的动作。
- 官网链接:https://microsoft.github.io/autogen/stable/user-guide/agentchat-user-guide/tutorial/teams.html# ;
Teams
现代Agent框架离不开多Agent结构,在smolagents系列笔记中已经对多Agent进行了详细描述,这里只简单提一下为什么需要多Agent,以及能否通过单一Agent+一大堆Tools的方式代替多Agent。
为什么需要多Agent主要有以下几个理由:
1. 认知与任务分工
- 多Agent结构:每个Agent负责特定任务,拥有专门的思维链(CoT)和推理流程,可以基于自己的领域知识进行决策。这样可以提高模块化和可维护性。
- 单Agent+Tools:单一Agent需要掌握所有Tools的调用逻辑,容易导致推理链条过长,增加了错误传播的可能性。
如果使用单Agent+Tools方案,Agent需要决定何时调用哪个工具,而不是多个Agent并行推理后再进行交互,这可能导致更低的决策效率。
2. 并行处理与效率
- 多Agent结构:不同的Agent可以同时运行,提高并行计算能力,减少单一Agent的负担,特别适合分布式环境;
- 单Agent+Tools:工具的调用通常是串行执行的,这会导致任务处理时间较长,特别是在大规模任务中表现不佳;
例如如果你需要无人机在实时根据目标检测计算速度控制命令,又想要有一个实时对当前画面描述的功能,那么多Agent提供的并行模式更适合。
3. 复杂推理任务的局限性
- 多Agent结构:可以模拟多主体协作每个Agent都能独立决策,适用于复杂推理任务;
- 单Agent+Tools:Agent在调用工具时,仍然是基于单一思维链进行决策,难以形成多种推理角度的竞争和优化,单一Agent需要 不断切换Tools,并且缺乏跨任务优化的能力;
4. 灵活性与可扩展性
- 多Agent结构:可以轻松添加或移除Agent,而不影响整个系统架构;
- 单Agent+Tools:由于思维链是单条的,如果要对其中某个部分进行修改或替换就要去考虑前后甚至整条链路的稳定性,极大增加了debug的难度;
5. 失败隔离与容错性
- 多Agent结构:如果某个Agent失败,整条链路仍然可以通过其他Agent完成部分任务,提高鲁棒性;
- 单Agent+Tools:如果核心Agent出错,整个系统可能瘫痪,并且难以隔离故障。
在上面的因素中其实主要是因为第一点,即 “认知与任务分工”,Agent框架设计的一个核心原则就是尽量避免思维链过长,如果你经常使用GPT等问答软件会体会到这么一个情况:如果你的问答过程在初期出现轻微错误,经历过一段次数的轮训后LLM给出的答案会越来越离谱,特别是对于代码生成而言,在没有提供有效报错信息的情况下,LLM很容易生成一堆不存在的函数调用。
Creating a Team & Running a Team
AutoGen提供了一个RoundRobinGroupChat
对象用于快速组建多Agent结构,并且内部所有的Agent都 共享相同的上下文,并以循环方式轮流响应,同时每个Agent在轮询期间都会将LLM的相应结果 广播 给其他所有的Agent以保证整个团队 上下文一致。
官方在这提供了一个demo,其功能如下:
primary_agent
用来向LLM提出要求,让其写一段诗歌;critic_agent
用来给对诗歌进行打分,如果满足其要求则用APPROVE
作为标志位结尾;text_termination
用来约束轮询次数,即检测生成的内容尾部是否有APPROVE
标志位;
两个Agent都调用了OpenAI的在线模型gpt-4o-2024-08-06
,总体流程大致如下:
对官网示例进行稍微修改以便更清晰查看内容:
import os, asyncio
from autogen_agentchat.agents import AssistantAgent
from autogen_agentchat.conditions import TextMentionTermination
from autogen_agentchat.teams import RoundRobinGroupChat
from autogen_ext.models.openai import OpenAIChatCompletionClient
os.environ["OPENAI_API_KEY"] = "你的OpenAI API Key"
# 模型client
model_client = OpenAIChatCompletionClient(
model="gpt-4o-2024-08-06",
# api_key="sk-...", # Optional if you have an OPENAI_API_KEY env variable set.
)
# 主Agent,用来让模型client生成诗歌
primary_agent = AssistantAgent(
"primary",
model_client=model_client,
system_message="You are a helpful AI assistant.",
)
# 评判Agent,用来将主Agent得到的诗歌再传递给模型client并得到打分信息
critic_agent = AssistantAgent(
"critic",
model_client=model_client,
system_message="Provide constructive feedback. Respond with 'APPROVE' to when your feedbacks are addressed.",
)
# 轮训控制器,以 APPROVE 关键字为结尾控制轮训次数
text_termination = TextMentionTermination("APPROVE")
# 多Agent组
team = RoundRobinGroupChat([primary_agent, critic_agent], termination_condition=text_termination)
result = asyncio.run(team.run(task="Write a short poem about the fall season."))
# 打印每轮生成与评价内容
print('-' * 50)
for iter in result.messages:
print(iter.content)
print('-' * 50)
print(result.stop_reason)
运行结果如下:
$ pyhton demo.py
Observing a Team
上面的demo运行后是等待一段时间,然后一次性将全部的结果都返回,如果想要清晰看到整个Agent Team之间的交互过程,那么可以用流式方法run_stream()
。
【注意】:官网demo因为使用的是jupyter notebook运行的,所以在Console()函数之前对team进行了一次reset重置,但如果你像我一样使用的是纯脚本方式那么就需要注释掉这句话,因为reset()调用必须在run()调用之后。
在这里官网提拱了两种运行方式,上半部分与上面那个demo一致,只有下半部分在run()
调用的时候存在区别,下面这段代码如果你想分别运行体验不同的方式则注意注释掉另一种:
import os
import asyncio
from autogen_agentchat.agents import AssistantAgent
from autogen_agentchat.conditions import TextMentionTermination
from autogen_agentchat.teams import RoundRobinGroupChat
from autogen_ext.models.openai import OpenAIChatCompletionClient
# 与上面那个demo代码一致
os.environ["OPENAI_API_KEY"] = "你的OpenAI API Key"
model_client = OpenAIChatCompletionClient(
model="gpt-4o-2024-08-06",
)
primary_agent = AssistantAgent(
"primary",
model_client=model_client,
system_message="You are a helpful AI assistant.",
)
critic_agent = AssistantAgent(
"critic",
model_client=model_client,
system_message="Provide constructive feedback. Respond with 'APPROVE' when your feedbacks are addressed.",
)
text_termination = TextMentionTermination("APPROVE")
team = RoundRobinGroupChat([primary_agent, critic_agent], termination_condition=text_termination)
#---------------------------------------------------------------------------#
# 需要额外导入的包
from autogen_agentchat.ui import Console
from autogen_agentchat.base import TaskResult
# 官网提供的方式一:
async def main():
async for message in team.run_stream(task="Write a short poem about the fall season."):
if isinstance(message, TaskResult):
print("Stop Reason:", message.stop_reason)
else:
print(message)
# 官网提供的方式二:
async def main():
await team.reset()
await Console(team.run_stream(task="Write a short poem about the fall season.")) # 直接运行 team.run_stream()
if __name__ == '__main__':
asyncio.run(main())
方式二运行结果:
$ python demo.py
Resetting a Team
AutoGen提供了一个重置Agent Team的方法 reset()
,其作用是让整个Agent Team忘记所有上下文以及历史状态,这里还需要注意一下(其实上面已经提到了),reset()
函数必须在 run()
函数之后调用,否则会抛出以下异常:
$ RuntimeError: The group chat has not been initialized. It must be run before it can be reset.
根据我个人的使用经验,建议在以下两种情况中使用 reset()
方法:
- 下一个任务与之前 完全不相关;
- 模型生成的错误累计过多,需要完全重制时;
Stopping a Team
暂停Team通常只在并行任务中有价值,因为串行任务本身就是阻塞等待的,如果你想在期间暂停基本等价于让当前功能暂停。当你在运行Agent Team的时候会出现以下两种需要暂停的情况:
- 这一轮输出的结果需要进行一次全体Agent阻塞判断:以上面写诗的例子为例,假设你得到了关键字
APPROVE
,这个时候你想要整个轮询暂停,人工介入判断一下质量,那么就需要所有的Agent都阻塞等待你的处理结果; - 同步Team内部Agent上下文:让执行快的Agent等等执行慢的Agent;
【注意】:AutoGen中的暂停有以下几个特点
- 暂停时整个Team的上下文环境是保留的,随时可以恢复(但这个demo中只展示了暂停没展示恢复,下个demo有恢复);
- 暂停不是打断正在运行的Agent,而是等所有Agen 完成当前轮询 后暂停,这是为了防止强制中断LLM输出会抛出一堆异常,如果某个Agent当前没有在工作则可以直接暂停;
官方提供的写诗例子其实不太明显,因为从宏观上来看就是一条流式思维链,但前面提到过 AutoGen 支持 并行计算 能力,那么假设存在下面这么一个场景:
你组织了10个Agent 并行 生成封面、目录、段落、插图、音频等内容并进行排版,最终的目的是合并成一个有插图的小说文本+配套的音频文件;
因为不同Agent的最终产物不同,生成的速度肯定有快有慢,其中封面、目录这两块可以直接生成,不用理会其他Agent的进度,但是插图要根据段落生成,音频要根据段落生成,而排版要配合段落与插图进行组织,如果所有Agent不进行一定规则的等待全都各顾各的生成,那么一定会出现这种情况:排版混乱、插图内容与段落不符、音频因为排版混乱会将描述插图的“image”字段录进去。
官方提供了一个对象 ExternalTermination
以及其成员函数 set()
用来暂停整个Team,这里对官网demo进行少量修改以确保能够运行:
import os
import asyncio
from autogen_agentchat.agents import AssistantAgent
from autogen_agentchat.conditions import TextMentionTermination, ExternalTermination
from autogen_agentchat.teams import RoundRobinGroupChat
from autogen_ext.models.openai import OpenAIChatCompletionClient
from autogen_agentchat.ui import Console
os.environ["OPENAI_API_KEY"] = "你的OpenAI API Key"
model_client = OpenAIChatCompletionClient(
model="gpt-4o-2024-08-06",
)
primary_agent = AssistantAgent(
"primary",
model_client=model_client,
system_message="You are a helpful AI assistant.",
)
critic_agent = AssistantAgent(
"critic",
model_client=model_client,
system_message="Provide constructive feedback. Respond with 'APPROVE' when your feedbacks are addressed.",
)
text_termination = TextMentionTermination("APPROVE")
team = RoundRobinGroupChat([primary_agent, critic_agent], termination_condition=text_termination)
#------------------------------------------------------------------#
# 创建一个暂停条件
external_termination = ExternalTermination()
team = RoundRobinGroupChat(
[primary_agent, critic_agent],
termination_condition=external_termination | text_termination,
)
async def main():
# 异步1: 运行Agent开始生成诗歌
run = asyncio.create_task(Console(team.run_stream(task="Write a short poem about the fall season.")))
# 阻塞等待0.1s休眠
await asyncio.sleep(0.1)
# 手动触发暂停
external_termination.set()
# 等待所有Agent当前轮询任务结束
await run
if __name__ == '__main__':
asyncio.run(main())
运行结果如下:
$ python demo.py
# 其实在这里已经触发了暂停,但因为primary agent将生成任务已经发给模型了,所以这里就会等着轮结束后再暂停
# 此时critic agent还没有任何输入就可以被直接暂停
---------- user ----------
Write a short poem about the fall season.
---------- primary ----------
Golden leaves drift to the ground,
Whispering tales without a sound.
Crisp air nips at the morning light,
As day turns soft and early night.
Pumpkins grin with candle glow,
Fields rest where harvests grow.
Sweaters wrap in cozy embrace,
In fall’s gentle, fleeting chase.
Nature dons her fiery gown,
While acorns tumble, spiraling down.
The world transforms in hues so bright,
In autumn’s brief and wondrous flight.
Resuming a Team
上面讲了如何暂停一个Team,现在介绍如何恢复Team。AutoGen的恢复函数就是运行函数 run()
或 run_stream()
即可,这里我们将上面代码中的休眠时间该长一点能更清晰体验到,只需修改 main()
函数中的几行即可:
async def main():
# 异步1: 运行Agent开始生成诗歌
run = asyncio.create_task(Console(team.run_stream(task="Write a short poem about the fall season.")))
# 阻塞等待0.1s休眠
await asyncio.sleep(10)
# 手动触发暂停
external_termination.set()
# 等待所有Agent当前轮询任务结束
await run
# 恢复继续生成
run = asyncio.create_task(Console(team.run_stream()))
Aborting a Team
有暂停、恢复,那一定有终止,其实我们上面一直在用这个终止信号,TextMentionTermination
就是一种指定文本的终止判定依据,但这里要介绍的是如何手动终止Team,通过CancellationToken
对象的 cancle()
函数就可以手动立即停掉整个Team:
【注意】:AutoGen提供的终止有以下几个特点:
- 直接打断模型输出过程;
- 需要用
try except
借住因为打断而抛出的异常;
import os
import asyncio
from autogen_agentchat.agents import AssistantAgent
from autogen_agentchat.conditions import TextMentionTermination, ExternalTermination
from autogen_agentchat.teams import RoundRobinGroupChat
from autogen_ext.models.openai import OpenAIChatCompletionClient
from autogen_agentchat.ui import Console
from autogen_core import CancellationToken
os.environ["OPENAI_API_KEY"] = "你的OpenAI API Key"
model_client = OpenAIChatCompletionClient(
model="gpt-4o-2024-08-06",
)
primary_agent = AssistantAgent(
"primary",
model_client=model_client,
system_message="You are a helpful AI assistant.",
)
critic_agent = AssistantAgent(
"critic",
model_client=model_client,
system_message="Provide constructive feedback. Respond with 'APPROVE' when your feedbacks are addressed.",
)
text_termination = TextMentionTermination("APPROVE")
team = RoundRobinGroupChat([primary_agent, critic_agent], termination_condition=text_termination)
external_termination = ExternalTermination()
team = RoundRobinGroupChat(
[primary_agent, critic_agent],
termination_condition=external_termination | text_termination,
)
#------------------------------------------------------------------#
# 创建一个终止对象
cancellation_token = CancellationToken()
async def main():
run = asyncio.create_task(
team.run(
task="Translate the poem to Spanish.",
cancellation_token=cancellation_token,
)
)
cancellation_token.cancel() # 手动触发终止
try:
result = await run
except asyncio.CancelledError:
print("Task was cancelled.")
if __name__ == '__main__':
asyncio.run(main())
运行结果如下,这里因为没有异步等待直接终止了所以只会输出一个Task was cancelled
:
$ python demo.py
Task was cancelled.
Single-Agent Team
多Agent组成的Team能够很好的进行交互,但是AutoGen提供哪些暂停、恢复、终止的功能仅限于Team使用,如果想要让单一的Agent也具备这个功能最简单的方法就是只给Team添加一个Agent:
【注意】:官方demo中有一处错误 TextMessageTermination
已经被 TextMentionTermination
代替。
from autogen_agentchat.agents import AssistantAgent
from autogen_agentchat.conditions import TextMentionTermination
from autogen_agentchat.teams import RoundRobinGroupChat
from autogen_agentchat.ui import Console
from autogen_ext.models.openai import OpenAIChatCompletionClient
import os, asyncio
os.environ["OPENAI_API_KEY"] = "你的OpenAI API Key"
model_client = OpenAIChatCompletionClient(
model="gpt-4o",
parallel_tool_calls=False,
)
# 创建一个工具用来累加数字
def increment_number(number: int) -> int:
"""Increment a number by 1."""
return number + 1
# 定义一个Agent并提供累加工具
looped_assistant = AssistantAgent(
"looped_assistant",
model_client=model_client,
tools=[increment_number], # Register the tool.
system_message="You are a helpful AI assistant, use the tool to increment the number.",
)
# 创建一个指定文本结束标志位
termination_condition = TextMentionTermination("looped_assistant")
# 创建一个单一Agent的Team
team = RoundRobinGroupChat(
[looped_assistant],
termination_condition=termination_condition,
)
async def main():
async for message in team.run_stream(task="Increment the number 5 to 10."):
print(type(message).__name__, message)
if __name__== '__main__':
asyncio.run(main())
运行结果如下:
$ python demo.py