AutoGen学习---(1)跑通一个最小 AutoGen多角色示例

重点:

搭建一个简单的AutoGen示例, 了解其中的流程

整个项目的角色:

  1. Planner(规划师) 和 Reviewer(审核员), Summarizer(总结者)一般复杂项目还会有Executor(执行者)
  2. 此案例将Planner(规划师) 和 Reviewer(审核员) 构建成一个team,将这个team的输出有Summarizer(总结者)进行总结

各个角色定义的功能

bash 复制代码
Planner(规划师) : 把复杂任务拆解成可执行的步骤或子任务。  只出方案不执行
Reviewer(审核员): 检查方案/结果,给出反馈, 不修改,不执行 
Executor(执行者): 根据规划者的方案,实际执行动作, 输出结果
Summarizer(总结者): 综合前面的对话和结论,产出最终的结构化结果

注意代码中是将Planner和Reviewer组成一个team, team输出的结果  给到Summarizer

重点学习:

复制代码
1. AssistantAgent是"单个智能体怎么工作"的核心类.
常用重点参数如下:
name --- 智能体名称,用于日志、团队调度、消息源标识。
model_client --- 模型客户端,决定用哪个大模型。
system_message --- 角色提示词,定义智能体的职责、说话风格和行为规则。
tools --- 函数工具列表,让智能体能调用外部能力(如搜索、代码执行)。
output_content_type --- 结构化输出模型(Pydantic),强制智能体输出符合 schema 的结构化结果。
max_tool_iterations --- 工具调用最大循环次数,控制工具链深度,防止死循环。
description --- 智能体简介,供团队编排器理解其用途,便于自动调度。
model_context --- 模型上下文(记忆容器),用于预加载历史消息或限制上下文窗口。
model_client_stream : 流式输出



2. RoundRobinGroupChat  是"多个智能体怎么轮流协作"的核心编排类.
常用重点参数
participants:  团队参与者列表, 可以放 ChatAgent, 也可以放 Team
name: 团队名字,主要用于标识和嵌套团队场景.
description: 团队描述,让上层编排器知道这个团队干什么.
termination_condition: 终止条件,可以设置消息数达到多少,文本中出现某个词,或者外部终止信号.
max_turns: 最多允许多少次发言, 不是多少轮,也不是每个智能体, 是整个团队的所有的agnet一共只允许发言的次数, 设置为None时,会一直 发言下去

Reviewer 为什么不需要 output_content_type

bash 复制代码
因为 Reviewer 的职责不是"产出最终结构化答案",而是"给草案做评审"。
所以 Reviewer 只要输出自然语言评审意见就够了,这类内容给后面的 Summarizer 当材料就行,不需要强制变成 Pydantic 结构。

Summarizer 为什么要单独再开一个 AssistantAgent 而不是塞进团队里

bash 复制代码
因为 Summarizer 的角色和前两个完全不同,它不是"参与讨论",而是"在讨论结束后做最终裁决和格式化"。 这样职责分离更清楚

团队(群聊)类型

bash 复制代码
RoundRobinGroupChat:一个按轮询方式让参与者轮流进行群聊的团队。(常用)
SelectorGroupChat:每次发言都由模型选谁说话,也可以用 selector_func 自己接管。(常用)

MagenticOneGroupChat:一个用于解决各种领域开放式网络和文件任务的通用多代理系统。
Swarm:一个使用HandoffMessage来表示代理之间转换的团队。

思考题: 如果Reviewer(审核员)审查出方案不合理, 需要让Planner(规划师)修改, 该如何搭建整个流程?

bash 复制代码
方案1: 使用SelectorGroupChat(推荐)
让模型判断选择哪个角色发言


 方案2: 使用RoundRobinGroupChat 加大max_turns,
 增加规则 让 executor 自己判断"当前计划是否通过评审", 通过就执行, 不通过就不执行,
等到下一个轮回  让Planner修改方案 

 

本次学习内容参考以下代码

