MAF快速入门(5)开发自定义Executor
大家好,我是Edison。
上一篇,我们学习了MAF中进行多Agent智能体的顺序和移交编排。但是,很多时候我们想要嵌入一些业务逻辑和结构化输出,亦或者是需要保持历史对话,这时我们就可以开发一些自定义Executor来组成工作流。
什么是Executor?
Executor又称为执行器,它是MAF中处理工作流消息的基本构建模块,是接受结构化消息、执行并生成输出结果消息或事件的独立单元。
很多时候,我们想要封装一些复杂的业务流程到Agent的工作流中,又或者想要完全控制Agent的生命周期和对话历史,这时候我们就需要开发一些自定义的Executor。
例如,我们想要做一些严格的评分判断、循环控制、条件终止的业务逻辑,就需要自定义Executor了。
这里以一个智能营销文案的场景为例,假设一个企业有两个专家,一个专门负责编写文案,另一个则负责对文案进行审核评分。只有当撰写的文案通过审核负责人的审核(假设量化指标:评分>=8)才能进行发布,否则文案编写者需要根据审核人提供的反馈改进建议进行反复修改。

案例来自圣杰《.NET + AI 智能体开发进阶》
由上图可知,这里不仅需要文案专家 和 审核专家 嵌入一些评分和反馈的逻辑,还要设置循环和终止的条件,才能让这个工作流能够比较准确的满足企业的需求。因此,我们就需要开发两个自定义的Executor来封装文案撰写 和 文案审核的Agent。
那么,哪些场景不需要自定义Executor呢?
比如,就只需要一次性Agent的调用输出回答,又或者不需要嵌入严格的业务逻辑的场景。
下面,就让我们来一一实现这个案例。
准备工作
在今天的这个案例中,我们创建了一个.NET控制台应用程序,安装了以下NuGet包:
- Microsoft.Agents.AI.OpenAI
- Microsoft.Agents.AI.Workflows
- Microsoft.Extensions.AI.OpenAI
我们的配置文件中定义了LLM API的信息:
{ "OpenAI": { "EndPoint": "https://api.siliconflow.cn", "ApiKey": "******************************", "ModelId": "Qwen/Qwen3-30B-A3B-Instruct-2507" } }
这里我们使用 SiliconCloud 提供的 Qwen/Qwen3-30B-A3B-Instruct-2507 模型,之前的 Qwen2.5 模型在这个案例中不适用。你可以通过这个URL注册账号:https://cloud.siliconflow.cn/i/DomqCefW 获取大量免费的Token来进行本次实验。
然后,我们将配置文件中的API信息读取出来:
var config = new ConfigurationBuilder() .AddJsonFile($"appsettings.json", optional: false, reloadOnChange: true) .Build(); var openAIProvider = config.GetSection("OpenAI").Get<OpenAIProvider>();
定义数据传输模型
首先,我们定义一下在这个工作流中需要生成传递的数据模型:
(1)SloganResult :文案生成结果
/// <summary> /// 文案生成结果 /// </summary> public sealed class SloganResult { /// <summary> /// 产品任务描述 /// </summary> [JsonPropertyName("task")] public required string Task { get; set; } /// <summary> /// 生成的标语 /// </summary> [JsonPropertyName("slogan")] public required string Slogan { get; set; } }
(2)FeedbackResult:审核反馈结果
/// <summary> /// 审核反馈结果 /// </summary> public sealed class FeedbackResult { /// <summary> /// 审核评论 /// </summary> [JsonPropertyName("comments")] public string Comments { get; set; } = string.Empty; /// <summary> /// 质量评分(1-10分) /// </summary> [JsonPropertyName("rating")] public int Rating { get; set; } /// <summary> /// 改进建议 /// </summary> [JsonPropertyName("actions")] public string Actions { get; set; } = string.Empty; }
定义自定义事件
MAF中定义了一个WorkflowEvent的基类,所有自定义Event都需要继承于它。
(1)SloganGeneratedEvent :文案已生成事件
public sealed class SloganGeneratedEvent : WorkflowEvent { private readonly SloganResult _sloganResult; public SloganGeneratedEvent(SloganResult sloganResult) : base(sloganResult) { this._sloganResult = sloganResult; } public override string ToString() => $"📝 [标语生成] {_sloganResult.Slogan}"; }
(2)FeedbackFinishedEvent : 反馈已完成事件
/// <summary> /// 自定义事件:审核反馈完成 /// </summary> public sealed class FeedbackFinishedEvent : WorkflowEvent { private readonly FeedbackResult _feedbackResult; public FeedbackFinishedEvent(FeedbackResult feedbackResult) : base(feedbackResult) { this._feedbackResult = feedbackResult; } public override string ToString() => $""" 📊 [审核反馈] 评分: {_feedbackResult.Rating}/10 评论: {_feedbackResult.Comments} 建议: {_feedbackResult.Actions} """; }
开发文案生成Executor
MAF中定义了一个Executor的基类,所有自定义Exectuor都需要继承于它。
/// <summary> /// 文案生成 Executor - 根据任务或反馈生成标语 /// </summary> public class SloganWriterExecutor : Executor { private readonly AIAgent _agent; private readonly AgentThread _thread; /// <summary> /// 初始化文案生成 Executor /// </summary> /// <param name="id">Executor 唯一标识</param> /// <param name="chatClient">AI 聊天客户端</param> public SloganWriterExecutor(string id, IChatClient chatClient) : base(id) { // 配置 Agent 选项 ChatClientAgentOptions agentOptions = new( instructions: "你是一名专业的文案撰写专家。你将根据产品特性创作简洁有力的宣传标语。" ) { ChatOptions = new() { // 配置结构化输出:要求返回 SloganResult JSON 格式 ResponseFormat = ChatResponseFormat.ForJsonSchema<SloganResult>() } }; // 创建 Agent 和对话线程 this._agent = new ChatClientAgent(chatClient, agentOptions); this._thread = this._agent.GetNewThread(); } /// <summary> /// 配置消息路由:支持两种输入类型 /// </summary> protected override RouteBuilder ConfigureRoutes(RouteBuilder routeBuilder) => routeBuilder .AddHandler<string, SloganResult>(this.HandleInitialTaskAsync) // 处理初始任务 .AddHandler<FeedbackResult, SloganResult>(this.HandleFeedbackAsync); // 处理反馈 /// <summary> /// 处理初始任务(首次生成) /// </summary> private async ValueTask<SloganResult> HandleInitialTaskAsync( string message, IWorkflowContext context, CancellationToken cancellationToken = default) { Console.WriteLine($"✍️ [文案生成] 接收到任务: {message}"); // 调用 Agent 生成标语 var result = await this._agent.RunAsync(message, this._thread, cancellationToken: cancellationToken); // 反序列化结构化输出 var sloganResult = JsonSerializer.Deserialize<SloganResult>(result.Text) ?? throw new InvalidOperationException("❌ 反序列化标语结果失败"); Console.WriteLine($"📝 [文案生成] 生成标语: {sloganResult.Slogan}"); // 发布自定义事件(将在后续定义) await context.AddEventAsync(new SloganGeneratedEvent(sloganResult), cancellationToken); return sloganResult; } /// <summary> /// 处理审核反馈(改进优化) /// </summary> private async ValueTask<SloganResult> HandleFeedbackAsync( FeedbackResult feedback, IWorkflowContext context, CancellationToken cancellationToken = default) { // 构造反馈消息 var feedbackMessage = $""" 以下是对你之前标语的审核反馈: 评论: {feedback.Comments} 评分: {feedback.Rating} / 10 改进建议: {feedback.Actions} 请根据反馈改进你的标语,使其更加精准有力。 """; Console.WriteLine($"🔄 [文案生成] 接收到反馈,评分: {feedback.Rating}/10"); // 调用 Agent 改进标语(保持对话上下文) var result = await this._agent.RunAsync(feedbackMessage, this._thread, cancellationToken: cancellationToken); var sloganResult = JsonSerializer.Deserialize<SloganResult>(result.Text) ?? throw new InvalidOperationException("❌ 反序列化标语结果失败"); Console.WriteLine($"📝 [文案生成] 改进后标语: {sloganResult.Slogan}"); // 发布事件 await context.AddEventAsync(new SloganGeneratedEvent(sloganResult), cancellationToken); return sloganResult; } }
在这个Executor中,需要注意以下几点:
(1)在实例化Agent时配置结构化输出,严格输出强类型的JSON格式数据;
(2)需要重写ConfigureRoutes方法配置消息路由,即从处理初始任务开始,并设置处理反馈闭环;
其工作机制如下:
-
当接收到string消息(即用户的任务信息)时,调用 HandleInitialTaskAsync 方法进行首次生成;
-
当接收到FeedbackResult类型消息(即质量审核反馈的消息)时,调用 HandleFeedbackAsync 方法进行改进生成。
(3)在调用完Agent获取响应之后,需要将其进行强类型的反序列化输出;
(4)最后通过发布自定义事件进行工作流传递,这里是 SloganGeneratedEvent;
(5)通过AgentThread实现对话历史保持,而不是每次从头开始。
开发质量审核Executor
在质量审核中,假设我们有如下的审核逻辑:评分>=8代表通过审核可以发布,评分<8则发送反馈继续循环,如果编辑次数>=3次则需要终止循环输出当前版本文案。
/// <summary> /// 审核反馈 Executor - 评估标语质量并提供反馈 /// </summary> public sealed class FeedbackExecutor : Executor<SloganResult> { private readonly AIAgent _agent; private readonly AgentThread _thread; private int _attempts = 0; /// <summary> /// 最低评分要求(1-10分) /// </summary> public int MinimumRating { get; init; } = 8; /// <summary> /// 最大尝试次数 /// </summary> public int MaxAttempts { get; init; } = 3; /// <summary> /// 初始化审核反馈 Executor /// </summary> /// <param name="id">Executor 唯一标识</param> /// <param name="chatClient">AI 聊天客户端</param> public FeedbackExecutor(string id, IChatClient chatClient) : base(id) { // 配置 Agent 选项 ChatClientAgentOptions agentOptions = new( instructions: "你是一名专业的文案审核专家。你将评估标语的质量,并提供改进建议。" ) { ChatOptions = new() { // 配置结构化输出:要求返回 FeedbackResult JSON 格式 ResponseFormat = ChatResponseFormat.ForJsonSchema<FeedbackResult>() } }; this._agent = new ChatClientAgent(chatClient, agentOptions); this._thread = this._agent.GetNewThread(); } /// <summary> /// 处理标语审核 /// </summary> public override async ValueTask HandleAsync( SloganResult slogan, IWorkflowContext context, CancellationToken cancellationToken = default) { // 构造审核消息 var reviewMessage = $""" 请审核以下标语: 任务: {slogan.Task} 标语: {slogan.Slogan} 请提供: 1. 详细的评论(comments) 2. 质量评分(rating,1-10分) 3. 改进建议(actions) """; Console.WriteLine($"🔍 [质量审核] 开始审核标语: {slogan.Slogan}"); // 调用 Agent 进行审核 var response = await this._agent.RunAsync(reviewMessage, this._thread, cancellationToken: cancellationToken); // 反序列化反馈结果 var feedback = JsonSerializer.Deserialize<FeedbackResult>(response.Text) ?? throw new InvalidOperationException("❌ 反序列化反馈结果失败"); Console.WriteLine($"📊 [质量审核] 评分: {feedback.Rating}/10"); // 发布自定义事件(将在后续定义) await context.AddEventAsync(new FeedbackFinishedEvent(feedback), cancellationToken); // 业务逻辑:判断是否通过审核 if (feedback.Rating >= this.MinimumRating) { // ✅ 通过审核 await context.YieldOutputAsync( $""" ✅ 标语已通过审核! 任务: {slogan.Task} 标语: {slogan.Slogan} 评分: {feedback.Rating}/10 评论: {feedback.Comments} """, cancellationToken ); Console.WriteLine($"✅ [质量审核] 标语通过审核"); return; } // ❌ 未通过审核,检查尝试次数 if (this._attempts >= this.MaxAttempts) { // 达到最大尝试次数,输出最终版本 await context.YieldOutputAsync( $""" ⚠️ 标语在 {this.MaxAttempts} 次尝试后未达到最低评分要求。 最终标语: {slogan.Slogan} 最终评分: {feedback.Rating}/10 评论: {feedback.Comments} """, cancellationToken ); Console.WriteLine($"⚠️ [质量审核] 达到最大尝试次数,终止流程"); return; } // 🔄 继续循环:发送反馈消息回到 SloganWriterExecutor await context.SendMessageAsync(feedback, cancellationToken: cancellationToken); this._attempts++; Console.WriteLine($"🔄 [质量审核] 发送反馈,第 {this._attempts} 次尝试"); } }
在这个Executor中,除了之前提到的几点之外,我们还需要注意:
-
在反序列化反馈结果及发布自定义事件之后,需要嵌入评分逻辑,即判断是否通过审核;如果通过审核,就及时结束循环;如果不通过,则发送反馈消息继续循环;
-
记录审核次数,如果达到设定的最大值,也及时结束循环不恋战!
-
通过 IWorkflowContext 的 SendMessageAsync 方法将反馈消息传递给其他参与者,这里是 SloganWriterExecutor。
构建工作流
现在万事俱备,只欠一个Workflow,现在Let's do it!
Step1: 获取ChatClient
var chatClient = new OpenAIClient( new ApiKeyCredential(openAIProvider.ApiKey), new OpenAIClientOptions { Endpoint = new Uri(openAIProvider.Endpoint) }) .GetChatClient(openAIProvider.ModelId) .AsIChatClient();
Step2: 实例化自定义Executors
var solganWriter = new SloganWriterExecutor(id: "SloganWriter", chatClient); var feebackHandler = new FeedbackExecutor(id: "FeedbackHandler", chatClient); Console.WriteLine("✅ Executor 实例创建完成");
Step3: 创建工作流
var workflow = new WorkflowBuilder(solganWriter) .AddEdge(source: solganWriter, target: feebackHandler) // 生成 → 审核 .AddEdge(source: feebackHandler, target: solganWriter) // 审核不通过 → 重新生成 .WithOutputFrom(feebackHandler) // 指定输出来源 .Build(); Console.WriteLine("✅ 工作流构建完成");
Step4: 测试工作流
// 定义产品任务 var productTask = "请为马自达一款经济实惠且驾驶乐趣十足的电动SUV创作标语,要求结合马自达电车的特性来创作"; Console.WriteLine($"📋 产品需求: {productTask}\n"); Console.WriteLine($"📊 审核标准: 评分 >= 8分"); Console.WriteLine($"🔄 最大尝试: 3次\n"); Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); Console.WriteLine("⏱️ 开始执行工作流..."); Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"); // 执行工作流 await using (var run = await InProcessExecution.StreamAsync(workflow, input: productTask)) { // 监听工作流事件 await foreach (WorkflowEvent evt in run.WatchStreamAsync()) { // 使用模式匹配识别不同类型的事件 switch (evt) { case SloganGeneratedEvent sloganEvent: // 处理标语生成事件 Console.WriteLine($"✨ {sloganEvent}"); Console.WriteLine(); break; case FeedbackFinishedEvent feedbackEvent: // 处理审核反馈事件 Console.WriteLine($"{feedbackEvent}"); Console.WriteLine(); break; case WorkflowOutputEvent outputEvent: // 处理最终输出事件 Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); Console.WriteLine("🎉 工作流执行完成"); Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"); Console.WriteLine($"{outputEvent.Data}"); break; } } Console.WriteLine("\n✅ 所有流程已完成"); }
测试结果如下图所示:
首先,获得了首次的文案撰写内容:

