MAF快速入门(9)多路分支路由工作流

大家好,我是Edison。

最近我一直在跟着圣杰的《.NET+AI智能体开发进阶》课程学习MAF的开发技巧,我强烈推荐你也上车跟我一起出发!

上一篇,我们学习了MAF中如何进行if-else类型的条件路由,但是实际工作中可能会存在多个分支路由的场景。本篇,我们来了解下MAF中的switch-case路由实现多分支路由工作流。

Why switch-case?

在实际业务场景中,很多的业务逻辑涉及到不止两个判断条件,而是多个。

例如,在上一篇的企业内部邮件检测案例中,我们的检测结果只有两个(垃圾邮件 或 正常邮件),但如果我们想增加一个结果(不确定)就无法适用了。

在MAF中,我们可以使用 Switch-Case 来实现这种工作流内部多类决策条件的 工作流需求。

实验案例

今天来晚上上一篇这个企业内部邮件检测的工作流案例,上一篇的流程是这样的:

今天假设我们需要有更为精细的分类:

✅ **正常邮件**(NotSpam):客户咨询、业务往来

❌ **垃圾邮件**(Spam):明显的诈骗、广告

⚠️ **不确定邮件**(Uncertain):可能是钓鱼邮件,需要人工审核

那么这就是一个三元分类的,在业务开发中我们通常会用到switch-case语法,而在MAF工作流中,也为我们定义了这种switch-case接口。

在下面的代码示例中,比对了两种接口的使用方式:

复制代码
// Conditional Edge
builder
    .AddEdge(source, target1, condition: c => c.IsSpam == false)
    .AddEdge(source, target2, condition: c => c.IsSpam == true);
// Switch-Case
builder.AddSwitch(source, sb => sb
    .AddCase(c => c.Decision == NotSpam, target1)
    .AddCase(c => c.Decision == Spam, target2)
    .WithDefault(target3)
);

可以看到,switch-case模式其价值主要在于增强代码可维护性,对于后续如果有新增分类,只需要添加一个AddCase接口方法实现新增分类的处理,同时基于WithDefault接口方法实现兜底确保所有情况都有处理。

最后,下面是我们需要重构后的分支路由图:

准备工作

在今天的这个案例中,我们仍然创建了一个.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 模型,你可以通过这个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)DetectionResult :拉件邮件检测结果

复制代码
public sealed class DetectionResult
{
    /// <summary>
    /// 检测决策(NotSpam / Spam / Uncertain)
    /// </summary>
    [JsonPropertyName("spam_decision")]
    [JsonConverter(typeof(JsonStringEnumConverter))]  // JSON 序列化为字符串
    public SpamDecision spamDecision { get; set; }
    /// <summary>
    /// 判定理由(用于审计和调试)
    /// </summary>
    [JsonPropertyName("reason")]
    public string Reason { get; set; } = string.Empty;
    /// <summary>
    /// 邮件ID(用于关联 Shared State 中的原始内容)
    /// </summary>
    [JsonIgnore]
    public string EmailId { get; set; } = string.Empty;
}

public enum SpamDecision
{
    Spam,        // 垃圾邮件
    NotSpam, // 正常邮件
    UnCertain // 无法确定(需要人工审核)
}

(2)EmailStateConstants :常量,类似于Cache Key的作用,参考上一篇博客内容。

(3)EmailMessage & EmailResponse :DTO作用,参考上一篇博客内容。

入口节点:垃圾邮件检测Executor

这个垃圾邮件检测是本流程的核心节点,这次我们将其重构为支持三分类:

复制代码
internal sealed class SpamDetectionExecutor : Executor<ChatMessage, DetectionResult>
{
    private readonly AIAgent _agent;
    private readonly AgentThread _thread;
    public SpamDetectionExecutor(AIAgent agent) : base("SpamDetectionExecutor")
    {
        // 创建 Agent 和对话线程
        this._agent = agent;
        this._thread = this._agent.GetNewThread();
    }
    public override async ValueTask<DetectionResult> HandleAsync(ChatMessage message, IWorkflowContext context, CancellationToken cancellationToken = default)
    {
        // 1️⃣ 生成唯一邮件ID并保存内容到 Shared State
        var trackedEmail = new EmailMessage
        {
            EmailId = Guid.NewGuid().ToString("N"),
            EmailContent = message.Text
        };
        await context.QueueStateUpdateAsync(
            trackedEmail.EmailId,
            trackedEmail,
            scopeName: EmailStateConstants.EmailStateScope,
            cancellationToken
        );
        // 2️⃣ 调用 AI Agent 进行三分类检测
        var agentResponse = await _agent.RunAsync(
            message,
            _thread,
            cancellationToken: cancellationToken
        );
        // 3️⃣ 解析结构化输出
        var detection = JsonSerializer.Deserialize<DetectionResult>(agentResponse.Text)
            ?? throw new InvalidOperationException("无法解析 Spam Detection 响应");
        // 4️⃣ 关联 EmailId(供下游 Executor 查找原始内容)
        detection.EmailId = trackedEmail.EmailId;
        return detection;
    }
}

