AutoGen.NET 轮询群聊实战:RoundRobinGroupChat 固定顺序多 Agent 对话的实现与源码解析
RoundRobinGroupChat 是 AutoGen.NET(AutoGen 的 C# 实现)中按固定轮询(round-robin)顺序调用多个 Agent 的群聊模式,适用于“先检索、再总结”这类需要严格顺序执行的多 Agent 工作流。本文基于官方文档 Roundrobin-chat.md 与示例 Example11_Sequential_GroupChat_Example.cs 展开,结合 RoundRobinGroupChat.cs、RoundRobinOrchestrator.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:提供RoundRobinGroupChat、GroupChatManager、UserProxyAgent、InitiateChatAsync等核心类型;AutoGen.SemanticKernel与AutoGen.SemanticKernel.Extension:提供SemanticKernelAgent及其消息连接器扩展;AutoGen.OpenAI与AutoGen.OpenAI.Extension:提供OpenAIChatAgent及其扩展方法;Microsoft.SemanticKernel.Plugins.Web.Bing:提供 Bing 连接器BingConnector与WebSearchEnginePlugin,用于让搜索 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.cs 的Validation()会在构造群聊时校验“所有 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);
这段代码有三个关键构件,逐一拆解:
- UserProxyAgent:代表人类用户参与轮询。
HumanInputMode.ALWAYS表示每一轮该 Agent 被调度时都向用户请求真实输入(即对话可以持续多轮交互,而不是发一次就结束)。由于它在agents列表中排第一位,每完成一轮“搜索 → 总结”后,轮到它再次向用户收集新的提问,这正是文档流程图里“结果回到 User、User 继续提问”的循环来源; - RoundRobinGroupChat:构造参数为
IEnumerable<IAgent> agents与可选的List<IMessage>? initializeMessages(见 RoundRobinGroupChat.cs)。轮询顺序完全由agents集合的顺序决定:示例中顺序是 user → bing-search → summarizer → user → …,如需调整执行序列,只需调整列表中的 Agent 排列; - GroupChatManager + InitiateChatAsync:
GroupChatManager是群聊对外的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 的构造函数恰好只传 agents 和 initializeMessages、不传 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]; // 环形取下一位
}
规则非常直白:
- 历史为空:返回候选列表第一个 Agent(示例中即
userProxyAgent); - 上一发言者在候选列表中:返回其后一位,末位时取模回绕到首位,形成闭环;
- 上一发言者不在候选列表中:返回
null,群聊主循环检测到null即退出循环。
这里有一个容易被忽略的语义细节:上一消息的 From 是按名字匹配的(a.Name == lastMessage.From)。这也再次解释了为什么群聊强制要求名字唯一——名字既是身份标识,也是轮询指针的定位依据。
GroupChat.CallAsync 主循环与终止机制
GroupChat.cs 的 CallAsync 是每一轮调度的驱动循环:
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.cs 中 SendAsync(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.cs、RoundRobinOrchestrator.cs |
| 名字必须非空且唯一 | 构造时强校验,名字同时是轮询定位与消息归属的依据 | GroupChat.cs Validation() |
| 用 GroupChatManager 暴露群聊 | 群聊本身不是 IAgent,需包装后作为 SendAsync/InitiateChatAsync 的接收方 |
GroupChatManager.cs |
| 用 UserProxyAgent 承载用户输入 | HumanInputMode.ALWAYS 使其每轮都被调度时向用户索取输入,形成可持续交互的闭环 |
示例 Step 4 |
maxRound 控制总轮数 |
SendAsync/InitiateChatAsync 默认 10 轮,超过即停止 |
AgentExtension.cs、GroupChatExtension.cs |
约定 [GROUPCHAT_TERMINATE] 提前结束 |
Agent 回复包含该标记时群聊立即终止并返回当前历史 | GroupChatExtension.cs |
| 弃用 SequentialGroupChat | 旧类名已标记 [Obsolete],新代码一律使用 RoundRobinGroupChat |
RoundRobinGroupChat.cs |
最后提醒适用前提:示例依赖 Azure OpenAI(通过 AddAzureOpenAIChatCompletion)与 Bing 搜索 API(BING_API_KEY 环境变量),并引用了 AutoGen.OpenAI、AutoGen.SemanticKernel 等 NuGet 项目;若只研究轮询机制而不做联网检索,可以将搜索 Agent 替换为任意自定义 IAgent,群聊编排方式不变。更多群聊玩法(如引入 admin 做角色扮演讲者选择、引入 Graph 做工作流编排)可进一步参考同目录下的 Group-chat-overview.md 与 Use-graph-in-group-chat.md。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00