AutoGen 技术博客系列 (六):SelectorGroupChat 的原理与实践

185 阅读18分钟

AutoGen系列一:基础介绍与入门教程

AutoGen系列二:深入自定义智能体

AutoGen系列三:内置智能体的应用与实战

AutoGen系列四: 自定义智能体的高级技巧

AutoGen系列五: 智能体团队协作的深度剖析与实践

AutoGen系列六: SelectorGroupChat 的原理与实践

AutoGen 技术博客系列 (七):状态管理与组件序列化解析

AutoGen 技术博客系列 (八):# 深入剖析 Swarm—— 智能体协作的新范式

AutoGen 技术博客系列 (九):从 v0.2 到 v0.4 的迁移指南

一、SelectorGroupChat 简介

SelectorGroupChat 在 AutoGen 中的角色

SelectorGroupChat 是 AutoGen 中 AgentChat 应用接口里一种预设的 Team 类型,在多智能体协作完成复杂任务的场景中扮演着关键角色。它的核心特点是参与者(Agent)以推荐选择的方式轮流向所有参与者发布信息,每次消息发布后,都会使用 ChatCompletionClient(LLM)选择下一个发言者(Agent)。这一机制使得智能体之间的协作更加智能和灵活,能够根据任务的进展和当前的对话上下文动态地确定下一个执行任务的智能体。

例如,在一个智能项目管理场景中,有负责需求分析的智能体、负责技术方案设计的智能体以及负责项目进度跟踪的智能体。SelectorGroupChat 可以根据项目的初始需求,通过 LLM 判断首先由需求分析智能体发言,对需求进行详细解读和分解。当需求分析智能体完成发言后,LLM 会根据当前的对话内容和各个智能体的能力,选择最适合的智能体继续发言,可能是技术方案设计智能体来针对需求提出技术实现方案。这种动态的选择机制避免了固定顺序发言的局限性,提高了任务处理的效率和质量。

SelectorGroupChat 还具备一些可配置的属性,使得其在不同场景下具有更强的适应性。例如,通过设置selector_prompt属性,可以定制用于选择下一个发言者(Agent)的 prompt 模版,从而更精准地引导 LLM 做出选择。allow_repeated_speaker属性则决定是否允许连续选择同一个发言者(Agent),默认设置为False,即不允许,这有助于保证不同智能体都有机会参与到任务处理中,避免某个智能体过度占用资源。selector_func属性允许用户提供自定义选择器函数,用于获取对话历史记录并返回下一个发言者(Agent)。如果启动该功能,则 LLM 选择会失效;若该函数返回None,则 LLM 会主动接管进行下一位发言者(Agent)的选择,这为开发者提供了高度的自定义能力,能够根据具体的业务逻辑和需求来定制智能体的选择策略。

二、SelectorGroupChat 工作原理剖析

2.1 核心概念与关键特性

  • 基于模型的发言者选择机制:在 SelectorGroupChat 的运行过程中,模型会对当前的对话上下文进行深入分析,其中包括丰富的对话历史信息以及各个参与者(Agent)的关键属性,如名称和详细描述等。通过对这些信息的综合考量,模型能够运用其强大的语言理解和推理能力,智能地确定下一个最适合发言的 Agent。例如,在一个涉及医疗咨询的任务中,如果对话中提到了特定的症状,模型可能会选择具有医学专业知识的智能体进行回应,确保回答的准确性和专业性。
  • 高度可配置的参与者角色和描述:开发者在创建智能体时,可以为每个参与者精心定义独特的角色和详细的描述信息。这些信息不仅能够帮助模型更好地理解每个智能体的专长和能力范围,从而在选择发言者时做出更合理的决策,还能使整个协作系统更加透明和易于管理。比如,在一个金融分析项目中,可以明确设定一个智能体负责收集市场数据,另一个智能体专注于风险评估,它们的角色和职责通过清晰的描述得以界定,便于系统协调工作。
  • 灵活的防止同一发言者连续发言设置:为了确保对话的多样性和充分的信息交流,SelectorGroupChat 默认提供了防止同一发言者连续发言的功能。这一特性有效避免了某个智能体过度主导对话,促进了不同观点和信息的充分融合。当然,开发者也可以根据具体任务的特殊需求,通过简单地设置allow_repeated_speaker=True来灵活调整这一行为,以适应某些特定场景下可能需要同一智能体连续提供信息或进行深入解释的情况。
  • 可定制的选择提示和选择函数:为了满足开发者对选择过程的精细化控制需求,SelectorGroupChat 提供了可定制的选择提示功能,开发者可以根据任务的特点和期望的协作流程,设计独特的提示信息,引导模型做出更符合预期的选择。更重要的是,通过自定义选择函数selector_func,开发者能够完全覆盖默认的基于模型的选择逻辑。这为应对复杂多变的任务场景提供了强大的支持,例如在一些对实时性要求极高的任务中,开发者可以编写自定义函数,根据任务的紧急程度和智能体的响应速度来优先选择发言者,确保任务能够高效完成。