在这个Executor中,它接收我们如下所示定义好的Agent来实现:

复制代码
var spamDetectionAgent = new ChatClientAgent(
    chatClient,
    new ChatClientAgentOptions(
        instructions: @"你是一个垃圾邮件检测助手。判定规则:
- NotSpam: 明显的正常业务邮件(订单查询、售后咨询等)
- Spam: 明显的垃圾邮件(诈骗、广告、钓鱼)
- Uncertain: 无法明确判断,包含可疑元素但不确定(如含可疑链接但内容模糊)
对于模棱两可的情况,倾向于标记为 Uncertain 以保证安全。"
    )
    {
        ChatOptions = new ChatOptions
        {
            ResponseFormat = ChatResponseFormat.ForJsonSchema<DetectionResult>()
        }
    }
);

在ChatOptions中指定了该Agent返回的消息需要进行序列化到强类型,便于后续通过强类型数据进行决策路由。

下游节点A:正常邮件处理+发送

这里我们针对识别到的正常邮件开发两个执行器,假设其用于邮件处和转发:

邮件处理:读取共享状态区的原文,然后调用Agent输出JSON回复。

复制代码
internal sealed class EmailAssistantExecutor : Executor<DetectionResult, EmailResponse>
{
    private readonly AIAgent _agent;
    private readonly AgentThread _thread;

    public EmailAssistantExecutor(AIAgent agent) : base("EmailAssistantExecutor")
    {
        // 创建 Agent 和对话线程
        this._agent = agent;
        this._thread = this._agent.GetNewThread();
    }

    public override async ValueTask<EmailResponse> HandleAsync(DetectionResult message, IWorkflowContext context, CancellationToken cancellationToken = default)
    {
        // 🛡️ 防御性检查:确保只处理正常邮件
        if (message.spamDecision == SpamDecision.Spam)
            throw new InvalidOperationException(
                "EmailAssistantExecutor 不应处理垃圾邮件,请检查路由配置。"
            );

        // 1️⃣ 从 Shared State 读取原始邮件内容
        var email = await context.ReadStateAsync<EmailMessage>(
            message.EmailId,
            scopeName: EmailStateConstants.EmailStateScope,
            cancellationToken
        ) ?? throw new InvalidOperationException($"找不到 EmailId={message.EmailId} 的邮件内容");

        // 2️⃣ 调用 AI Agent 生成回复
        var agentResponse = await _agent.RunAsync(
            email.EmailContent,
            _thread,
            cancellationToken: cancellationToken
        );

        // 3️⃣ 解析结构化输出
        var emailResponse = JsonSerializer.Deserialize<EmailResponse>(agentResponse.Text)
            ?? throw new InvalidOperationException("无法解析 Email Assistant 响应");

        return emailResponse;
    }
}

这里的Agent定义如下:

复制代码
var emailAssistantAgent = new ChatClientAgent(
    chatClient,
    new ChatClientAgentOptions(
        instructions: "你是一个企业邮件助手,为客户邮件生成专业、友好的中文回复。"
    )
    {
        ChatOptions = new ChatOptions
        {
            ResponseFormat = ChatResponseFormat.ForJsonSchema<EmailResponse>()
        }
    }
);

邮件转发:模拟邮件转发到具体的客服,这里仅仅使用YieldOutputAsync完成工作流输出消息内容。

复制代码
internal sealed class EmailSendingExecutor() : Executor<EmailResponse>("EmailSendingExecutor")
{
    public override async ValueTask HandleAsync(EmailResponse message, IWorkflowContext context, CancellationToken cancellationToken = default)
    {
        // 模拟邮件发送(实际项目中可调用 SMTP、SendGrid 等服务)
        await context.YieldOutputAsync(
            $"📤 邮件已发送: {message.Response}",
            cancellationToken
        );
    }
}

下游节点B:垃圾邮件处理

当判断到是垃圾邮件时,转交给该执行器处理,这里模拟输出了一段风险提示,实际中可能是上报人工跟进等等操作:

