AutoGen .NET Core 入门实战:用 Modifier 与 Checker 两个 Agent 搭建消息驱动倒计时工作流
本篇基于 AutoGen .NET SDK(Microsoft.AutoGen)官方教程 docs/dotnet/core/tutorial.md 展开,演示如何用 Modifier 和 Checker 两个 Agent 协作完成"从 10 倒数到 1"的倒计时工作流。读完后你将掌握 .NET 版 AutoGen 的三个核心编程要素:强类型消息定义(CountMessage/CountUpdate)、基于 BaseAgent + IHandle<T> + [TypeSubscription] 的 Agent 定义方式,以及 AgentsAppBuilder 组装 in-process 运行时并驱动应用的完整流程。
示例总览:一个消息驱动的倒数工作流
教程的完整示例代码位于 dotnet/samples/GettingStarted 目录(可直接在仓库中查看和运行)。示例的目标很明确:
- 定义两个 Agent:
Modifier负责修改计数值,Checker负责校验计数值,并在计数到达 1 时停止应用; - 应用启动后先向
default主题发布一条CountMessage(初始值 10),之后Modifier与Checker通过消息互相传递,驱动倒数循环,直到Checker判定结束并调用宿主生命周期服务停止应用。
消息流向为:CountMessage(10) → Modifier 减一后发布 CountUpdate(9) → Checker 判定 9 > 1 通过,回发 CountMessage(9) → ……如此往复,直到 CountUpdate(1) 触发 Checker 停止应用。
开始前需要安装 SDK 的两个核心包(详见 docs/dotnet/core/index.md 与 docs/dotnet/core/installation.md):
dotnet add package Microsoft.AutoGen.Contracts
dotnet add package Microsoft.AutoGen.Core
第一步:定义强类型消息
教程指出,Agent 之间传递的消息应当先定义为强类型类。示例中使用了两个消息类型:用 CountMessage 传递当前计数值,用 CountUpdate 传递修改后的计数值(CountMessage.cs):
public class CountMessage
{
public int Content { get; set; }
}
public class CountUpdate
{
public int NewCount { get; set; }
}
将消息类型拆分为独立的强类型类,好处在于可以构建"Agent 响应某类型消息、产出某类型消息"的确定性工作流:运行时按消息类型精确分发,Agent 只处理自己声明要处理的消息。
第二步:创建 Modifier Agent
继承 BaseAgent
在 AutoGen .NET 中,Agent 是一个可以接收和发送消息的类,它自己定义如何处理收到的消息。定义 Agent 的方式是创建一个继承自 BaseAgent(BaseAgent.cs)的类:
using Microsoft.AutoGen.Contracts;
using Microsoft.AutoGen.Core;
public class Modifier(
AgentId id,
IAgentRuntime runtime,
) :
BaseAgent(id, runtime, "MyAgent", null),
{
}
构造参数说明:AgentId 和 IAgentRuntime 会由框架在构造时始终传入,并应原样转发给基类构造函数;另外两个参数是 Agent 的描述和一个可选的 ILogger<BaseAgent>。AgentId 由 Type(Agent 类型名)与 Key(实例键)组成,是 Agent 在运行时中的身份标识。
实现消息处理器 IHandle<T>
我们希望 Modifier 收到 CountMessage 后修改计数并发布 CountUpdate。这通过实现 IHandle<CountMessage> 接口完成:
public class Modifier(
// ...
) :
BaseAgent(...),
IHandle<CountMessage>
{
public async ValueTask HandleAsync(CountMessage item, MessageContext messageContext)
{
// ...
}
}
MessageContext 携带消息投递时的上下文信息(如发送方、消息 ID 等),来自 MessageContext.cs。
添加主题订阅 [TypeSubscription]
仅仅定义了处理器还不够,在消息真正投递给 Agent 之前,还必须让 Agent 订阅消息所在的主题。这通过类上的 TypeSubscription 特性完成:
[TypeSubscription("default")]
public class Modifier(
// ...
从源码看,TypeSubscriptionAttribute(TypeSubscriptionAttribute.cs)实现了 IUnboundSubscriptionDefinition 接口,构造参数就是主题名;框架注册 Agent 时会调用其 Bind(AgentType) 方法,将该 Agent 的具体类型与主题绑定成一个 TypeSubscription。也就是说,[TypeSubscription("default")] 的含义是:该 Agent 的这个类型订阅 default 主题上的消息。同目录下的 TypePrefixSubscriptionAttribute.cs 还提供了按主题前缀订阅的变体。
发布消息
有了处理器和订阅后,就可以在处理函数内部发布结果:
public async ValueTask HandleAsync(CountMessage item, MessageContext messageContext)
{
int newValue = item.Content - 1;
Console.WriteLine($"\nModifier:\nModified {item.Content} to {newValue}");
CountUpdate updateMessage = new CountUpdate { NewCount = newValue };
await this.PublishMessageAsync(updateMessage, topic: new TopicId("default"));
}
发布消息时需要显式指定目标主题。这里发布到 default 主题,与订阅的是同一个主题——当然也可以发布到别的话题,本教程为保持简单使用了同一主题。
通过构造函数注入配置逻辑
教程进一步把"如何修改计数"做成可配置的:向 Agent 传入一个 Func<int, int> 委托,由外部决定计数逻辑:
using ModifyF = System.Func<int, int>;
[TypeSubscription("default")]
public class Modifier(
AgentId id,
IAgentRuntime runtime,
ModifyF modifyFunc // <-- 新增
) :
BaseAgent(...),
IHandle<CountMessage>
{
public async ValueTask HandleAsync(CountMessage item, MessageContext messageContext)
{
int newValue = modifyFunc(item.Content); // <-- 在这里使用
// ...
}
}
这里的依赖注入能生效,是因为示例程序把 modifyFunc 注册成了单例服务(见下文 Program 部分),AgentsAppBuilder 通过标准 DI 容器解析 Agent 构造参数。
Modifier 最终实现
示例中的最终代码(Modifier.cs):
using Microsoft.AutoGen.Contracts;
using Microsoft.AutoGen.Core;
using ModifyF = System.Func<int, int>;
namespace GettingStartedSample;
[TypeSubscription("default")]
public class Modifier(
AgentId id,
IAgentRuntime runtime,
ModifyF modifyFunc
) :
BaseAgent(id, runtime, "Modifier", null),
IHandle<CountMessage>
{
public async ValueTask HandleAsync(CountMessage item, MessageContext messageContext)
{
int newValue = modifyFunc(item.Content);
Console.WriteLine($"\nModifier:\nModified {item.Content} to {newValue}");
CountUpdate updateMessage = new CountUpdate { NewCount = newValue };
await this.PublishMessageAsync(updateMessage, topic: new TopicId("default"));
}
}
源码级原理:消息如何被分发到 Handler
结合 BaseAgent.cs 可以理解上述写法背后的机制:
- 构造期反射建索引:
BaseAgent构造函数调用ReflectInvokers(),扫描当前类型实现的所有IHandle<T>/IHandle<T, T2>泛型接口,提取每个接口的HandleAsync方法并包装为HandlerInvoker,以消息类型T为键存入Dictionary<Type, HandlerInvoker>(见 ReflectInvokers)。 - 运行期按类型精确分发:当运行时把消息交给 Agent 时,
OnMessageAsync用message.GetType()查表,命中则调用对应的HandlerInvoker.InvokeAsync,未命中则返回null不做处理(见 OnMessageAsync)。 - 收发消息统一走 Runtime:
BaseAgent.PublishMessageAsync只是把sender: this.Id补上后委托给IAgentRuntime.PublishMessageAsync(BaseAgent.cs);SendMessageAsync点对点发送同理。这意味着替换IAgentRuntime的实现(例如换成 gRPC 分布式运行时)即可让同一套 Agent 代码运行在分布式环境中。
这也解释了为什么"实现 IHandle<T>"与"加 [TypeSubscription]"缺一不可:前者决定 Agent 能处理什么消息,后者决定 Agent 能收到什么消息。
第三步:创建 Checker Agent
Checker 负责校验计数,并在计数到达 1 时停止应用。这里额外演示了用依赖注入获取 IHostApplicationLifetime 服务来关停应用。完整代码(Checker.cs):
using Microsoft.AutoGen.Contracts;
using Microsoft.AutoGen.Core;
using Microsoft.Extensions.Hosting;
using TerminationF = System.Func<int, bool>;
namespace GettingStartedSample;
[TypeSubscription("default")]
public class Checker(
AgentId id,
IAgentRuntime runtime,
IHostApplicationLifetime hostApplicationLifetime,
TerminationF runUntilFunc
) :
BaseAgent(id, runtime, "Modifier", null),
IHandle<CountUpdate>
{
public async ValueTask HandleAsync(CountUpdate item, MessageContext messageContext)
{
if (!runUntilFunc(item.NewCount))
{
Console.WriteLine($"\nChecker:\n{item.NewCount} passed the check, continue.");
await this.PublishMessageAsync(new CountMessage { Content = item.NewCount }, new TopicId("default"));
}
else
{
Console.WriteLine($"\nChecker:\n{item.NewCount} failed the check, stopping.");
hostApplicationLifetime.StopApplication();
}
}
}
要点:
Checker实现的是IHandle<CountUpdate>,即它接收的是Modifier发布出来的CountUpdate,与Modifier形成消息闭环;- 校验逻辑同样是注入的
TerminationF runUntilFunc(Func<int, bool>),返回true表示达到终止条件; - 校验未通过时,把新计数包装回
CountMessage发布到default主题,交给Modifier继续下一轮;校验通过(计数到达 1)时调用hostApplicationLifetime.StopApplication()优雅停止宿主应用。
一个小细节值得注意:示例源码中 Checker 传给基类的描述字符串仍是 "Modifier"(Checker.cs),这是示例代码里从 Modifier 复制而来的一处笔误,不影响消息分发逻辑(分发依赖 IHandle<T> 与订阅特性,而非描述字符串),但读者在自己实现时应改为 "Checker"。
第四步:用 AgentsAppBuilder 组装并运行应用
现在把两个 Agent 组装成一个完整应用(Program.cs)。
定义业务委托
using 之后,先定义两个业务委托——修改函数和终止判定函数:
using ModifyF = System.Func<int, int>;
using TerminationF = System.Func<int, bool>;
ModifyF modifyFunc = (int x) => x - 1;
TerminationF runUntilFunc = (int x) =>
{
return x <= 1;
};
构建应用
AgentsAppBuilder(定义于 AgentsApp.cs)是应用组装入口,示例中依次做了四件事:
AgentsAppBuilder appBuilder = new AgentsAppBuilder();
appBuilder.UseInProcessRuntime();
appBuilder.Services.TryAddSingleton(modifyFunc);
appBuilder.Services.TryAddSingleton(runUntilFunc);
appBuilder.AddAgent<Checker>("Checker");
appBuilder.AddAgent<Modifier>("Modifier");
var app = await appBuilder.BuildAsync();
await app.StartAsync();
- 指定 in-process 运行时:
UseInProcessRuntime()使用进程内单进程运行时; - 注册业务服务:把两个委托注册为单例,Agent 构造函数即可通过 DI 拿到;
- 注册 Agent 类:
AddAgent<T>(key)按类型注册,key 作为 AgentId 的实例键("Checker"、"Modifier"); - 构建并启动:
BuildAsync()生成应用,StartAsync()启动宿主。
发布首条消息并等待退出
应用启动后需要一条消息来触发整个流程:向 Agent 订阅的 default 主题发布初始 CountMessage(值为 10),然后等待应用退出:
await app.PublishMessageAsync(new CountMessage
{
Content = 10
}, new TopicId("default"));
// Run until application shutdown
await app.WaitForShutdownAsync();
运行后控制台会看到计数从 10 一路倒数到 1,Checker 输出 1 failed the check, stopping. 后应用正常退出。
自己动手试试
教程最后给出了几个练习方向,都可以直接在 dotnet/samples/GettingStarted 上修改验证:
- 修改初始计数值(
Program.cs中的Content = 10); - 新增一个"递增"的
modifyFunc,让计数向上走(记得同步修改runUntilFunc的终止条件); - 新增一个只负责打印到控制台的 Agent,替代
Modifier/Checker自己Console.WriteLine(提示:定义一个新的消息类型并让对应 Agent 订阅default主题即可)。
做这些练习时,正好可以练习本教程的三件套:新消息类型类、IHandle<新类型> 处理器、[TypeSubscription("default")] 订阅。
参考文件
| 内容 | 仓库路径 |
|---|---|
| 教程原文 | docs/dotnet/core/tutorial.md |
| .NET Core 概念总览 | docs/dotnet/core/index.md |
| 安装说明 | docs/dotnet/core/installation.md |
| 与 Python 版差异 | docs/dotnet/core/differences-from-python.md |
| 示例:消息类型 | dotnet/samples/GettingStarted/CountMessage.cs、dotnet/samples/GettingStarted/CountUpdate.cs |
| 示例:Agent 实现 | dotnet/samples/GettingStarted/Modifier.cs、dotnet/samples/GettingStarted/Checker.cs |
| 示例:程序入口 | dotnet/samples/GettingStarted/Program.cs |
| 源码:Agent 基类与消息分发 | dotnet/src/Microsoft.AutoGen/Core/BaseAgent.cs |
| 源码:类型订阅特性 | dotnet/src/Microsoft.AutoGen/Core/TypeSubscriptionAttribute.cs |
| 源码:应用构建器 | dotnet/src/Microsoft.AutoGen/Core/AgentsApp.cs |
| 合同接口 | dotnet/src/Microsoft.AutoGen/Contracts(IAgentRuntime、IHandle、AgentId、TopicId、MessageContext 等) |
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 StartedRust0623
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