2.2 运行机制与执行流程

当 SelectorGroupChat 团队接收到一个任务(通过run()run_stream()方法触发)时,一系列复杂而有序的步骤随即展开:

  • 首先,团队会运用先进的自然语言处理技术和模型算法,全面而细致地分析当前的对话上下文。这包括对之前所有的对话历史进行梳理,提取关键信息和语义线索,同时结合各个智能体的名称、描述以及它们在之前对话中的表现等多维度信息,构建一个全面的任务理解模型。在此基础上,利用预训练的生成模型(如 GPT-4o 等强大的 LLM),根据既定的选择策略和逻辑,精准地确定下一个最有可能为任务推进提供有价值信息的发言者。例如,在一个涉及科技产品研发的讨论中,如果之前的对话中提到了技术难题,模型可能会优先选择具有相关技术背景的智能体进行回应,以提供专业的解决方案。
  • 接着,一旦确定了发言者,团队会向其发送明确的提示信息,要求该智能体基于当前的任务需求和对话背景生成相应的回应。智能体在接收到提示后,会运用自身的知识和推理能力,结合所配备的工具(如果有的话),生成一段包含丰富信息的回答。这个回答会被立即广播给团队中的所有其他参与者,确保每个智能体都能及时获取最新的信息,从而保持对话的连贯性和信息的一致性。例如,在一个市场调研任务中,负责数据分析的智能体可能会根据收集到的数据生成一份详细的报告,并将其分享给其他成员,以便共同探讨市场趋势和策略。
  • 最后,团队会严格按照预先设定的终止条件对对话的进展进行检查。这些终止条件可以是多种多样的,如达到最大消息数量(通过MaxMessageTermination设置)、特定文本被提及(如TextMentionTermination)等。如果满足了终止条件,对话将正式结束,团队会整理并返回包含完整对话历史的TaskResult对象,为后续的任务分析和总结提供宝贵的数据支持。如果尚未满足终止条件,整个过程将从第一步重新开始,循环往复,直至任务完成或达到终止条件为止。

2.3 源码层面解读

从源码角度深入剖析 SelectorGroupChat,可以更清晰地理解其内部实现机制。在 AutoGen 的代码库中,SelectorGroupChat 的核心实现主要涉及到几个关键的类和方法。

在SelectorGroupChat类的初始化方法中,我们可以看到对各种属性的设置,包括参与者(Agent)列表、模型客户端(model_client)、终止条件(termination_condition)等。这些属性的设置为 SelectorGroupChat 的运行提供了基础配置。

class SelectorGroupChat(BaseGroupChat):
    def __init__(
        self,
        participants: Sequence[Agent],
        model_client: ChatCompletionClient,
        termination_condition: TerminationCondition | None = None,
        max_turns: int | None = None,
        selector_prompt: str | None = None,
        allow_repeated_speaker: bool = False,
        selector_func: SelectorFunction | None = None,
    ):
        super().__init__(participants, termination_condition, max_turns)
        self.model_client = model_client
        self.selector_prompt = selector_prompt
        self.allow_repeated_speaker = allow_repeated_speaker
        self.selector_func = selector_func

在选择下一个发言者的过程中,_select_next_speaker方法起到了关键作用。如果用户提供了自定义选择器函数selector_func,则会调用该函数来选择下一个发言者;否则,会使用模型客户端(LLM)来进行选择。

    def _select_next_speaker(self, messages: Sequence[AgentEvent | ChatMessage]) -> str | None:
        if self.selector_func:
            return self.selector_func(messages)
        # 此处省略使用LLM选择发言者的具体逻辑

在处理对话历史和上下文方面,_update_conversation_history方法负责将每次的消息记录到对话历史中,以便后续的分析和处理。这样,智能体在进行决策时,能够充分利用之前的对话信息,实现更智能的协作。

    def _update_conversation_history(self, message: AgentEvent | ChatMessage):
        self.conversation_history.append(message)

