首页
/ AutoGen.NET 双 Agent 对话实战:用 InitiateChatAsync 启动多轮对话并用终止消息优雅收尾

AutoGen.NET 双 Agent 对话实战:用 InitiateChatAsync 启动多轮对话并用终止消息优雅收尾

2026-09-04 17:22:36作者:贡沫苏Truman

本文基于 AutoGen(.NET 版)官方文档 Two-agent-chat 展开,讲解如何在 AutoGen 中让两个 Agent(如教师与学生)进行自动多轮对话:如何通过 AgentExtension.InitiateChatAsync / SendAsync 启动对话、对话何时终止(终止消息 [GROUPCHAT_TERMINATE]maxRound 双重机制),并完整拆解官方示例 Example02_TwoAgent_MathChat 的代码细节与底层调用链(RoundRobinGroupChatGroupChat.CallAsync)。读完本文,你可以独立编写可运行、可控轮数、可稳定终止的双 Agent 对话程序。

双 Agent 对话的基本机制

在 AutoGen 中,两个 Agent 之间启动对话有两种入口 API(均定义在 AutoGen.Core 程序集的扩展方法中):

  • AgentExtension.InitiateChatAsync:一次性发起对话并直接拿到完整的对话历史Task<IEnumerable<IMessage>>);
  • AgentExtension.SendAsync(接收 receiver 参数的重载):返回 IAsyncEnumerable<IMessage>,可以逐条迭代对话中产生的新消息,适合需要流式处理每轮回复的场景。

对话的运行规则如下(与文档描述一致):

  1. 对话开始时,发送方 Agent(sender)先向接收方 Agent(receiver)发送一条消息
  2. 接收方生成回复并回传给发送方;
  3. 之后两个 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)。
  • .RegisterPrintMessage():注册 PrintMessageMiddleware,在控制台以友好格式打印 Agent 的每条回复,便于观察对话过程。

2. 创建学生 Agent

学生只挂 RegisterMessageConnectorRegisterPrintMessage,无需终止逻辑。

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.csAutoGen.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 的 SendAsyncAgentExtension.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 内部也是调用它。

从源码结构看,带 receiverSendAsync 还内置了一个分支:如果 receiverGroupChatManager(群聊管理器),会直接把请求转给其内部群聊处理(见 AgentExtension.cs);否则,就把 sender 与 receiver 两个 Agent 包进一个 RoundRobinGroupChat(轮流发言群聊)来执行——也就是说,"双 Agent 对话"在实现层面就是一个恰好只有两个成员、无 admin、无 workflow 的轮询群聊。

底层调用链:从 InitiateChatAsync 到终止消息检测

把前面的 API 串起来,一次 InitiateChatAsync 的完整执行路径为:

  1. InitiateChatAsync 构造首条消息后调用 SendAsync(agent, receiver, chatHistory, maxRound)AgentExtension.cs);
  2. 该重载创建 RoundRobinGroupChat(agents: [agent, receiver]) 并调用 groupChat.SendAsync(chatHistory, maxRound, ct)RoundRobinGroupChat.cs);
  3. GroupChatExtension.SendAsyncGroupChatExtension.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.CallAsyncGroupChat.cs)负责单轮内部的执行:把 initializeMessages 与传入的 chatHistory 合并为对话历史,循环里先由 orchestrator.GetNextSpeakerAsync 决定下一个发言人,再调用 nextSpeaker.GenerateReplyAsync(conversationHistory) 生成回复并追加到历史;若回复是终止消息则立即返回完整历史,否则轮数减一再继续。

关于发言人决策:GroupChat 构造时会按 admin / workflow 的有无选择 Orchestrator(GroupChat.cs)——有 admin 用 RolePlayOrchestrator,有 workflow 用 WorkflowOrchestrator,都没有(本示例的 RoundRobinGroupChat 即属此类)则退化为 RoundRobinOrchestrator,也就是严格的 A、B 交替发言。此外 GroupChat 的构造校验要求所有成员 Agent 必须有唯一且非空NameGroupChat.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.mdRoundrobin-chat.mdUse-graph-in-group-chat.md

关键源码与示例索引:Two-agent-chat.mdExample02_TwoAgent_MathChat.csAgentExtension.csGroupChatExtension.csGroupChat.csRoundRobinGroupChat.csMiddlewareExtension.csPrintMessageMiddlewareExtension.cs

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

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
889
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
982
502
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384