首页
/ AutoGen.NET 轮询群聊实战:RoundRobinGroupChat 固定顺序多 Agent 对话的实现与源码解析

AutoGen.NET 轮询群聊实战:RoundRobinGroupChat 固定顺序多 Agent 对话的实现与源码解析

2026-09-04 21:06:47作者:贡沫苏Truman

RoundRobinGroupChat 是 AutoGen.NET(AutoGen 的 C# 实现)中按固定轮询(round-robin)顺序调用多个 Agent 的群聊模式,适用于“先检索、再总结”这类需要严格顺序执行的多 Agent 工作流。本文基于官方文档 Roundrobin-chat.md 与示例 Example11_Sequential_GroupChat_Example.cs 展开,结合 RoundRobinGroupChat.csRoundRobinOrchestrator.cs 等核心源码,带你从实战配置深入到调度原理,掌握在 AutoGen.NET 中编排固定顺序多 Agent 协作的完整方法。

什么是轮询群聊:固定顺序的多 Agent 协作

官方文档对 RoundRobinGroupChat 的定位非常清晰:它是一个按轮询顺序调用 Agent 的群聊。当你希望多个 Agent 以固定序列依次发言时(最典型的是“搜索 Agent 检索相关信息,随后总结 Agent 对信息进行归纳”的搜索—总结流程),轮询群聊就是对应工具。

文档给出的核心工作流如下图所示:

flowchart LR
    A[User] -->|Ask a question| B[Search Agent]
    B -->|Retrieve information| C[Summarization Agent]
    C -->|Summarize result| A[User]

整个循环不断重复:用户提问 → 搜索 Agent 检索 → 总结 Agent 归纳 → 结果回到用户,直到达到最大轮数或出现终止信号。

此外,文档特别指出:RoundRobinGroupChat 还被 SendAsync(见 AgentExtension.cs)用于实现两个 Agent 之间的聊天——也就是说,即使你只想让两个 Agent 一来一回地对话,底层也自动构建了一个双成员的轮询群聊。这一点在文末专门展开。

实战:用 RoundRobinGroupChat 实现搜索—总结聊天流

完整代码见仓库示例 Example11_Sequential_GroupChat_Example.cs。下面按官方文档的四步走流程逐步实现,并对每个参数做源码级注释。

Step 1: 添加必要的 using 语句

using AutoGen.Core;
using AutoGen.OpenAI;
using AutoGen.OpenAI.Extension;
using AutoGen.SemanticKernel;
using AutoGen.SemanticKernel.Extension;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Plugins.Web;
using Microsoft.SemanticKernel.Plugins.Web.Bing;

要点说明:

  • AutoGen.Core:提供 RoundRobinGroupChatGroupChatManagerUserProxyAgentInitiateChatAsync 等核心类型;
  • AutoGen.SemanticKernelAutoGen.SemanticKernel.Extension:提供 SemanticKernelAgent 及其消息连接器扩展;
  • AutoGen.OpenAIAutoGen.OpenAI.Extension:提供 OpenAIChatAgent 及其扩展方法;
  • Microsoft.SemanticKernel.Plugins.Web.Bing:提供 Bing 连接器 BingConnectorWebSearchEnginePlugin,用于让搜索 Agent 具备联网检索能力。

Step 2: 用 SemanticKernelAgent 创建 bingSearch 搜索 Agent

var config = LLMConfiguration.GetAzureOpenAIGPT3_5_Turbo();
var apiKey = config.ApiKey;
var kernelBuilder = Kernel.CreateBuilder()
    .AddAzureOpenAIChatCompletion(config.DeploymentName, config.Endpoint, apiKey);
var bingApiKey = Environment.GetEnvironmentVariable("BING_API_KEY")
    ?? throw new Exception("BING_API_KEY environment variable is not set");
var bingSearch = new BingConnector(bingApiKey);
var webSearchPlugin = new WebSearchEnginePlugin(bingSearch);
kernelBuilder.Plugins.AddFromObject(webSearchPlugin);

var kernel = kernelBuilder.Build();
var kernelAgent = new SemanticKernelAgent(
    kernel: kernel,
    name: "bing-search",
    systemMessage: """
    You search results from Bing and return it as-is.
    You put the original search result between ```bing and ```

    e.g.
    ```bing
    xxx
    ```
    """)
    .RegisterMessageConnector()
    .RegisterPrintMessage(); // pretty print the message

关键细节:

  • 名称必须唯一name: "bing-search" 不是装饰性字符串。GroupChat.csValidation() 会在构造群聊时校验“所有 Agent 必须有名字且名字唯一”,重名或空名会直接抛出 ArgumentException
  • System Message 约束输出格式:提示词要求把 Bing 原始检索结果包裹在 ```bing 代码块中原样返回,这样下游总结 Agent 能明确区分“原始检索内容”与“模型加工内容”,是搜索—总结流水线上保证信息保真的常见做法;
  • RegisterMessageConnector() 负责把 AutoGen 的消息类型与 Semantic Kernel 的消息类型互转,RegisterPrintMessage() 通过中间件把每条消息打印到控制台,便于观察轮询过程;
  • 运行前提:需要配置 Azure OpenAI 连接参数(示例通过 LLMConfiguration.GetAzureOpenAIGPT3_5_Turbo() 读取),并设置环境变量 BING_API_KEY,否则示例会在构造搜索插件前抛异常。

Step 3: 创建 summarization 总结 Agent

var gpt4o = LLMConfiguration.GetOpenAIGPT4o_mini();
var openAIClientAgent = new OpenAIChatAgent(
    chatClient: gpt4o,
    name: "summarizer",
    systemMessage: "You summarize search result from bing in a short and concise manner");

return openAIClientAgent
    .RegisterMessageConnector()
    .RegisterPrintMessage(); // pretty print the message

总结 Agent 使用 OpenAIChatAgent(对应 AutoGen.OpenAI 包)而非 Semantic Kernel Agent,说明轮询群聊的成员可以是异构 Agent——只要都实现 IAgent 接口即可,无需绑定同一模型供应商或同一套底层框架。System Message 的职责是“用简短精炼的方式总结 Bing 搜索结果”,与搜索 Agent 的职责形成明确分工。

Step 4: 创建 RoundRobinGroupChat 并发起聊天

var userProxyAgent = new UserProxyAgent(
    name: "user",
    humanInputMode: HumanInputMode.ALWAYS)
    .RegisterPrintMessage();

var bingSearchAgent = await CreateBingSearchAgentAsync();
var summarizerAgent = await CreateSummarizerAgentAsync();

var groupChat = new RoundRobinGroupChat(
    agents: [userProxyAgent, bingSearchAgent, summarizerAgent]);

var groupChatAgent = new GroupChatManager(groupChat);

var history = await userProxyAgent.InitiateChatAsync(
    receiver: groupChatAgent,
    message: "How to deploy an openai resource on azure",
    maxRound: 10);

这段代码有三个关键构件,逐一拆解:

  1. UserProxyAgent:代表人类用户参与轮询。HumanInputMode.ALWAYS 表示每一轮该 Agent 被调度时都向用户请求真实输入(即对话可以持续多轮交互,而不是发一次就结束)。由于它在 agents 列表中排第一位,每完成一轮“搜索 → 总结”后,轮到它再次向用户收集新的提问,这正是文档流程图里“结果回到 User、User 继续提问”的循环来源;
  2. RoundRobinGroupChat:构造参数为 IEnumerable<IAgent> agents 与可选的 List<IMessage>? initializeMessages(见 RoundRobinGroupChat.cs)。轮询顺序完全由 agents 集合的顺序决定:示例中顺序是 user → bing-search → summarizer → user → …,如需调整执行序列,只需调整列表中的 Agent 排列;
  3. GroupChatManager + InitiateChatAsyncGroupChatManager 是群聊对外的 IAgent 门面(见 GroupChatManager.cs),它把整个群聊包装成“一个可对话的 Agent”;InitiateChatAsync(定义于 AgentExtension.cs)是发起聊天的快捷 API,maxRound: 10 限制最多进行 10 轮对话(该方法默认值同样是 10),返回值 history 包含完整对话历史。

运行后,你会在控制台看到三类消息交替打印:用户问题、bing-search 返回的 ```bing 检索结果块、summarizer 输出的摘要,循环往复直至达到轮数上限或对话终止。

源码解析:轮询调度是怎么实现的

下面结合源码回答一个核心问题:群聊到底如何决定“下一个说话的是谁”

RoundRobinGroupChat 与已废弃的 SequentialGroupChat

RoundRobinGroupChat.cs 中可以看到:

[Obsolete("please use RoundRobinGroupChat")]
public class SequentialGroupChat : RoundRobinGroupChat
{
    [Obsolete("please use RoundRobinGroupChat")]
    public SequentialGroupChat(IEnumerable<IAgent> agents, List<IMessage>? initializeMessages = null)
        : base(agents, initializeMessages)
    {
    }
}

public class RoundRobinGroupChat : GroupChat
{
    public RoundRobinGroupChat(
        IEnumerable<IAgent> agents,
        List<IMessage>? initializeMessages = null)
        : base(agents, initializeMessages: initializeMessages)
    {
    }
}

两个事实值得注意:

  • 早期的 SequentialGroupChat 已被标记 [Obsolete],官方明确要求改用 RoundRobinGroupChat。如果你在看早期资料或旧代码中见到 SequentialGroupChat,它本质上就是 RoundRobinGroupChat 的别名继承;
  • RoundRobinGroupChat 本身没有重写任何逻辑,它完全继承自 GroupChat。轮询行为的差异体现在基类构造时的编排器选择上。

GroupChat 的编排器选择逻辑

GroupChat.cs 的构造函数揭示了群聊的三种编排模式:

if (admin is not null)
{
    this.orchestrator = new RolePlayOrchestrator(admin, workflow);
}
else if (workflow is not null)
{
    this.orchestrator = new WorkflowOrchestrator(workflow);
}
else
{
    this.orchestrator = new RoundRobinOrchestrator();
}

即:提供 admin Agent 时用角色扮演式编排(由 LLM 决定下一位发言者);提供 workflow 图时用工作流编排;两者都不提供时,默认回落到 RoundRobinOrchestrator。而 RoundRobinGroupChat 的构造函数恰好只传 agentsinitializeMessages、不传 admin 与 workflow,因此必然使用轮询编排——这就是该类名字与行为的对应关系。

同时,构造时还会执行 Validation():校验所有 Agent 名字非空且全局唯一,这是示例中每个 Agent 都必须显式命名 name 参数的原因。

RoundRobinOrchestrator 的取人算法

轮询调度的核心实现只有 20 余行,见 RoundRobinOrchestrator.cs

public async Task<IAgent?> GetNextSpeakerAsync(
    OrchestrationContext context,
    CancellationToken cancellationToken = default)
{
    var lastMessage = context.ChatHistory.LastOrDefault();

    if (lastMessage == null)
    {
        return context.Candidates.FirstOrDefault();   // 历史为空 → 第一个候选
    }

    var candidates = context.Candidates.ToList();
    var lastAgentIndex = candidates.FindIndex(a => a.Name == lastMessage.From);
    if (lastAgentIndex == -1)
    {
        return null;                                     // 上一发言者不在候选中 → 结束
    }

    var nextAgentIndex = (lastAgentIndex + 1) % candidates.Count;
    return candidates[nextAgentIndex];                  // 环形取下一位
}

规则非常直白:

  1. 历史为空:返回候选列表第一个 Agent(示例中即 userProxyAgent);
  2. 上一发言者在候选列表中:返回其后一位,末位时取模回绕到首位,形成闭环;
  3. 上一发言者不在候选列表中:返回 null,群聊主循环检测到 null 即退出循环。

这里有一个容易被忽略的语义细节:上一消息的 From 是按名字匹配的(a.Name == lastMessage.From)。这也再次解释了为什么群聊强制要求名字唯一——名字既是身份标识,也是轮询指针的定位依据。

GroupChat.CallAsync 主循环与终止机制

GroupChat.csCallAsync 是每一轮调度的驱动循环:

while (roundLeft > 0)
{
    var orchestratorContext = new OrchestrationContext
    {
        Candidates = this.agents,
        ChatHistory = conversationHistory,
    };
    var nextSpeaker = await this.orchestrator.GetNextSpeakerAsync(orchestratorContext, ct);
    if (nextSpeaker == null)
    {
        break;
    }

    var result = await nextSpeaker.GenerateReplyAsync(conversationHistory, cancellationToken: ct);
    conversationHistory.Add(result);

    if (result.IsGroupChatTerminateMessage())
    {
        return conversationHistory;
    }

    roundLeft--;
}

每一轮做四件事:编排器选下一位发言者 → 该发言者基于完整历史生成回复 → 回复追加进历史 → 检查终止信号并扣减剩余轮数。结合 GroupChatExtension.cs 可以看到终止协议的两个常量:

public const string TERMINATE = "[GROUPCHAT_TERMINATE]";
public const string CLEAR_MESSAGES = "[GROUPCHAT_CLEAR_MESSAGES]";

即:某个 Agent 的回复内容里包含 [GROUPCHAT_TERMINATE] 时,群聊立即返回当前完整历史、不再继续轮询;包含 [GROUPCHAT_CLEAR_MESSAGES] 时则会触发历史裁剪(由 MessageToKeep 处理,用于控制上下文长度)。这意味着终止轮询不需要修改框架代码——只需在某个 Agent 的 system message 中约定“满足某条件时输出 [GROUPCHAT_TERMINATE]”即可,这是设计 Agent 提示词时值得掌握的扩展点。

GroupChatManager.GenerateReplyAsync(见 GroupChatManager.cs)则负责把这个循环暴露成标准的 IAgent 接口:它调用 GroupChat.CallAsync 并返回最后一条消息,使“整个群聊”可以像单个 Agent 一样被 SendAsync / InitiateChatAsync 驱动——示例 Step 4 中 userProxyAgent.InitiateChatAsync(receiver: groupChatAgent, ...) 走的正是这条路径。

顺带掌握:两 Agent 聊天为什么也是 RoundRobinGroupChat

官方文档提到 RoundRobinGroupChat 被用于两个 Agent 的聊天。查看 AgentExtension.csSendAsync(this IAgent agent, IAgent receiver, IEnumerable<IMessage> chatHistory, ...) 的实现:

if (receiver is GroupChatManager manager)
{
    var gc = manager.GroupChat;
    return gc.SendAsync(chatHistory, maxRound, ct);
}

var groupChat = new RoundRobinGroupChat(
    agents:
    [
        agent,
        receiver,
    ]);

return groupChat.SendAsync(chatHistory, maxRound, cancellationToken: ct);

也就是说,当你调用 agentA.SendAsync(agentB, ...) 这类两 Agent 对话 API 时,框架会在背后自动构造一个成员为 [agentA, agentB]RoundRobinGroupChat,让两者交替发言,直到达到 maxRound(默认 10)或出现终止消息。而 GroupChatExtension.cs 中的 IGroupChat.SendAsync 扩展则负责把“一次性跑完整轮数”拆成逐条 yield return 的异步流:每轮只调用一次群聊、把最后一条新消息吐给调用方,遇到 [GROUPCHAT_TERMINATE] 立即 yield break。理解了这一段,就理解了 AutoGen.NET 中“单 Agent 对话、双 Agent 对话、多 Agent 群聊”三者共用同一套消息与终止协议的统一性。

实践要点小结

结合文档与源码,使用 RoundRobinGroupChat 时建议记住以下几点:

要点 说明 依据
顺序即语义 agents 列表的排列顺序就是轮询执行顺序,调整列表即可改变流程 RoundRobinGroupChat.csRoundRobinOrchestrator.cs
名字必须非空且唯一 构造时强校验,名字同时是轮询定位与消息归属的依据 GroupChat.cs Validation()
用 GroupChatManager 暴露群聊 群聊本身不是 IAgent,需包装后作为 SendAsync/InitiateChatAsync 的接收方 GroupChatManager.cs
用 UserProxyAgent 承载用户输入 HumanInputMode.ALWAYS 使其每轮都被调度时向用户索取输入,形成可持续交互的闭环 示例 Step 4
maxRound 控制总轮数 SendAsync/InitiateChatAsync 默认 10 轮,超过即停止 AgentExtension.csGroupChatExtension.cs
约定 [GROUPCHAT_TERMINATE] 提前结束 Agent 回复包含该标记时群聊立即终止并返回当前历史 GroupChatExtension.cs
弃用 SequentialGroupChat 旧类名已标记 [Obsolete],新代码一律使用 RoundRobinGroupChat RoundRobinGroupChat.cs

最后提醒适用前提:示例依赖 Azure OpenAI(通过 AddAzureOpenAIChatCompletion)与 Bing 搜索 API(BING_API_KEY 环境变量),并引用了 AutoGen.OpenAIAutoGen.SemanticKernel 等 NuGet 项目;若只研究轮询机制而不做联网检索,可以将搜索 Agent 替换为任意自定义 IAgent,群聊编排方式不变。更多群聊玩法(如引入 admin 做角色扮演讲者选择、引入 Graph 做工作流编排)可进一步参考同目录下的 Group-chat-overview.mdUse-graph-in-group-chat.md

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
docsdocs
暂无描述
Markdown
889
5.78 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384
flutter_flutterflutter_flutter
本仓库是 Flutter SDK 与 Flutter Engine 的 OpenHarmony 适配版本,由 CPF-Flutter 团队维护。开发者可使用熟悉的 Flutter 技术栈开发 OpenHarmony 应用,3.35.7 及以后的适配版本可基于本仓库源码构建支持 OpenHarmony 的 Flutter Engine。
Dart
1.17 K
341