对于终止条件的检查,_check_termination方法会在每次循环中被调用,判断当前的对话是否满足终止条件。如果满足,则停止对话流程,返回任务结果。

    def _check_termination(self) -> bool:
        if self.termination_condition:
            return self.termination_condition(self.conversation_history)
        return False

通过对这些关键源码的分析,我们可以看到 SelectorGroupChat 在实现过程中,充分利用了 Python 的面向对象编程特性,将各种功能模块化,使得代码结构清晰,易于维护和扩展。同时,通过合理地调用模型客户端和处理对话历史,实现了智能体之间的高效协作和任务的有效执行。

三、SelectorGroupChat 实践

3.1 环境搭建与准备工作

在开始使用 SelectorGroupChat 之前,需要进行一系列的环境搭建和准备工作。首先,确保已经安装了 Python 环境,并且版本符合 AutoGen 的要求(通常建议使用 Python 3.7 及以上版本)。接下来,通过pip命令安装 AutoGen 及其相关依赖库,如autogen_agentchatautogen_ext.models.openai等。

同时,由于示例中使用了 OpenAI 的模型,还需要配置 OpenAI API 密钥。这可以通过在系统环境变量中设置OPENAI_API_KEY来实现,确保在运行代码时能够正确访问 OpenAI 的服务。在获取 API 密钥时,需要在 OpenAI 官方网站上进行注册和申请,按照官方的指引完成相关步骤,获取到属于自己的 API 密钥,并妥善保管,避免泄露。

3.2 代码示例与详细解释

以下是一个基于上述文档中示例的完整代码示例,实现了一个简单的 NBA 球员数据查询和分析任务:

from typing import Sequence
from autogen_agentchat.agents import AssistantAgent
from autogen_agentchat.conditions import MaxMessageTermination, TextMentionTermination
from autogen_agentchat.messages import AgentEvent, ChatMessage
from autogen_agentchat.teams import SelectorGroupChat
from autogen_agentchat.ui import Console
from autogen_ext.models.openai import OpenAIChatCompletionClient

# 定义模拟的搜索工具函数,用于查询球员数据
def search_web_tool(query: str) -> str:
    if "2006-2007" in query:
        return """Here are the total points scored by Miami Heat players in the 2006-2007 season:
        Udonis Haslem: 844 points
        Dwayne Wade: 1397 points
        James Posey: 550 points
       ...
        """
    elif "2007-2008" in query:
        return "The number of total rebounds for Dwayne Wade in the Miami Heat season 2007-2008 is 214."
    elif "2008-2009" in query:
        return "The number of total rebounds for Dwayne Wade in the Miami Heat season 2008-2009 is 398."
    return "No data found."

# 定义计算百分比变化的工具函数
def percentage_change_tool(start: float, end: float) -> float:
    return ((end - start) / start) * 100

# 创建OpenAI模型客户端
model_client = OpenAIChatCompletionClient(model="gpt-4o")

# 创建规划智能体
planning_agent = AssistantAgent(
    "PlanningAgent",
    description="An agent for planning tasks, this agent should be the first to engage when given a new task.",
    model_client=model_client,
    system_message="""
    You are a planning agent.
    Your job is to break down complex tasks into smaller, manageable subtasks.
    Your team members are:
        Web search agent: Searches for information
        Data analyst: Performs calculations
    You only plan and delegate tasks - you do not execute them yourself.
    When assigning tasks, use this format:
    1. <agent> : <task>
    After all tasks are complete, summarize the findings and end with "TERMINATE".
    """
)

# 创建网页搜索智能体,并关联搜索工具
web_search_agent = AssistantAgent(
    "WebSearchAgent",
    description="A web search agent.",
    tools=[search_web_tool],
    model_client=model_client,
    system_message="""
    You are a web search agent.
    Your only tool is search_tool - use it to find information.
    You make only one search call at a time.
    Once you have the results, you never do calculations based on them.
    """
)

# 创建数据分析智能体,并关联计算工具
data_analyst_agent = AssistantAgent(
    "DataAnalystAgent",
    description="A data analyst agent. Useful for performing calculations.",
    model_client=model_client,
    tools=[percentage_change_tool],
    system_message="""
    You are a data analyst.
    Given the tasks you have been assigned, you should analyze the data and provide results using the tools provided.
    """
)

# 创建终止条件,当规划智能体发送“TERMINATE”或达到25条消息时结束对话
text_mention_termination = TextMentionTermination("TERMINATE")
max_messages_termination = MaxMessageTermination(max_messages=25)
termination = text_mention_termination | max_messages_termination

