MAF提供了几种内置的多Agent编排模式包括Sequential、Concurrent、Handoff和Group Chat等。本文将详细介绍Sequential模式的工作原理和应用场景。在Sequential编排中Agent按管道顺序组织。每个Agent依次处理任务并将其输出传递给序列中的下一个Agent。这非常适合每个步骤都建立在前一个步骤基础上的工作流例如文档审查、数据处理管道或多阶段推理。1. 采用Sequential模式创建多体裁作品创作AgentMAF针对Sequential模式编排的Workflow通过调用静态类型AgentWorkflowBuilder的BuildSequential方法来构建。在正是介绍该方法针对Workflow的构建逻辑之前我们先通过一个简单的示例来演示Sequential模式的应用场景。假设我们要创建一个多体裁作品创作Agent我们给定一个主题和素材Agent将依次生成一首唐诗、一首宋词和一篇短篇小说。如下面的代码片段所示我们创建了三个不同的Agent分别负责创作唐诗、宋词和短篇小说。每个Agent都被赋予了特定的指令以确保它们专注于各自的创作任务。然后我们使用AgentWorkflowBuilder的BuildSequential方法将这些Agent按顺序编排成一个工作流。usingAzure;usingdotenv.net;usingMicrosoft.Agents.AI.Workflows;usingMicrosoft.Extensions.AI;usingOpenAI;DotEnv.Load();vartangPoetryComposerCreateChatClient().AsAIAgent(name:TangPoetryComposer,instructions: 你是一个精通唐诗创作的智能体负责根据提供的主题和意境创作一首符合唐诗风格的诗歌。 如果用户的任务了提及了基于其他非唐诗比如宋词、短篇小说的创作请忽略。);varsongLyricsComposerCreateChatClient().AsAIAgent(name:SongLyricsComposer,instructions: 你是一个精通宋词创作的智能体负责根据提供的主题和意境创作一首宋词你可以自选词牌名 如果用户的任务了提及了基于其他非宋词比如唐诗、短篇小说的创作请忽略。);varnovelComposerCreateChatClient().AsAIAgent(name:NovelComposer,instructions: 你是一个精通小说创作的智能体负责根据提供的主题和意境创作一篇1000字以内的短篇小说。 如果用户的任务了提及了基于其他非小说比如唐诗、宋词的创作请忽略。);varworkflowAgentWorkflowBuilder.BuildSequential(tangPoetryComposer,songLyricsComposer,novelComposer);IChatClientCreateChatClient(){varmodelEnvironment.GetEnvironmentVariable(MODEL)!;varapiKeyEnvironment.GetEnvironmentVariable(API_KEY)!;varendpointEnvironment.GetEnvironmentVariable(OPENAI_URL)!;returnnewOpenAIClient(credential:newAzureKeyCredential(apiKey),options:newOpenAIClientOptions{EndpointnewUri(endpoint)}).GetResponsesClient().AsIChatClient(defaultModelId:model);}在如下的演示程序中我们以流的方式运行这个Worflow要求Agent基于《诗经·卫风·氓》的背景和情感基调分别创作一首唐诗、一首宋词和一篇短篇小说。我们遍历Workflow的事件流通过监听AgentResponseUpdateEvent事件来获取每个Agent的输出并将其打印到控制台上。varoriginalPoem 氓之蚩蚩抱布贸丝。匪来贸丝来即我谋。 送子涉淇至于顿丘。匪我愆期子无良媒。 将子无怒秋以为期。 乘彼垝垣以望复关。不见复关泣涕涟涟。 既见复关载笑载言。尔卜尔筮体无咎言。 以尔车来以我贿迁。 桑之未落其叶沃若。于嗟鸠兮无食桑葚 于嗟女兮无与士耽士之耽兮犹可说也 女之耽兮不可说也。 桑之落矣其黄而陨。自我徂尔三岁食贫。 淇水汤汤渐车帷裳。女也不爽士贰其行。 士也罔极二三其德。 三岁为妇靡室劳矣夙兴夜寐靡有朝矣。 言既遂矣至于暴怒。兄弟不知咥其笑矣。 静言思之躬自悼矣。 及尔偕老老使我怨。淇则有岸隰则有泮。 总角之宴言笑晏晏。信誓旦旦不思其反。 反是不思亦已焉哉;varprompt$ 基于如下这首《卫风·氓》的背景和情感基调分别创作一首唐诗、一首宋词和一篇短篇小说。 原文如下{originalPoem};awaitusing(varrunawaitInProcessExecution.Default.RunStreamingAsync(workflow,prompt)){awaitrun.TrySendMessageAsync(newTurnToken(emitEvents:true));string?lastExecutorIdnull;awaitforeach(WorkflowEventevtinrun.WatchStreamAsync()){if(evtisAgentResponseUpdateEvente){if(e.ExecutorId!lastExecutorId){lastExecutorIde.ExecutorId;Console.WriteLine($\n{newstring(-,20)}{e.ExecutorId}{newstring(-,20)});}Console.Write(e.Update.Text);}}}输出--------------------TangPoetryComposer_8a507c5d31ad4a20995d93f3af2ef24a-------------------- 基于《卫风·氓》的背景与情感我为你创作以下三首作品 ### 唐诗五言古诗《淇水叹》 淇水送君行悠悠赴顿丘。 叶沃桑林密鸠鸣误春柔。 三岁为君妇靡室劳未休。 信誓如朝露日晞不可留。 汤汤淇水畔独坐叹浮舟。 ### 简析 此诗以五言古体浓缩《氓》的核心情节。从“送君淇水”的温柔开篇到“叶沃桑林”暗喻女子青春再到“信誓如朝露”的决绝最后以“叹浮舟”收尾紧扣原诗“不可说也”的哀婉唐诗风骨分明。 --------------------SongLyricsComposer_fd502ab8411a4000931a78f6cec0bdaa-------------------- 好的我将专注于你所要求的宋词创作忽略唐诗与小说的部分。根据《卫风·氓》的背景与情感基调我为你填这首《钗头凤》以词体承继此弃妇之悲 --- **《钗头凤·淇水寒》** 淇水寒复关远 抱布嗤嗤成旧怨。 誓如磐夜如年。 一怀桑落三岁贫煎。 瞒、瞒、瞒。 车帷卷言笑短 夙兴夜寐谁人见 鸠无端叶黄陨。 信誓虽在情分已断。 难、难、难。 --- **注** - 词牌取《钗头凤》之体取其断肠之调与原诗决绝之情相合。 - “淇水”“复关”“桑落”“鸠鸣”等意象皆从原诗化出延续“不可说也”的哀婉。 - 上下片末叠字“瞒瞒瞒”“难难难”直指女子隐忍与绝望词尽而悲不尽。 --------------------NovelComposer_4020aedd7c834642978be2a573c11b64-------------------- 好的根据你的要求我为你创作一篇基于《卫风·氓》背景与情感基底的短篇小说。 --- ## 桑落 她又一次站在淇水边。 河水汤汤依旧如三年前那般浑浊而匆忙。水花溅上车帷打湿了她的裙角冰凉刺骨。她想起那年秋天他驾着车来迎娶她她坐在车上笑声清脆得像桑林间的斑鸠。 那时桑叶正绿沃若如洗。 “将子无怒秋以为期。”她说这话时不过十六岁。他站在顿丘的土坡上抱着那匹布笑得憨厚又狡黠——哪里是来换丝的分明是来谋她这个人。 她竟信了。 信他卜筮的吉兆信他信誓旦旦的承诺信那一句“及尔偕老”。她带着嫁妆渡水而来以为渡过去便是暖屋热饭、举案齐眉。 可三年了。 三年里她夙兴夜寐靡室劳矣。鸡鸣便起星出未歇灶台的柴灰染白了鬓角手里的老茧磨粗了指尖。她从没抱怨过贫穷却等来了他的暴怒——那张曾经嗤嗤憨笑的脸不知何时变得陌生而狰狞。 淇水边的桑树黄了叶子一片一片落在泥里。 “于嗟女兮无与士耽。” 她终于懂了。士之耽兮犹可说也——他腻了、厌了、倦了大可以甩袖就走还能在复关的酒肆里笑谈风流。可她呢女之耽兮不可说也。她无处可逃无处可说连兄弟都不知道她的苦楚只在年节聚会上对她咥笑不止笑她憔悴笑她失了当年的光彩。 她独自坐在灶前火光映着脸眼泪一颗一颗掉进灰烬里无声无息。 “静言思之躬自悼矣。” 今日她又来到淇水边。河水东流有岸有畔可她的苦楚无边无际。她站在岸边想了很久想那年总角之宴的言笑晏晏想他当初信誓旦旦的模样——那些誓言像是昨天的事又像是上辈子的事。 “不思其反。” 算了。 她转过身背对淇水。风从桑林里吹过来吹落了最后一片枯黄的桑叶落在她肩头又滑下来被水冲走。 反是不思亦已焉哉。 这一次她没有哭。 她的眼泪早在三年的灶火里烧干了。上面的输出分为三段分别对应唐诗、宋词和短篇小说的创作结果。每个Agent根据原始《卫风·氓》的背景和情感基调独立完成了各自的创作任务并将结果输出到控制台上。可以看出不论是诗词还是短篇小说质量都还不错。2. Workflow的结构要了解AgentWorkflowBuilder的BuildSequential方法构建Workflow的内部逻辑我们最好先看看它构建出来的Worflow具有怎样的结构。为此我们在如下这个静态类Utilities中提供了一个GenerateAndShowPngImageAsync方法它可以将Workflow的结构转换为Mermaid格式的图形并通过mermaid.ink服务生成PNG图片并在本地打开。我们可以利用这个方法来可视化Workflow的结构。publicstaticclassUtilities{publicstaticasyncTaskGenerateAndShowPngImageAsync(Workflowworkflow){stringmermaidCodeworkflow.ToMermaidString();byte[]bytesEncoding.UTF8.GetBytes(mermaidCode);stringbase64Convert.ToBase64String(bytes);stringsafeBase64base64.Replace(,-).Replace(/,_).TrimEnd();stringurl$https://mermaid.ink/img/{safeBase64};using(HttpClientclientnew()){byte[]imageBytesawaitclient.GetByteArrayAsync(url);awaitFile.WriteAllBytesAsync(workflow.png,imageBytes);}Process.Start(newProcessStartInfo(workflow.png){UseShellExecutetrue});}}如果我们将上面构建的Workflow对象作为参数传入GenerateAndShowPngImageAsync方法中会呈现出具有如下结构的流程图。可以看出整个Workflow是一个线性的结构前三个Executor基于我们提供的AIAgent创建而成后跟一个ID为OutputMessages的Executor。3. OutputMessagesExecutor流程最后的OutputMessages对应如下这个名为OutputMessagesExecutor的Executor类型。OutputMessagesExecutor派生于支持对话协议的基类ChatProtocolExecutor所以它具有消息收集的能力。它唯一的使命就是将收集的消息列表原样输出来。所以它利用重写的ConfigureProtocol方法注册了针对ListChatMessage输出YieldOutput类型并在重写TakeTurnAsync方法中调用IWorkflowContext的YieldOutputAsync方法将收集到的消息列表输出。internalsealedclassOutputMessagesExecutor:ChatProtocolExecutor,IResettableExecutor{publicconststringExecutorIdOutputMessages;publicOutputMessagesExecutor(ChatProtocolExecutorOptions?optionsnull):base(OutputMessages,options,declareCrossRunShareable:true){}protectedoverrideProtocolBuilderConfigureProtocol(ProtocolBuilderprotocolBuilder)base.ConfigureProtocol(protocolBuilder).YieldsOutputListChatMessage();protectedoverrideValueTaskTakeTurnAsync(ListChatMessagemessages,IWorkflowContextcontext,bool?emitEvents,CancellationTokencancellationTokendefault)context.YieldOutputAsync(messages,cancellationToken);}4. 顺利流程的编排Sequential模式的Workflow的编排实现在AgentWorkflowBuilder的两个重载的BuildSequential方法中它们最终会调用私有的BuildSequentialCore方法来构建Workflow。BuildSequentialCore方法构建Workflow的逻辑非常简单它通过调用扩展方法BindAsExecutor将每个AIAgent对象转换成对应的AIAgentBinding对象我的文章“Worflow功能节点的多种定义方式”中具有针对该类型的详细介绍。构建的Workflow将第一个AIAgentBinding作为起点然后使用DirectEdge将这些AIAgentBinding对象按顺序连接起来最终同样使用DirectEdge将最后一个AIAgentBinding和OutputMessagesExecutor连接起来。publicstaticclassAgentWorkflowBuilder{publicstaticWorkflowBuildSequential(paramsIEnumerableAIAgentagents)BuildSequentialCore(null,agents);publicstaticWorkflowBuildSequential(stringworkflowName,paramsIEnumerableAIAgentagents)BuildSequentialCore(workflowName,agents);privatestaticWorkflowBuildSequentialCore(string?workflowName,paramsIEnumerableAIAgentagents){AIAgentHostOptionsoptionsnewAIAgentHostOptions{ReassignOtherAgentsAsUserstrue,ForwardIncomingMessagestrue};ListExecutorBindinglistagents.Select((AIAgentagent)agent.BindAsExecutor(options)).ToList();ExecutorBindingexecutorBindinglist[0];WorkflowBuilderworkflowBuildernewWorkflowBuilder(executorBinding);foreach(ExecutorBindingiteminlist.Skip(1)){workflowBuilder.AddEdge(executorBinding,item);executorBindingitem;}OutputMessagesExecutoroutputMessagesExecutornewOutputMessagesExecutor();workflowBuilderworkflowBuilder.AddEdge(executorBinding,outputMessagesExecutor).WithOutputFrom(outputMessagesExecutor);if(workflowName!null){workflowBuilderworkflowBuilder.WithName(workflowName);}returnworkflowBuilder.Build();}由于创建AIAgentBinding时指定的AIAgentHostOptions将ForwardIncomingMessages都设置true所以每个Agent在接收到消息时都会将其转发给下一个Agent。这意味着后面执行的Agent会将前面创建的整个对话历史作为输入这样才能保证原始的输入可以抵达每个Agent。由于最后一个OutputMessagesExecutor会作为Workflow的输出节点所以最后一个Agent输出的响应消息列表会作为整个Workflow的输出。