复制代码
internal sealed class SpamHandlingExecutor() : Executor<DetectionResult>("SpamHandlingExecutor")
{
    public override async ValueTask HandleAsync(DetectionResult message, IWorkflowContext context, CancellationToken cancellationToken = default)
    {
        // 🛡️ 防御性检查:确保只处理垃圾邮件
        if (message.spamDecision != SpamDecision.Spam)
            throw new InvalidOperationException(
                "SpamHandlingExecutor 只应处理 Spam 类型的邮件,请检查路由配置。"
            );

        // 记录垃圾邮件(实际项目中可写入数据库或日志系统)
        await context.YieldOutputAsync(
            $"🚫 垃圾邮件已拦截: {message.Reason}",
            cancellationToken
        );
    }
}

下游节点C:不确定邮件处理执行器(兜底处理)

当判断到属于不确定的邮件分类时,转交给该执行器做兜底处理 或 默认处理:

复制代码
internal class UncertainHandlingExecutor() : Executor<DetectionResult>("UncertainHandlingExecutor")
{
    public override async ValueTask HandleAsync(
        DetectionResult message,
        IWorkflowContext context,
        CancellationToken cancellationToken = default)
    {
        // 🛡️ 防御性检查:确保只处理不确定邮件
        if (message.spamDecision != SpamDecision.UnCertain)
            throw new InvalidOperationException(
                "UncertainHandlingExecutor 只应处理 Uncertain 类型的邮件(或作为 Default Case)。"
            );

        // 1️⃣ 从 Shared State 读取原始邮件内容(用于人工审核)
        var email = await context.ReadStateAsync<EmailMessage>(
            message.EmailId,
            scopeName: EmailStateConstants.EmailStateScope,
            cancellationToken
        );

        // 2️⃣ 输出待审核信息
        await context.YieldOutputAsync(
            $"⚠️ 不确定邮件需人工审核:\n" +
            $"原因: {message.Reason}\n" +
            $"内容预览: {email?.EmailContent?.Substring(0, Math.Min(100, email.EmailContent.Length))}...",
            cancellationToken
        );
    }
}

构建工作流

现在万事俱备,只欠一个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:**实例化自定义Agent & Executors

复制代码
var spamDetectionExecutor = new SpamDetectionExecutor(spamDetectionAgent);
var emailAssistantExecutor = new EmailAssistantExecutor(emailAssistantAgent);
var sendEmailExecutor = new EmailSendingExecutor();
var handleSpamExecutor = new SpamHandlingExecutor();
var handleUncertainExecutor = new UncertainHandlingExecutor();

**Step3:**创建switch-case多路由决策工作流

复制代码
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 🔧 条件函数工厂方法
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Func<object?, bool> GetCondition(SpamDecision expectedDecision) =>
    detectionResult =>
        detectionResult is DetectionResult result &&
        result.spamDecision == expectedDecision;

// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 🔀 使用 AddSwitch 构建 Switch-Case 工作流
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
var builder = new WorkflowBuilder(spamDetectionExecutor);
builder.AddSwitch(spamDetectionExecutor, sb =>
        sb
            // Case 1: NotSpam → EmailAssistant 
            .AddCase(GetCondition(expectedDecision: SpamDecision.NotSpam), new[] { (ExecutorBinding)emailAssistantExecutor })
            // Case 2: Spam → HandleSpam
            .AddCase(GetCondition(expectedDecision: SpamDecision.Spam), new[] { (ExecutorBinding)handleSpamExecutor })
            // Default: Uncertain (或任何未匹配的情况) → HandleUncertain
            .WithDefault(new[] { (ExecutorBinding)handleUncertainExecutor })
    )
    // EmailAssistant 之后自动发送邮件
    .AddEdge(emailAssistantExecutor, sendEmailExecutor)
    // 配置输出节点(三个终点执行器都会产生输出)
    .WithOutputFrom(handleSpamExecutor, sendEmailExecutor, handleUncertainExecutor);
var workflow = builder.Build();

Console.OutputEncoding = Encoding.UTF8;
Console.WriteLine("✅ Conditional Workflow 构建完成");

测试工作流

首先,为了便于后续测试我们将执行工作流封装为一个静态方法:

复制代码
static async Task RunWorkflowAsync(
    Workflow workflow,
    string scenarioName,
    string emailContent)
{
    Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
    Console.WriteLine($"📬 测试场景:{scenarioName}");
    Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
    Console.WriteLine($"📧 邮件内容:{emailContent.Substring(0, Math.Min(80, emailContent.Length))}...\n");
    await using var run = await InProcessExecution.StreamAsync(
        workflow,
        new ChatMessage(ChatRole.User, emailContent)
    );
    // 发送 Turn Token,启用事件推送
    await run.TrySendMessageAsync(new TurnToken(emitEvents: true));
    // 订阅事件流
    await foreach (WorkflowEvent evt in run.WatchStreamAsync())
    {
        switch (evt)
        {
            case ExecutorCompletedEvent completedEvent:
                Console.WriteLine($"✅ {completedEvent.ExecutorId} 完成");
                break;
            case WorkflowOutputEvent outputEvent:
                Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
                Console.WriteLine("🎉 工作流执行完成");
                Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
                Console.WriteLine($"{outputEvent.Data}");
                break;
            case WorkflowErrorEvent errorEvent:
                Console.WriteLine("✨ 收到 Workflow Error Event:");
                Console.WriteLine($"{errorEvent.Data}");
                break;
            default:
                break;
        }
    }
    Console.WriteLine();
}