bash 复制代码
"""Day 1: AutoGen 多角色协作最小可运行示例。

说明:
- 这是一个严格对齐 AutoGen AgentChat 的最小示例。
- 角色采用 Planner / Reviewer / Summarizer 三段式分工。
- 最终结果使用 Pydantic Schema 收敛,方便后续对接阶段 3 的结构化任务结果。
"""

from __future__ import annotations

import argparse
import asyncio
import json
import os
import re
from pathlib import Path

from dotenv import load_dotenv
from pydantic import BaseModel, Field

from autogen_agentchat.agents import AssistantAgent
from autogen_agentchat.base import TaskResult
from autogen_agentchat.teams import RoundRobinGroupChat
from autogen_ext.models.openai import OpenAIChatCompletionClient


ROOT_DIR = Path(__file__).resolve().parents[2]
DEFAULT_TASK = "为新手设计一个 3 天的 LangGraph 学习计划"
README_PATH = ROOT_DIR / "README.md"


class FinalPlan(BaseModel):
    """Summarizer 的最终结构化输出。"""

    title: str = Field(description="最终方案标题。")
    summary: str = Field(description="对多角色协作过程的简要总结。")
    steps: list["StepItem"] = Field(description="可执行步骤列表。")
    risks: list[str] = Field(default_factory=list, description="需要注意的风险点。")
    role_explanations: list[str] = Field(
        default_factory=list,
        description="每个角色的职责说明,每项格式为 '角色名: 职责说明'。"
    )


class StepItem(BaseModel):
    """单个步骤的结构化表示。"""

    day: str = Field(description="步骤所属天数,如 Day 1。")
    title: str = Field(description="步骤标题。")
    question: str = Field(description="该步骤要解决的核心问题。")


FinalPlan.model_rebuild()


def build_model_client(
    api_key: str,
    model: str,
    base_url: str,
) -> OpenAIChatCompletionClient:
    """构造 AutoGen 官方推荐的 OpenAIChatCompletionClient。"""
    return OpenAIChatCompletionClient(
        model=model,
        api_key=api_key,
        base_url=base_url,
        model_info={
            "vision": False,
            "function_calling": True,
            "json_output": True,
            "structured_output": True,
            "family": "unknown",
        },
    )


def build_team(model_client: OpenAIChatCompletionClient) -> RoundRobinGroupChat:
    """构造 Planner / Reviewer 的 RoundRobinGroupChat。"""
    planner = AssistantAgent(
        name="Planner",
        model_client=model_client,
        system_message=(
            "你是 Planner. 你的任务是先把用户需求拆成清晰的草案. "
            "steps 必须输出为对象列表,每个对象包含 day, title, question 三个字段. "
            "其中 day 使用字符串形式,如 Day 1. "
            "只输出规划和拆解,不要给最终结论."
        ),
    )
    reviewer = AssistantAgent(
        name="Reviewer",
        model_client=model_client,
        system_message=(
            "你是 Reviewer. 你的任务是检查 Planner 的草案,指出缺口、风险和改进建议. "
            "只做评审,不要代替 Summarizer 输出最终方案."
        ),
    )

    return RoundRobinGroupChat(
        [planner, reviewer],
        max_turns=2,
    )


def build_summarizer(
    model_client: OpenAIChatCompletionClient,
) -> AssistantAgent:
    """构造最终的结构化 Summarizer。"""
    return AssistantAgent(
        name="Summarizer",
        model_client=model_client,
        system_message=(
            "你是 Summarizer. 你要综合前面两位角色的内容,最终只输出符合 schema 的结果. "
            "请确保输出包含 title, summary, steps, risks, role_explanations 这五个字段. "
            "其中 steps 必须是对象列表,每个对象包含 day, title, question 三个字段, "
            "day 使用字符串形式,如 Day 1."
            "role_explanations 必须是字符串数组,每个元素格式为 '角色名: 职责说明'."
        ),
        output_content_type=FinalPlan,
    )