# 创建SelectorGroupChat团队,并传入智能体列表、模型客户端和终止条件
team = SelectorGroupChat(
    [planning_agent, web_search_agent, data_analyst_agent],
    model_client=OpenAIChatCompletionClient(model="gpt-4o-mini"),
    termination_condition=termination
)

# 定义任务,查询2006-2007赛季迈阿密热火队得分最高的球员,并计算其在2007-2008和2008-2009赛季篮板数的百分比变化
task = "Who was the Miami Heat player with the highest points in the 2006-2007 season, and what was the percentage change in his total rebounds between the 2007-2008 and 2008-2009 seasons?"

# 使用asyncio.run(...)在脚本中运行团队,并输出对话流
import asyncio
asyncio.run(Console(team.run_stream(task=task)))

在上述代码中:

  • 首先,通过OpenAIChatCompletionClient创建了与 OpenAI 模型的连接,指定了使用的模型为gpt-4o
  • 接着,分别创建了PlanningAgentWebSearchAgentDataAnalystAgent三个智能体,并为它们设置了详细的描述和系统消息。PlanningAgent负责将复杂任务分解为子任务,并按照规定格式分配给其他智能体;WebSearchAgent配备了search_web_tool工具,用于查询相关数据;DataAnalystAgent则拥有percentage_change_tool工具,用于进行数据计算。
  • 然后,定义了TextMentionTerminationMaxMessageTermination两个终止条件,并将它们组合成一个总的终止条件termination
  • 最后,创建了SelectorGroupChat团队,将智能体列表、模型客户端和终止条件传入,并通过team.run_stream(task=task)运行任务,同时使用Console类将对话流输出到控制台,以便观察任务的执行过程。

3.3 运行结果分析

当运行上述代码时,控制台会输出详细的对话过程和结果:

---------- user ----------
Who was the Miami Heat player with the highest points in the 2006-2007 season, and what was the percentage change in his total rebounds between the 2007-2008 and 2008-2009 seasons?
---------- PlanningAgent ----------
To address this request, we will divide the task into manageable subtasks. 
1. Web search agent: Identify the Miami Heat player with the highest points in the 2006-2007 season.
2. Web search agent: Gather the total rebounds for the identified player during the 2007-2008 season.
3. Web search agent: Gather the total rebounds for the identified player during the 2008-2009 season.
4. Data analyst: Calculate the percentage change in total rebounds for the identified player between the 2007-2008 and 2008-2009 seasons.
[Prompt tokens: 159, Completion tokens: 122]
---------- WebSearchAgent ----------
[FunctionCall(id='call_xdYlGP2lsqDeWdSiOlwOBNiO', arguments='{"query":"Miami Heat highest points player 2006-2007 season"}', name='search_web_tool')]
[Prompt tokens: 271, Completion tokens: 26]
---------- WebSearchAgent ----------
[FunctionExecutionResult(content='Here are the total points scored by Miami Heat players in the 2006-2007 season:\n        Udonis Haslem: 844 points\n        Dwayne Wade: 1397 points\n        James Posey: 550 points\n       ...\n        ', call_id='call_xdYlGP2lsqDeWdSiOlwOBNiO')]
---------- WebSearchAgent ----------
Tool calls:
search_web_tool({"query":"Miami Heat highest points player 2006-2007 season"}) = Here are the total points scored by Miami Heat players in the 2006-2007 season:
        Udonis Haslem: 844 points
        Dwayne Wade: 1397 points

Custom Selector Function 自定义选择器功能

代码案例:

from typing import Sequence
from autogen_agentchat.agents import AssistantAgent
from autogen_agentchat.conditions import MaxMessageTermination, TextMentionTermination
from autogen_agentchat.messages import AgentEvent, ChatMessage
from autogen_agentchat.teams import SelectorGroupChat
from autogen_agentchat.ui import Console
from autogen_ext.models.openai import OpenAIChatCompletionClient

# 定义模拟的搜索工具函数,用于查询球员数据
def search_web_tool(query: str) -> str:
    if "2006-2007" in query:
        return """Here are the total points scored by Miami Heat players in the 2006-2007 season:
        Udonis Haslem: 844 points
        Dwayne Wade: 1397 points
        James Posey: 550 points
      ...
        """
    elif "2007-2008" in query:
        return "The number of total rebounds for Dwayne Wade in the Miami Heat season 2007-2008 is 214."
    elif "2008-2009" in query:
        return "The number of total rebounds for Dwayne Wade in the Miami Heat season 2008-2009 is 398."
    return "No data found."