然后,第一轮审核评分6分并给出反馈:

然后,文案撰写开始修改形成第二版文案:

然后,再次评分为6分又给出反馈:

然后,终于获得审核通过(本次评分9分>=8分),可以发布:

至此,工作流已经结束,可以看见,第三次生成的文案内容比前两次要好一些。
小结
本文介绍了Executor的基本概念 以及 如何开发自定义Executor,然后给出了一个营销文案生成审核的工作流案例详细介绍了自定义Executor的应用。
下一篇,我们将继续学习MAF中如何进行混合编排 Agent 和 Executor,覆盖实际场景中 确定性的业务逻辑 和 AI智能决策 的结合应用。
示例源码
GitHub: https://github.com/EdisonTalk/MAFD
参考资料
Microsoft Learn,《Agent Framework Tutorials》
推荐学习
圣杰,《.NET + AI 智能体开发进阶》


上一篇,我们学习了MAF中进行多Agent智能体的顺序和移交编排。但是,很多时候我们想要嵌入一些业务逻辑和结构化输出,亦或者是需要保持历史对话,这时我们就可以开发一些自定义Executor来组成工作流。本文介绍了Executor的基本概念 以及 如何开发自定义Executor,然后给出了一个营销文案生成审核的工作流案例详细介绍了自定义Executor的应用。

浙公网安备 33010602011771号