def print_task_result(title: str, result: TaskResult) -> None:
    """打印团队或 Agent 的运行结果。"""
    print("=" * 72)
    print(title)
    print("=" * 72)
    print(f"停止原因: {result.stop_reason}")
    print(f"消息数量: {len(result.messages)}")
    print()

    for index, message in enumerate(result.messages, start=1):
        source = getattr(message, "source", "unknown")
        content = getattr(message, "content", None)
        print(f"[{index}] source={source}")
        if isinstance(content, BaseModel):
            print(json.dumps(content.model_dump(), ensure_ascii=False, indent=2))
        else:
            print(content)
        print()


def print_structured_result(result: TaskResult) -> None:
    """打印团队运行结果和最终结构化对象。"""
    print("=" * 72)
    print("团队运行完成")
    print("=" * 72)
    print(f"停止原因: {result.stop_reason}")
    print(f"消息数量: {len(result.messages)}")
    print()

    for index, message in enumerate(result.messages, start=1):
        source = getattr(message, "source", "unknown")
        content = getattr(message, "content", None)
        print(f"[{index}] source={source}")
        if isinstance(content, BaseModel):
            print(json.dumps(content.model_dump(), ensure_ascii=False, indent=2))
        else:
            print(content)
        print()

    final_message = result.messages[-1]
    final_content = getattr(final_message, "content", None)
    if not isinstance(final_content, FinalPlan):
        raise SystemExit("最终输出不是 FinalPlan 结构化对象,请检查模型是否支持 structured output.")
    final_content = finalize_plan(final_content)

    print("=" * 72)
    print("最终结构化结果")
    print("=" * 72)
    print(json.dumps(final_content.model_dump(), ensure_ascii=False, indent=2))


#  summarizer.run(task=...) 接受的是 str、BaseChatMessage、Sequence[BaseChatMessage] 或 None,并不接受整个 TaskResult 对象,所以要转一下
def render_summary_task(result: TaskResult) -> str:
    """把 Planner / Reviewer 的对话整理成 Summarizer 输入。"""
    lines: list[str] = [
        "请根据以下 Planner / Reviewer 对话,输出最终结构化结果.",
        "",
    ]

    for message in result.messages:
        source = getattr(message, "source", "unknown")
        content = getattr(message, "content", None)
        lines.append(f"### {source}")
        if isinstance(content, BaseModel):
            lines.append(json.dumps(content.model_dump(), ensure_ascii=False, indent=2))
        else:
            lines.append(str(content))
        lines.append("")

    return "\n".join(lines).strip()


def finalize_plan(plan: FinalPlan) -> FinalPlan:
    """补齐模型偶尔省略的辅助字段。"""
    if not plan.risks:
        plan.risks = [
            "如果没有先跑通最小图,后续概念会更容易发散。",
            "新手在状态设计和条件路由上可能会卡住。",
        ]

    if not plan.role_explanations:
        plan.role_explanations = [
            "Planner: 负责把用户需求拆成草案。",
            "Reviewer: 负责检查草案并指出缺口。",
            "Summarizer: 负责把协作结果收口成最终方案。",
        ]

    return plan


async def async_main() -> None:
    """脚本主逻辑。"""
    task = "为新手设计一个 3 天的 LangGraph 学习计划"
    api_key = "sk-xxxx"
    model = "mimo-v2.5"
    base_url = "https://api.xiaomimimo.com/v1"

    print("=" * 72)
    print("Day 1: AutoGen 多角色协作入门")
    print("=" * 72)
    print(f"任务: {task}")
    print("角色分工: Planner(规划师) -> Reviewer(审核员) -> Summarizer(总结员)")
    print(f"模型: {model}")
    print(f"Base URL: {base_url}")
    print()

    model_client = build_model_client(api_key, model, base_url)
    team = build_team(model_client)
    summarizer = build_summarizer(model_client)

    try:
        team_result = await team.run(task=task)
        print_task_result("Planner / Reviewer 协作结果", team_result)

        summary_task = render_summary_task(team_result)
        summarizer_result = await summarizer.run(task=summary_task)
        print_structured_result(summarizer_result)
    finally:
        await model_client.close()


def main() -> None:
    """同步入口。"""
    asyncio.run(async_main())


if __name__ == "__main__":
    main()