# 定义计算百分比变化的工具函数
def percentage_change_tool(start: float, end: float) -> float:
    return ((end - start) / start) * 100

# 创建OpenAI模型客户端
model_client = OpenAIChatCompletionClient(model="gpt-4o")

# 创建规划智能体
planning_agent = AssistantAgent(
    "PlanningAgent",
    description="An agent for planning tasks, this agent should be the first to engage when given a new task.",
    model_client=model_client,
    system_message="""
    You are a planning agent.
    Your job is to break down complex tasks into smaller, manageable subtasks.
    Your team members are:
        Web search agent: Searches for information
        Data analyst: Performs calculations
    You only plan and delegate tasks - you do not execute them yourself.
    When assigning tasks, use this format:
    1. <agent> : <task>
    After all tasks are complete, summarize the findings and end with "TERMINATE".
    """
)

# 创建网页搜索智能体,并关联搜索工具
web_search_agent = AssistantAgent(
    "WebSearchAgent",
    description="A web search agent.",
    tools=[search_web_tool],
    model_client=model_client,
    system_message="""
    You are a web search agent.
    Your only tool is search_tool - use it to find information.
    You make only one search call at a time.
    Once you have the results, you never do calculations based on them.
    """
)

# 创建数据分析智能体,并关联计算工具
data_analyst_agent = AssistantAgent(
    "DataAnalystAgent",
    description="A data analyst agent. Useful for performing calculations.",
    model_client=model_client,
    tools=[percentage_change_tool],
    system_message="""
    You are a data analyst.
    Given the tasks you have been assigned, you should analyze the data and provide results using the tools provided.
    """
)

# 创建终止条件,当规划智能体发送“TERMINATE”或达到 25 条消息时结束对话
text_mention_termination = TextMentionTermination("TERMINATE")
max_messages_termination = MaxMessageTermination(max_messages=25)
termination = text_mention_termination | max_messages_termination

# 定义自定义选择器函数,例如让数据分析智能体在网页搜索智能体之后发言,如果网页搜索智能体连续发言两次,则让规划智能体介入
def custom_selector_function(messages: Sequence[AgentEvent | ChatMessage]) -> str | None:
    last_two_speakers = [messages[-1].source, messages[-2].source if len(messages) > 1 else None]
    if web_search_agent.name in last_two_speakers and data_analyst_agent.name not in last_two_speakers:
        return data_analyst_agent.name
    elif last_two_speakers.count(web_search_agent.name) == 2:
        return planning_agent.name
    return None

# 创建SelectorGroupChat团队,并传入智能体列表、模型客户端、终止条件和自定义选择器函数
team = SelectorGroupChat(
    [planning_agent, web_search_agent, data_analyst_agent],
    model_client=OpenAIChatCompletionClient(model="gpt-4o-mini"),
    termination_condition=termination,
    selector_func=custom_selector_function
)

# 定义任务,查询 2006-2007 赛季迈阿密热火队得分最高的球员,并计算其在 2007-2008 和 2008-2009 赛季篮板数的百分比变化
task = "Who was the Miami Heat player with the highest points in the 2006-2007 season, and what was the percentage change in his total rebounds between the 2007-2008 and 2008-2009 seasons?"

# 使用 asyncio.run(...) 在脚本中运行团队,并输出对话流
import asyncio
asyncio.run(Console(team.run_stream(task=task)))

在这个案例中,custom_selector_function 函数实现了自定义的选择逻辑。它首先检查最近的两个发言者,如果是网页搜索智能体发言且数据分析智能体尚未发言,那么下一个选择数据分析智能体;如果网页搜索智能体连续发言两次,则选择规划智能体。这样可以根据任务的特点和智能体的表现,更加灵活地控制对话流程和任务分配。

运行结果分析:

当运行上述代码时,根据自定义选择器函数的逻辑,对话流程会按照预期进行调整。例如,在网页搜索智能体完成一次数据查询后,会优先选择数据分析智能体进行数据处理,而不是按照默认的模型选择逻辑。如果网页搜索智能体连续两次进行查询操作且没有其他智能体介入,规划智能体将会被选中,重新规划任务或协调智能体之间的工作。这种自定义选择器函数的应用,使得智能体之间的协作更加符合特定任务的需求,提高了任务处理的效率和准确性。在实际应用中,可以根据不同的任务场景和智能体的能力特点,编写更加复杂和智能的自定义选择器函数,进一步优化多智能体协作系统的性能。