My Github

MAF快速入门(5)开发自定义Executor

大家好,我是Edison。

上一篇,我们学习了MAF中进行多Agent智能体的顺序和移交编排。但是,很多时候我们想要嵌入一些业务逻辑和结构化输出,亦或者是需要保持历史对话,这时我们就可以开发一些自定义Executor来组成工作流。

什么是Executor?

Executor又称为执行器,它是MAF中处理工作流消息的基本构建模块,是接受结构化消息、执行并生成输出结果消息或事件的独立单元

很多时候,我们想要封装一些复杂的业务流程到Agent的工作流中,又或者想要完全控制Agent的生命周期和对话历史,这时候我们就需要开发一些自定义的Executor。

例如,我们想要做一些严格的评分判断、循环控制、条件终止的业务逻辑,就需要自定义Executor了。

这里以一个智能营销文案的场景为例,假设一个企业有两个专家,一个专门负责编写文案,另一个则负责对文案进行审核评分。只有当撰写的文案通过审核负责人的审核(假设量化指标:评分>=8)才能进行发布,否则文案编写者需要根据审核人提供的反馈改进建议进行反复修改。

image

案例来自圣杰《.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✅ 所有流程已完成");
}

测试结果如下图所示:

首先,获得了首次的文案撰写内容:

image

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

image

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

image

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

image

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

image

至此,工作流已经结束,可以看见,第三次生成的文案内容比前两次要好一些。

小结

本文介绍了Executor的基本概念 以及 如何开发自定义Executor,然后给出了一个营销文案生成审核的工作流案例详细介绍了自定义Executor的应用。

下一篇,我们将继续学习MAF中如何进行混合编排 Agent 和 Executor,覆盖实际场景中 确定性的业务逻辑 和 AI智能决策 的结合应用。

示例源码

GitHub: https://github.com/EdisonTalk/MAFD

参考资料

Microsoft Learn,Agent Framework Tutorials

推荐学习

圣杰,《.NET + AI 智能体开发进阶

 

posted @ 2025-12-05 08:30  EdisonZhou  阅读(36)  评论(0)    收藏  举报