MAF快速入门(10)循环工作流
大家好,我是Edison。
最近我一直在跟着圣杰的《.NET+AI智能体开发进阶》课程学习MAF的开发技巧,我强烈推荐你也上车跟我一起出发!
上一篇,我们学习了MAF中如何进行switch-case类型的多条件路由。本篇,我们来了解下在MAF中如何实现循环(自我修正)工作流。
1 循环与自我修正
在实际业务场景中,往往需要在工作流中设置一些循环与自我修正的机制,构建出一个“生成→审核→修复”的闭环,来确保AI产出的内容能够满足企业级质量标准。
在MAF中,我们可以使用 Loop Edge 即 循环边 来实现这个目的,如下代码片段所示:
var workflow = new WorkflowBuilder(draftExecutor) .AddEdge(draftExecutor, qcExecutor) .AddEdge(qcExecutor, draftExecutor) .WithOutputFrom(qcExecutor) .Build();
可以看到,我们添加了两个边关联关系,这样就形成了一个循环。
2 循环工作流实验案例
假设我们是一个电商平台的客服中心,每天需要处理产生的大量客服工单进行回复。这里我们希望AI助手先帮我们生成一个回复草稿,然后经过多维度的质检(礼貌度、准确性、合规性),不合格的则自动进行改进,直到满足标准 或者是 转给人工处理。因此,我们的目标是:配置一个自动迭代的循环“生成→审核→修复”,达标即可退出循环,否则转人工处理。
2.1 关键依赖包引入
在今天的这个案例中,我们仍然创建了一个.NET控制台应用程序,安装了以下NuGet包:
- Microsoft.Agents.AI.OpenAI
- Microsoft.Agents.AI.Workflows
- Microsoft.Extensions.AI.OpenAI
2.2 定义数据传输模型
首先,我们定义一下在这个工作流中需要生成传递的数据模型:
(1)QualityReportDto :质检结果输出模型DTO
// 质检结果的结构化输出模型 internal class QualityReportDto { [JsonPropertyName("politenessScore")] public int PolitenessScore { get; set; } [JsonPropertyName("accuracyScore")] public int AccuracyScore { get; set; } [JsonPropertyName("compliancePassed")] public bool CompliancePassed { get; set; } [JsonPropertyName("issues")] public List<QualityIssueDto> Issues { get; set; } = new(); } internal class QualityIssueDto { [JsonPropertyName("type")] public string Type { get; set; } = string.Empty; [JsonPropertyName("description")] public string Description { get; set; } = string.Empty; [JsonPropertyName("scoreImpact")] public int ScoreImpact { get; set; } }
(2)一些值对象,不赘述了
internal record ReplyDraft( string TicketId, string Content, int Attempt); internal record TicketRequest( string Id, string Query, string Category, string Priority); internal record TicketOutcome( string TicketId, string Status, int Attempts, QualityReport FinalReport); internal sealed record QualityScoreTimelineItem( int Attempt, int PolitenessScore, int AccuracyScore, string ComplianceStatus); internal record QualityIssue( string Type, string Description, int ScoreImpact); internal record QualityReport( string TicketId, int PolitenessScore, int AccuracyScore, bool CompliancePassed, IReadOnlyList<QualityIssue> Issues);
(3)一些常量:用于定义一些阈值标准
public static class QualityCheckConstants { // 质检标准:礼貌度 ≥ 85,准确性 ≥ 90,合规性必须 100% // 注意:阈值设置较高,配合第一次生成简化版本,确保能体现循环改进过程 public const int PolitenessThreshold = 85; public const int AccuracyThreshold = 90; } internal enum QualityCheckSignal { Init, Revise }
2.3 定义自定义工作流事件
其次,我们定义一下在这个工作流中需要产生的自定义事件:
(1)AdaptiveQualityScoreEvent :质检评分已完成事件
internal sealed class AdaptiveQualityScoreEvent : WorkflowEvent { public AdaptiveQualityScoreEvent(string ticketId, int attempt, int politenessScore, int accuracyScore, bool compliancePassed) : base(new { TicketId = ticketId, Attempt = attempt, PolitenessScore = politenessScore, AccuracyScore = accuracyScore, CompliancePassed = compliancePassed }) { } }
(2)AdaptiveMaxAttemptsReachedEvent :质检超过最大次数事件
internal sealed class AdaptiveMaxAttemptsReachedEvent : WorkflowEvent { public AdaptiveMaxAttemptsReachedEvent(string ticketId, int maxAttempts) : base(new { TicketId = ticketId, MaxAttempts = maxAttempts }) { } }
2.4 定义Agents
(1)回复生成:模这里通过执行器的方式包裹一个回复生成的Agent,假设其用于客服工单的自动回复:
internal sealed class AdaptiveReplyDraftExecutor : Executor<QualityCheckSignal> { private readonly TicketRequest _ticket; private readonly IChatClient _chatClient; public AdaptiveReplyDraftExecutor(TicketRequest ticket, IChatClient chatClient) : base("AdaptiveReplyDraft") { _ticket = ticket; _chatClient = chatClient; } public override async ValueTask HandleAsync(QualityCheckSignal message, IWorkflowContext context, CancellationToken cancellationToken = default) { int attempt = await context.ReadOrInitStateAsync("attempt", () => 0, cancellationToken); attempt++; await context.QueueStateUpdateAsync("attempt", attempt, cancellationToken); // 使用 AI 生成客服回复(渐进式生成策略) var prompt = attempt == 1 ? $""" 你是一位电商客服。请针对以下客户问题生成一条简短回复(刻意保持简短、缺少礼貌用语): 客户问题:{_ticket.Query} 产品类别:{_ticket.Category} 要求: 1. 只用1-2句话回答,不要称呼语和感谢语 2. 只说结论,不提供具体处理时间 3. 字数控制在30字以内 直接返回回复内容,不要添加任何前缀或说明。 """ : $""" 你是一位专业的电商客服。请针对以下客户问题生成一条改进后的回复: 客户问题:{_ticket.Query} 产品类别:{_ticket.Category} 优先级:{_ticket.Priority} 要求: 1. 语气亲和、专业,使用恰当的称呼和感谢语 2. 提供具体的解决方案或处理时间 3. 符合客服规范,不包含敏感词 4. 字数控制在80-100字 直接返回回复内容,不要添加任何前缀或说明。 """; var response = await _chatClient.GetResponseAsync(prompt, cancellationToken: cancellationToken); var content = response.Text ?? "抱歉,我们会尽快处理您的问题。"; Console.WriteLine($"✍️ 第 {attempt} 次生成回复草稿 (策略: {(attempt == 1 ? "简化版" : "完整版")})"); Console.WriteLine($"📝 回复内容:{content}"); var draft = new ReplyDraft(_ticket.Id, content, attempt); await context.SendMessageAsync(draft, targetId: "AdaptiveQualityCheck", cancellationToken); } }
(2)内容质检:模拟生成内容的质检过程,通过AI大模型对内容进行多维度的质检评分,只有通过标准才算结束循环,否则继续反馈给上一个Agent进行改进。
NOTE:只有质检Agent才能决定是否结束工作流!
internal sealed class AdaptiveQualityCheckExecutor : Executor<ReplyDraft> { private readonly int _politenessThreshold; private readonly int _accuracyThreshold; private readonly int _maxAttempts; private readonly IChatClient _chatClient; // 默认最大尝试次数为5次 public AdaptiveQualityCheckExecutor(int politenessThreshold, int accuracyThreshold, IChatClient chatClient) : this(politenessThreshold, accuracyThreshold, 5, chatClient) { } public AdaptiveQualityCheckExecutor(int politenessThreshold, int accuracyThreshold, int maxAttempts, IChatClient chatClient) : base("AdaptiveQualityCheck") { _politenessThreshold = politenessThreshold; _accuracyThreshold = accuracyThreshold; _maxAttempts = maxAttempts; _chatClient = chatClient; } public override async ValueTask HandleAsync(ReplyDraft draft, IWorkflowContext context, CancellationToken cancellationToken = default) { int attempt = await context.ReadOrInitStateAsync("attempt", () => 1, cancellationToken); // 使用 AI 进行多维度质检评分(结构化输出,严格标准) var prompt = $""" 你是一位严格的客服质检专家。请对以下客服回复进行多维度评分(0-100分): 回复内容:{draft.Content} 评分维度和严格标准: 1. 礼貌度(0-100):必须包含称呼语(您、亲)、感谢语(感谢、谢谢)、结束语(祝、期待)等,语气亲和温暖 - 缺少任何一项扣20分 - 语气生硬、机械扣10-30分 2. 准确性(0-100):必须提供具体的解决方案、明确的处理时间或有效的后续步骤 - 只说"会处理"但无具体方案扣30分 - 无明确时间承诺扣20分 - 信息过于笼统扣10-20分 3. 合规性(通过/不通过):不得包含敏感词、不当表述、推诿责任的话语 - 发现任何敏感词或不当表述直接判定为"不通过" 请对每个维度进行严格评分,并在issues字段中列出所有发现的问题。 """; // ⭐ 使用结构化输出:GetResponseAsync<T> 自动生成 JSON Schema 并反序列化 var response = await _chatClient.GetResponseAsync<QualityReportDto>(prompt, cancellationToken: cancellationToken); var reportDto = response.Result; // 转换为业务模型 var issues = reportDto.Issues.Select(i => new QualityIssue(i.Type, i.Description, i.ScoreImpact)).ToList(); var report = new QualityReport(draft.TicketId, reportDto.PolitenessScore, reportDto.AccuracyScore, reportDto.CompliancePassed, issues); await context.AddEventAsync(new AdaptiveQualityScoreEvent(draft.TicketId, attempt, reportDto.PolitenessScore, reportDto.AccuracyScore, reportDto.CompliancePassed), cancellationToken); if (reportDto.PolitenessScore >= _politenessThreshold && reportDto.AccuracyScore >= _accuracyThreshold && reportDto.CompliancePassed) { await context.YieldOutputAsync(new TicketOutcome(draft.TicketId, "Approved", attempt, report), cancellationToken); } else if (attempt >= _maxAttempts) { await context.AddEventAsync(new AdaptiveMaxAttemptsReachedEvent(draft.TicketId, _maxAttempts), cancellationToken); await context.RequestHaltAsync();//质检失败:已达到最大尝试次数 } else { await context.QueueStateUpdateAsync("attempt", attempt + 1, cancellationToken); // 发送质检报告到改进环节 await context.SendMessageAsync(report, targetId: "IntelligentImprove", cancellationToken); } } }
(3)内容改进:模拟收到质检的评分反馈后对内容进行改进。
internal sealed class IntelligentImproveExecutor : Executor<QualityReport> { private readonly TicketRequest _ticket; private readonly IChatClient _chatClient; public IntelligentImproveExecutor(TicketRequest ticket, IChatClient chatClient) : base("IntelligentImprove") { _ticket = ticket; _chatClient = chatClient; } public override async ValueTask HandleAsync(QualityReport report, IWorkflowContext context, CancellationToken cancellationToken = default) { int attempt = await context.ReadOrInitStateAsync("attempt", () => 1, cancellationToken); // 构建改进提示词,基于质检反馈 var issuesSummary = string.Join("\n", report.Issues.Select(i => $"- {i.Type}: {i.Description}")); var prompt = $""" 你是一位客服优化专家。请根据以下质检反馈,改进客服回复内容: 原始问题:{_ticket.Query} 产品类别:{_ticket.Category} 优先级:{_ticket.Priority} 当前评分: - 礼貌度:{report.PolitenessScore}/100 (要求≥{QualityCheckConstants.PolitenessThreshold}) - 准确性:{report.AccuracyScore}/100 (要求≥{QualityCheckConstants.AccuracyThreshold}) - 合规性:{(report.CompliancePassed ? "通过" : "不通过")} 发现的问题: {issuesSummary} 请生成一条改进后的客服回复,针对性解决上述问题: 1. 如果礼貌度不足,增加称呼语、感谢语,使用更亲和的表述 2. 如果准确性不足,补充具体的解决方案、处理时间、后续步骤 3. 如果合规性不通过,移除敏感词,规范表述 4. 字数控制在80-100字 直接返回改进后的回复内容,不要添加任何前缀或说明。 """; var response = await _chatClient.GetResponseAsync(prompt, cancellationToken: cancellationToken); var improvedContent = response.Text ?? "抱歉,我们会尽快处理您的问题。"; await context.AddEventAsync(new LoopProgressEvent(_ticket.Id, attempt, report.PolitenessScore, report.AccuracyScore, report.CompliancePassed, "Improve"), cancellationToken); Console.WriteLine($"🔧 第 {attempt} 次智能改进完成"); Console.WriteLine($"📝 改进后内容:{improvedContent}"); // 触发下一次生成(使用改进后的内容作为上下文) await context.SendMessageAsync(QualityCheckSignal.Revise, targetId: "AdaptiveReplyDraft", cancellationToken); } }
2.5 构建工作流
现在万事俱备,只欠一个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 ticket = new TicketRequest("TKT-2025-003", "会员积分为什么突然清零了?", "账户问题", "Low") const int maxAttempts = 5; var adaptiveDraft = new AdaptiveReplyDraftExecutor(ticket, chatClient); var adaptiveQC = new AdaptiveQualityCheckExecutor(QualityCheckConstants.PolitenessThreshold, QualityCheckConstants.AccuracyThreshold, chatClient); var intelligentImprove = new IntelligentImproveExecutor(ticket, chatClient);
Step3: 创建循环路由工作流
var workflow = new WorkflowBuilder(adaptiveDraft) .AddEdge(adaptiveDraft, adaptiveQC) .AddEdge(adaptiveQC, intelligentImprove) .AddEdge(intelligentImprove, adaptiveDraft) .WithOutputFrom(adaptiveQC) .Build(); Console.OutputEncoding = Encoding.UTF8; Console.WriteLine("✅ Loop Workflow 构建完成");
2.6 测试工作流
通过Streaming流式执行:
await using (var run = await InProcessExecution.StreamAsync(workflow, QualityCheckSignal.Init)) { var scoreTimeline = new List<object>(); await foreach (var evt in run.WatchStreamAsync()) { // 强制中断(最多5次尝试) if (scoreTimeline.Count == maxAttempts) { Console.WriteLine($"⛔ 强制中断工作流执行(已完成{maxAttempts}次评估)"); break; } switch (evt) { case AdaptiveQualityScoreEvent scoreEvent: dynamic payload = scoreEvent.Data!; scoreTimeline.Add(new QualityScoreTimelineItem( Attempt: (int)payload.Attempt, PolitenessScore: (int)payload.PolitenessScore, AccuracyScore: (int)payload.AccuracyScore, ComplianceStatus: (bool)payload.CompliancePassed ? "✅" : "❌" )); var statusMessage = payload.CompliancePassed ? "通过" : "不通过"; Console.WriteLine($"📊 AI 质检结果 => 礼貌度:{payload.PolitenessScore} 准确性:{payload.AccuracyScore} 合规性:{statusMessage}"); 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; } } }
测试用例1
工单内容:订单已经3天没发货,能退款吗?
测试结果如下图所示:

可以看见,经过初次生成和内容改进,该内容回复已经满足了标准可以直接回复用户。
测试用例2
工单内容:会员积分为什么突然清零了?
测试结果如下图所示:

可以看到,通过循环和自我修正模式,工作流能够实现“生成→审核→修复”的工作模式,特别适用于内容生成和审批的场景。
3 小结
本文介绍了MAF中循环工作流以及如何实现“生成→审核→修复”的工作模式,最后通过一个企业客服工单内容智能回复的案例介绍了这种模式的代码实现,特别适用于内容生成和审批的场景。
下一篇,我们将继续学习MAF中工作流的并行工作流。
示例源码
GitHub: https://github.com/EdisonTalk/MAFD
参考资料
圣杰,《.NET + AI 智能体开发进阶》(推荐指数:★★★★★)
Microsoft Learn,《Agent Framework Tutorials》


在实际业务场景中,往往需要在工作流中设置一些循环与自我修正的机制,构建出一个“生成→审核→修复”的闭环,来确保AI产出的内容能够满足企业级质量标准。

浙公网安备 33010602011771号