MAF快速入门(4)多Agent工作流编排

大家好,我是Edison。

上一篇,我们学习了MAF中如何持久化聊天记录到关系型数据库。这一篇,我们来学习一下工作流编排。不知大家是否记得,我们在之前用Semantic Kernel学习过多Agent编排的一些知识,例如顺序,并发,移交等模式仍然历历在目。那么,今天,我们用MAF的工作流编排来实现一下,看看有什么不一样。

多Agent编排

传统的单代理系统在处理复杂多面任务的能力方面受到限制。 通过协调多个代理,每个代理都有专门的技能或角色,我们可以创建更可靠、更自适应且能够协作解决实际问题的系统。

目前,MAF支持以下编排模式,和SK几乎一致:

今天,我们用MAF来实践一下顺序编排 和 移交编排 两个最常用的模式。

顺序编排的典型案例如下图所示:

移交编排的典型案例如下图所示:

准备工作

在今天的这个案例中,我们创建了一个.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/Qwen2.5-32B-Instruct"
  }
}

这里我们使用 SiliconCloud 提供的 Qwen2.5-32B-Instruct 模型,你可以通过这个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>();

实现移交编排

首先,我们创建一个ChatClient供后续使用:

复制代码
// Step1. Create one ChatClient
var chatClient = new OpenAIClient(
        new ApiKeyCredential(openAIProvider.ApiKey),
        new OpenAIClientOptions { Endpoint = new Uri(openAIProvider.Endpoint) })
    .GetChatClient(openAIProvider.ModelId);

然后,我们定义一个FunctionAgentFactory,封装我们需要编排的几个Agent:

复制代码
public class FunctionAgentFactory
{
    public static ChatClientAgent CreateTriageAgent(ChatClient client)
    {
        return client.CreateAIAgent(
            "You determine which agent to use based on the user's homework question. ALWAYS handoff to another agent.",
            "triage_agent",
            "Routes messages to the appropriate specialist agent");
    }
    public static ChatClientAgent CreateHistoryTutorAgent(ChatClient client)
    {
        return client.CreateAIAgent(
            "You provide assistance with historical queries. Explain important events and context clearly. Please only respond about history.",
            "history_tutor",
            "Specialist agent for historical questions");
    }
    public static ChatClientAgent CreateMathTutorAgent(ChatClient client)
    {
        return client.CreateAIAgent(
            "You provide help with math problems. Explain your reasoning at each step and include examples. Please only respond about math.",
            "math_tutor",
            "Specialist agent for mathematical questions");
    }
}

基于AgentFactory依次创建编排Agent,历史Agent 和 数学Agent:

复制代码
var triageAgent = FunctionAgentFactory.CreateTriageAgent(chatClient);
var historyTutor = FunctionAgentFactory.CreateHistoryTutorAgent(chatClient);
var mathTutor = FunctionAgentFactory.CreateMathTutorAgent(chatClient);

基于上面的3个Agent可以快速创建一个移交编排Workflow:

复制代码
var workflow = AgentWorkflowBuilder.CreateHandoffBuilderWith(triageAgent)
    .WithHandoffs(triageAgent, [mathTutor, historyTutor]) // Triage can route to either specialist
    .WithHandoffs([mathTutor, historyTutor], triageAgent) // Math or History tutor can return to triage
    .Build();

怎么样,是不是很简单?

下面,我们进行一个多轮对话测试:

复制代码
List<ChatMessage> messages = new();
while (true)
{
    Console.Write("User: ");
    string userInput = Console.ReadLine()!;
    messages.Add(new(ChatRole.User, userInput));
    // Execute workflow and process events
    var response = await workflow.AsAgent().RunAsync(messages);
    Console.WriteLine($"Agent: {response}\n");
    // Add new messages to conversation history
    messages.AddRange(response.Messages);
}

这里我们使用了Workflow as Agent的方式,即将工作流转换为一个独立的Agent来调用。

测试结果如下图所示:

可以看到,编排Agent根据用户的输入将请求转给了对应的数学话题Agent 和 历史话题Agent,进而使用户获得正确的回答。

实现顺序编排

首先,创建一个ChatClient,同上。

然后,封装一个AgentFactory来定义我们用到的3个Agent:

  • Analyst 需求分析

  • Writer 文案写手

  • Editor 文案审稿人/编辑