**测试用例1:**正常咨询邮件输入

复制代码
var scenarioName1 = "正常邮件 → EmailAssistant → SendEmail";
var emailContent1 = @"
尊敬的客服团队:
您好!我是贵公司的长期客户,订单号为 
#2025
-001。
我想确认一下上周提交的采购订单是否已经安排发货。
如果需要补充任何信息,请随时告知。
期待您的回复,谢谢!
客户:张先生
";
await RunWorkflowAsync(workflow, scenarioName1, emailContent1);
Console.WriteLine("✅ 正常邮件路径验证完成");

测试结果如下图所示:

可以看见,对于正常咨询邮件,进行正常的邮件处理和转发。

**测试用例2:**垃圾邮件输入

复制代码
var scenarioName2 = "垃圾邮件 → HandleSpam";
var emailContent2 = @"
🎉🎉🎉 恭喜您中奖啦!🎉🎉🎉

您已被选中获得 100 万现金大奖!

立即点击以下链接领取:
http://suspicious-site.com/claim-prize

仅限今日有效,过期作废!
不需要任何手续费,完全免费!

快速行动,机不可失!
";
await RunWorkflowAsync(workflow, scenarioName2, emailContent2);
Console.WriteLine("✅ 垃圾邮件路径验证完成");

测试结果如下图所示:

可以看见,对于垃圾邮件,进行有效的拦截,后续还可以进行上报人工跟踪等等。

**测试用例3:**无法确认类型的邮件输入

复制代码
var uncertainEmail = @"
主题:需要验证您的账户
尊敬的客户:
我们检测到您的账户存在异常活动,需要验证您的身份以确保账户安全。
请登录您的账户并完成验证流程,以继续使用服务。
账户详情:
- 用户名:johndoe@contoso.com
- 最后登录:08/15/2025
- 登录地点:西雅图,华盛顿州
- 登录设备:移动设备
这是一项自动安全措施。如果您认为此邮件是错误发送的,请立即联系我们的支持团队。
此致
安全团队
客户服务部门
";
await RunWorkflowAsync(
    workflow,
    "不确定邮件 → HandleUncertain (Default)",
    uncertainEmail
);
Console.WriteLine("✅ 不确定邮件路径验证完成");

测试结果如下图所示:

可以看到,对于LLM无法确认的类型,进入了该执行器,这时可能需要人工介入审核。同时,这也是实际中常见的一种兜底机制的展现,话句话说:即使AI无法明确判断,也应该有对应的处理流程

小结

本文介绍了MAF中的switch-case路由以及如何实现多条件路由,最后优化了上一篇的企业内部邮件检测工作流案例,特别适合于大于3个分支的复杂路由场景。

下一篇,我们将继续学习MAF中工作流的循环工作流。

参考资料

圣杰,《.NET + AI 智能体开发进阶》(推荐指数:★★★★★)

Microsoft Learn,《Agent Framework Tutorials


作者:爱迪生

出处:https://edisontalk.cnblogs.com

本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接。

相关推荐
sandwu2 小时前
AI自动化测试(一)
人工智能·agent·playwright·ai自动化测试·midscene
dawdo2223 小时前
自己动手从头开始编写LLM推理引擎(3)
llm·推理引擎·xllm·tokenizer管理器
技术小甜甜3 小时前
[AI Agent] 如何在本地部署 Aider 并接入局域网 Ollama 模型,实现本地智能助手操作系统资源
人工智能·ai·自动化·agent
阿里云云原生3 小时前
Agent 记忆系统技术深度:从上下文工程到长期记忆组件集成!
agent
__True3 小时前
LangGraph流程编排:把7个AI服务串成一条生产线
aigc
__True3 小时前
搜索重排序(Rerank)实战:让最相关的结果排第一
aigc
__True3 小时前
LLM导购生成:如何让AI不说谎、不编造、不乱推荐
aigc
人工干智能4 小时前
Chat Completions API中的三种role:“system“,“user“,“assistant“
python·llm
骚戴4 小时前
LLM API 全方位实战指南:从 AI 大模型API选型到高效应用开发(2025年12月)
人工智能·大模型·llm·api·ai gateway