AutoGen.NET 双 Agent 对话实战:用 InitiateChatAsync 启动多轮对话并用终止消息优雅收尾
本文基于 AutoGen(.NET 版)官方文档 Two-agent-chat 展开,讲解如何在 AutoGen 中让两个 Agent(如教师与学生)进行自动多轮对话:如何通过 AgentExtension.InitiateChatAsync / SendAsync 启动对话、对话何时终止(终止消息 [GROUPCHAT_TERMINATE] 与 maxRound 双重机制),并完整拆解官方示例 Example02_TwoAgent_MathChat 的代码细节与底层调用链(RoundRobinGroupChat → GroupChat.CallAsync)。读完本文,你可以独立编写可运行、可控轮数、可稳定终止的双 Agent 对话程序。
双 Agent 对话的基本机制
在 AutoGen 中,两个 Agent 之间启动对话有两种入口 API(均定义在 AutoGen.Core 程序集的扩展方法中):
AgentExtension.InitiateChatAsync:一次性发起对话并直接拿到完整的对话历史(Task<IEnumerable<IMessage>>);AgentExtension.SendAsync(接收receiver参数的重载):返回IAsyncEnumerable<IMessage>,可以逐条迭代对话中产生的新消息,适合需要流式处理每轮回复的场景。
对话的运行规则如下(与文档描述一致):
- 对话开始时,发送方 Agent(sender)先向接收方 Agent(receiver)发送一条消息;
- 接收方生成回复并回传给发送方;
- 之后两个 Agent 按轮流(round-robin)方式交替发言,循环往复,直到满足以下任一条件:
- 某个 Agent 发送了一条终止消息(termination message);
- 达到了最大轮数
maxRound(API 默认值为 10)。
终止消息:[GROUPCHAT_TERMINATE] 关键字
所谓终止消息,就是一条内容中包含关键字 [GROUPCHAT_TERMINATE] 的 IMessage。这一关键字是 AutoGen.Core.GroupChatExtension 中的常量:
// dotnet/src/AutoGen.Core/Extension/GroupChatExtension.cs
public const string TERMINATE = "[GROUPCHAT_TERMINATE]";
public const string CLEAR_MESSAGES = "[GROUPCHAT_CLEAR_MESSAGES]";
判断一条消息是否为终止消息,使用扩展方法 IsGroupChatTerminateMessage,其实现非常直接——检查消息内容是否包含该关键字(见 GroupChatExtension.cs):
public static bool IsGroupChatTerminateMessage(this IMessage message)
{
return message.GetContent()?.Contains(TERMINATE) ?? false;
}
实践建议(与官方文档一致):把
[GROUPCHAT_TERMINATE]直接写进系统提示词、指望 LLM"自己说"出这个关键字,在弱模型下并不可靠。更稳健的做法是注册一个后处理(post process)钩子/中间件,在 Agent 生成回复之后用代码判断条件(例如回复里出现了[COMPLETE]),满足条件时在代码层面替换为一条硬编码的终止消息。官方示例正是这样做的,下文会完整拆解。
完整示例:教师出题、学生作答
下面的示例来自官方样本 Example02_TwoAgent_MathChat.cs,演示"教师 Agent 出学前班数学题,学生 Agent 作答,答对后教师终止对话"的场景。运行前提:环境变量 OPENAI_API_KEY 已设置(见 LLMConfiguration.cs,内部使用 gpt-4o-mini 模型,密钥取自 OPENAI_API_KEY)。
var gpt4oMini = LLMConfiguration.GetOpenAIGPT4o_mini();
// create teacher agent
// teacher agent will create math questions
var teacher = new OpenAIChatAgent(
chatClient: gpt4oMini,
name: "teacher",
systemMessage: @"You are a teacher that create pre-school math question for student and check answer.
If the answer is correct, you stop the conversation by saying [COMPLETE].
If the answer is wrong, you ask student to fix it.")
.RegisterMessageConnector()
.RegisterMiddleware(async (msgs, option, agent, _) =>
{
var reply = await agent.GenerateReplyAsync(msgs, option);
if (reply.GetContent()?.ToLower().Contains("complete") is true)
{
return new TextMessage(Role.Assistant, GroupChatExtension.TERMINATE, from: reply.From);
}
return reply;
})
.RegisterPrintMessage();
// create student agent
// student agent will answer the math questions
var student = new OpenAIChatAgent(
chatClient: gpt4oMini,
name: "student",
systemMessage: "You are a student that answer question from teacher")
.RegisterMessageConnector()
.RegisterPrintMessage();
// start the conversation
var conversation = await student.InitiateChatAsync(
receiver: teacher,
message: "Hey teacher, please create math question for me.",
maxRound: 10);
逐段说明:
1. 创建教师 Agent,并挂两层"管道"
new OpenAIChatAgent(chatClient:, name:, systemMessage:):基于 OpenAI .NET SDK 的ChatClient创建 Agent,系统提示词要求"出题并判分;答对就说[COMPLETE]停止,答错让学生改正"。.RegisterMessageConnector():注册消息连接器中间件,负责在 AutoGen 消息体系与 OpenAI 消息格式之间做转换(OpenAI 扩展提供)。.RegisterMiddleware(...):注册一个委托中间件,等价于文档中提到的"post process 后处理"思路。它在教师 Agent 真正生成回复之后介入:若回复内容包含complete,就不返回 LLM 原始回复,而是构造一条内容恒为GroupChatExtension.TERMINATE(即[GROUPCHAT_TERMINATE])的TextMessage返回,from字段保留原回复的发送者。这就是文档强调的"更稳健"的终止方式——是否终止由代码判断,而不是靠 LLM 吐出特定字符串。- 对应源码:MiddlewareExtension.cs 中,
RegisterMiddleware会把传入的Func<...>包装为DelegateMiddleware并生成一个MiddlewareAgent<TAgent>包裹原 Agent;同一文件中的RegisterPostProcess是旧版后处理 API(已被标记[Obsolete],源码注释建议改用RegisterMiddleware)。
- 对应源码:MiddlewareExtension.cs 中,
.RegisterPrintMessage():注册 PrintMessageMiddleware,在控制台以友好格式打印 Agent 的每条回复,便于观察对话过程。
2. 创建学生 Agent
学生只挂 RegisterMessageConnector 与 RegisterPrintMessage,无需终止逻辑。
3. 发起对话
student.InitiateChatAsync(receiver: teacher, message: ..., maxRound: 10):由学生 Agent 作为发起方,第一条消息 "Hey teacher, please create math question for me." 会带上 From = "student" 的标记(From 取自 agent.Name),然后开始轮流对话,最多 10 轮。
该样本预期输出的对话片段(摘自 Example02_TwoAgent_MathChat.cs 中的注释):
// Message from teacher
// --------------------
// content: Of course!Here's a math question for you:
//
// What is 2 + 3 ?
// --------------------
//
// Message from student
// --------------------
// content: The sum of 2 and 3 is 5.
// --------------------
//
// Message from teacher
// --------------------
// content: [GROUPCHAT_TERMINATE]
// --------------------
样本最后还有两条断言(Example02_TwoAgent_MathChat.cs),可用于验证对话行为是否符合预期:对话总轮数小于 10,且最后一条消息是终止消息:
conversation.Count().Should().BeLessThan(10);
conversation.Last().IsGroupChatTerminateMessage().Should().BeTrue();
API 详解:InitiateChatAsync 与 SendAsync
以下 API 均位于 AgentExtension.cs(AutoGen.Core 命名空间)。
InitiateChatAsync(推荐用于拿到完整对话历史)
public static async Task<IEnumerable<IMessage>> InitiateChatAsync(
this IAgent agent,
IAgent receiver,
string? message = null,
int maxRound = 10,
CancellationToken ct = default)
参数说明:
| 参数 | 说明 |
|---|---|
agent |
发起对话的 Agent(sender),其 Name 会被写入首条消息的 From 字段 |
receiver |
接收方 Agent |
message |
可选的首条消息;传入时自动构造为 TextMessage(Role.User, message) 且 From = agent.Name |
maxRound |
最大对话轮数,默认 10 |
ct |
取消令牌 |
返回值为首条消息 + 对话过程中产生的所有新消息拼接而成的完整对话历史。实现上(AgentExtension.cs):先构造初始 chatHistory,再 await foreach 遍历 SendAsync 得到的消息流,最后 Concat 返回。
SendAsync(多种重载,按需选择)
同一个扩展类中还有两个单 Agent 的 SendAsync(AgentExtension.cs),只发送一条消息并返回 Agent 的一次回复,不会进入多轮循环:
// 发送 IMessage(可附带既有对话历史 chatHistory)
public static async Task<IMessage> SendAsync(
this IAgent agent,
IMessage? message = null,
IEnumerable<IMessage>? chatHistory = null,
CancellationToken ct = default);
// 发送字符串,内部自动包装为 TextMessage(Role.User, message)
public static async Task<IMessage> SendAsync(
this IAgent agent,
string message,
IEnumerable<IMessage>? chatHistory = null,
CancellationToken ct = default);
真正驱动"双 Agent 轮流对话"的是下面两个带 receiver 的重载(AgentExtension.cs):
public static IAsyncEnumerable<IMessage> SendAsync(
this IAgent agent,
IAgent receiver,
IEnumerable<IMessage> chatHistory,
int maxRound = 10,
CancellationToken ct = default);
public static IAsyncEnumerable<IMessage> SendAsync(
this IAgent agent,
IAgent receiver,
string message,
IEnumerable<IMessage>? chatHistory = null,
int maxRound = 10,
CancellationToken ct = default);
字符串重载会把 message 包装成 TextMessage(Role.User, message) 且 From = agent.Name,追加到历史末尾后转发给上面的重载。InitiateChatAsync 内部也是调用它。
从源码结构看,带 receiver 的 SendAsync 还内置了一个分支:如果 receiver 是 GroupChatManager(群聊管理器),会直接把请求转给其内部群聊处理(见 AgentExtension.cs);否则,就把 sender 与 receiver 两个 Agent 包进一个 RoundRobinGroupChat(轮流发言群聊)来执行——也就是说,"双 Agent 对话"在实现层面就是一个恰好只有两个成员、无 admin、无 workflow 的轮询群聊。
底层调用链:从 InitiateChatAsync 到终止消息检测
把前面的 API 串起来,一次 InitiateChatAsync 的完整执行路径为:
InitiateChatAsync构造首条消息后调用SendAsync(agent, receiver, chatHistory, maxRound)(AgentExtension.cs);- 该重载创建
RoundRobinGroupChat(agents: [agent, receiver])并调用groupChat.SendAsync(chatHistory, maxRound, ct)(RoundRobinGroupChat.cs); GroupChatExtension.SendAsync(GroupChatExtension.cs)负责轮次循环:
while (maxRound-- > 0)
{
var messages = await groupChat.CallAsync(chatHistory, maxRound: 1, cancellationToken);
// if no new messages, break the loop
if (messages.Count() == chatHistory.Count())
{
yield break;
}
var lastMessage = messages.Last();
yield return lastMessage;
if (lastMessage.IsGroupChatTerminateMessage())
{
yield break;
}
// fix #3268:CallAsync 返回的是完整历史,只需把新消息追加一条
chatHistory = chatHistory.Append(lastMessage);
}
每一轮:调用一次群聊拿到新消息 → 若无新消息则结束 → 产出最后一条消息 → 检测 IsGroupChatTerminateMessage(),命中即 yield break 终止对话 → 否则把该消息追加进历史、进入下一轮;maxRound 耗尽后循环自然退出。
4. GroupChat.CallAsync(GroupChat.cs)负责单轮内部的执行:把 initializeMessages 与传入的 chatHistory 合并为对话历史,循环里先由 orchestrator.GetNextSpeakerAsync 决定下一个发言人,再调用 nextSpeaker.GenerateReplyAsync(conversationHistory) 生成回复并追加到历史;若回复是终止消息则立即返回完整历史,否则轮数减一再继续。
关于发言人决策:GroupChat 构造时会按 admin / workflow 的有无选择 Orchestrator(GroupChat.cs)——有 admin 用 RolePlayOrchestrator,有 workflow 用 WorkflowOrchestrator,都没有(本示例的 RoundRobinGroupChat 即属此类)则退化为 RoundRobinOrchestrator,也就是严格的 A、B 交替发言。此外 GroupChat 的构造校验要求所有成员 Agent 必须有唯一且非空的 Name(GroupChat.cs),这也是示例中给 teacher/student 显式命名的重要原因。
把两层循环合起来理解 maxRound:外层 SendAsync 每轮调 CallAsync(maxRound: 1) 取到一条新消息,InitiateChatAsync 返回的历史即"首条消息 + 每次 yield 的消息"。示例中"教师出题 → 学生作答 → 教师收到 [COMPLETE] 触发中间件替换为 [GROUPCHAT_TERMINATE]",正是外层循环在第三条新消息处 yield break,这也对应了样本断言"最后一条是终止消息、总长度小于 10"。
要点回顾与延伸阅读
- 启动双 Agent 对话:
InitiateChatAsync(拿完整历史)或SendAsync(逐条迭代新消息);maxRound默认 10。 - 终止有两条路:Agent 消息内容包含
[GROUPCHAT_TERMINATE](由IsGroupChatTerminateMessage检测),或达到maxRound。 - 稳健的终止做法:系统提示词让 LLM 输出普通信号词(如
[COMPLETE]),再用RegisterMiddleware在代码层检测并替换为硬编码的GroupChatExtension.TERMINATE消息——弱模型下尤为可靠。 - 实现上双 Agent 对话是
RoundRobinGroupChat的特例;需要多人协作时可扩展阅读 Group-chat-overview.md、Roundrobin-chat.md、Use-graph-in-group-chat.md。
关键源码与示例索引:Two-agent-chat.md、Example02_TwoAgent_MathChat.cs、AgentExtension.cs、GroupChatExtension.cs、GroupChat.cs、RoundRobinGroupChat.cs、MiddlewareExtension.cs、PrintMessageMiddlewareExtension.cs。
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