复制代码
public class FunctionAgentFactory
{
    public static ChatClientAgent CreateAnalystAgent(ChatClient client)
    {
        return client.CreateAIAgent(
            """
                You are a marketing analyst. Given a product description, identify:
                - Key features
                - Target audience
                - Unique selling points
                """,
            "Analyst",
            "An agent that extracts key concepts from a product description.");
    }
    public static ChatClientAgent CreatWriterAgent(ChatClient client)
    {
        return client.CreateAIAgent(
            """
                You are a marketing copywriter. Given a block of text describing features, audience, and USPs,
                compose a compelling marketing copy (like a newsletter section) that highlights these points.
                Output should be short (around 150 words), output just the copy as a single text block.
                """,
            "CopyWriter",
            "An agent that writes a marketing copy based on the extracted concepts.");
    }
    public static ChatClientAgent CreateEditorAgent(ChatClient client)
    {
        return client.CreateAIAgent(
            """
                You are an editor. Given the draft copy, correct grammar, improve clarity, ensure consistent tone,
                give format and make it polished. Output the final improved copy as a single text block.
                """,
            "Editor",
            "An agent that formats and proofreads the marketing copy.");
    }
}

基于这几个Agent我们快速定一个顺序工作流:

复制代码
var analyst = FunctionAgentFactory.CreateAnalystAgent(chatClient);
var writer = FunctionAgentFactory.CreatWriterAgent(chatClient);
var editor = FunctionAgentFactory.CreateEditorAgent(chatClient);
// Workflow: Analyst -> Writer -> Editor
var workflow = AgentWorkflowBuilder.BuildSequential(
  "content-team-workflow", 
  [analyst, writer, editor]);

快速运行,直接得到答案:

复制代码
var userMessage = "Please help to introduce our new product: An eco-friendly stainless steel water bottle that keeps drinks cold for 24 hours.";
Console.Write($"User: {userMessage}\n");
// Execute the workflow via RunAsync
var response = await workflow.AsAgent().RunAsync(userMessage);
Console.WriteLine($"Agent: {response}");

这里用户给的任务是:"Please help to introduce our new product: An eco-friendly stainless steel water bottle that keeps drinks cold for 24 hours."

假设客户公司有一个新产品:一个环保的不锈钢水瓶,可以让饮料保持24小时的低温,需要帮忙创作一个广告文案。

我们仍然将其转换为Agent来执行,获取结果下图所示:


可以看到,它将最后审核校对的最终版内容输出给了我们。

借助事件机制监控

假设我们想要获取每个步骤对应Agent的输出内容,进而实现一定程度的监控。那么,同步执行就不合适了,这时我们就需要用到工作流的事件机制 结合 流式执行获取事件流 来实现。

MAF的工作流事件机制内置了一些系统事件,例如:

  • AgentRunUpdatedEvent

  • ExecutorCompletedEvent 执行器完成事件,例如Workflow中的某个Agent完成了任务,准备转到下个Agent执行。

  • WorkflowOutputEvent 整个工作流已完成任务处理准备进行最终输出。

这里我们使用MAF中的StreamAsync 和 WatchStreamAsync,其主要流程如下图所示:

下面我们直接看代码:

复制代码
await using (StreamingRun run = await InProcessExecution.StreamAsync(workflow, userMessage))
{
    await run.TrySendMessageAsync(new TurnToken(emitEvents: true)); // Enable event emitting
    Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
    Console.WriteLine("Process Tracking");
    Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
    Console.WriteLine();
    var result = new List<ChatMessage>();
    var stageOutput = new StringBuilder();
    int stepNumber = 1;
    await foreach (WorkflowEvent evt in run.WatchStreamAsync())
    {
        if (evt is AgentRunUpdateEvent updatedEvent)
        {
            stageOutput.Append($"{updatedEvent.Data} ");
        }
        else if (evt is ExecutorCompletedEvent completedEvent)
        {
            if (stageOutput.Length > 0)
            {
                Console.WriteLine($"Step {stepNumber}: {completedEvent.ExecutorId}");
                Console.WriteLine($"Output: {stageOutput.ToString()}\n");
                stepNumber++;
                stageOutput.Clear();
            }
        }
        else if (evt is WorkflowOutputEvent endEvent)
        {
            result = (List<ChatMessage>)endEvent.Data!;
            break;
        }
    }
    // Display final result and skip user message
    foreach (var message in result.Skip(1))
        Console.WriteLine($"Agent: {message.Text}");
}
Console.WriteLine("\nTask Finished!");

最终,输出的结果如下所示:

Step1: Analyst

Step2 ~3: Writer & Editor

最终输出的内容

由上图可以看出,如果我们需要对于Workflow进行进度监控,使用流式执行 并 监听内置事件 完全可以实现这个目的。

其他更多的内置工作流事件如下代码示例所示:

复制代码
// 执行工作流并监听所有事件
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("🚀 开始执行工作流 (流式模式)");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
var input = "Hello Workflow Events!";
Console.WriteLine($"📥 输入: \"{input}\"\n");
// 启动流式执行
StreamingRun run = await InProcessExecution.StreamAsync(simpleWorkflow, input);
Console.WriteLine("📡 开始监听事件流...\n");
Console.WriteLine(new string('─', 60));
int eventCount = 0;
// 订阅事件流
await foreach (WorkflowEvent evt in run.WatchStreamAsync())
{
    eventCount++;
    // 显示事件的完整信息
    Console.WriteLine($"\n🔔 事件 #{eventCount}");
    Console.WriteLine($"   类型: {evt.GetType().Name}");
    // 根据事件类型显示详细信息
    // switch (evt)
    // {
    //     case WorkflowStartedEvent:
    //         Console.WriteLine($"   说明: 工作流开始执行");
    //         break;
    //     case ExecutorInvokedEvent invokedEvent:
    //         Console.WriteLine($"   Executor: {invokedEvent.ExecutorId}");
    //         Console.WriteLine($"   说明: Executor 开始执行");
    //         Console.WriteLine($"   输入: {invokedEvent.Data}");
    //         break;
    //     case ExecutorCompletedEvent completedEvent:
    //         Console.WriteLine($"   Executor: {completedEvent.ExecutorId}");
    //         Console.WriteLine($"   说明: Executor 执行完成");
    //         Console.WriteLine($"   输出: {completedEvent.Data}");
    //         break;
    //     case SuperStepCompletedEvent stepEvent:
    //         Console.WriteLine($"   SuperStep: #{stepEvent.StepNumber}");
    //         Console.WriteLine($"   说明: 执行步骤完成");
    //         break;
    //     case WorkflowOutputEvent outputEvent:
    //         Console.WriteLine($"   来源: {outputEvent.SourceId}");
    //         Console.WriteLine($"   说明: 工作流产生输出");
    //         Console.WriteLine($"   结果: {outputEvent.Data}");
    //         break;
    //     default:
    //         Console.WriteLine($"   数据: {evt.Data}");
    //         break;
    // }
    Console.WriteLine(new string('─', 60));
}
Console.WriteLine($"\n✅ 工作流执行完成,共产生 {eventCount} 个事件");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");

小结

本文介绍了如何使用MAF框架完成基本的多Agent工作流编排,并给出了两个常用的编排案例:移交编排 和 顺序编排,最后介绍了如何借助结合流式执行 和 内置事件 完成工作流的进度监控。

下一篇,我们将继续MAF的学习。

示例源码

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

参考资料

Microsoft Learn,《Agent Framework Tutorials

推荐学习

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


作者:爱迪生

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

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

相关推荐
A达峰绮1 小时前
AI时代下的护城河:哪些行业正被重塑,哪些将永不消失?
人工智能·ai·aigc
阿里云云原生2 小时前
AgentScope 拥抱函数计算 FC,为 Agent 应用提供 Serverless 运行底座
serverless·agent
冴羽2 小时前
10 个 Nano Banana Pro 专业级生图技巧
前端·人工智能·aigc
沛沛老爹2 小时前
AI入门之LangChain Agent工具链组合设计:从理论到产业落地的AI智能体架构指南
人工智能·架构·langchain·agent·ai入门
GeekPMAlex2 小时前
用 LangGraph v1.0 构建生产级 RAG:高吞吐、低幻觉、全链路可观测
agent
mortimer3 小时前
视频自动翻译里的“时空折叠”:简单实用的音画同步实践
python·ffmpeg·aigc
PetterHillWater3 小时前
AI浏览器Comet用户体验测试
aigc·测试
badmonster03 小时前
AI ETL需要不同的原语:从构建CocoIndex中学到的Rust经验🦀
rust·aigc
赋范大模型技术社区3 小时前
LangChain1.0 搭建法务合同审核 Agent(附源码)
langchain·ocr·agent·rag·文档审核·langchain1.0