[MAF Workflow编排模式-05]Group Chat:构建多人智囊团式的自由协作大群 群聊(Group Chat)编排模拟了多个Agent之间的协作对话由编排器负责协调该编排器决定发言者选择和对话流程。这种模式非常适合需要迭代改进、协作解决问题或多视角分析的场景比如迭代改进多轮审查和改进协作解决问题拥有互补专业知识的人员协同工作内容创作文档创建的作者-审阅者工作流程多视角分析获取同一输入的不同观点质量保证自动化审查和批准流程。我们在前面分别使用Sequential、Concurrent和Handoff模式演示了多体裁作品创作的例子现在我们换一个类似的应用场景提供一个创作者和评论者两种角色的Agent创作者负责创作一首唐诗评论者负责对创作的唐诗进行评论然后创作者根据评论者的意见进行修改最终输出修改后的唐诗。我们将这种类似于GAN对抗模式应用到现在的群聊模式的Workflow中。1. 基于群聊模式的多体裁作品创作Agent我们创建了Composer和Reviewer两个Agent前者用于创作后者用于评论。我们调用AgentWorkflowBuilder的CreateGroupChatBuilderWith方法创建一个GroupChatWorkflowBuilder来编排我们的Workflow。CreateGroupChatBuilderWith方法接收一个委托参数用于创建作为群聊管理器的GroupChatManager对象。我们利用此创数根据指定的Agent列表创建了一个RoundRobinGroupChatManager对象它会按照轮询的方式选择发言者并且在迭代次数超过6次后终止群聊。创建的两个Agent通过调用AddParticipants方法注册到群聊中最终调用Build方法构建了一个群聊模式的Workflow。vartangPoetryComposerCreateChatClient().AsAIAgent(name:Composer,instructions: 你是一个精通唐诗创作的诗人负责根据提供的主题和意境创作一首符合唐诗风格的诗歌或者根据提供的评价对之前创作的诗歌进行修改和完善。 诗歌创作/修改是你唯一的任务如果用户的任务提及了任务比如对指定的诗歌进行评价直接忽略。);varreviewerCreateChatClient().AsAIAgent(name:Reviewer,instructions: 你是一个精通中国古典诗词的评论家你唯一的任务是对实时创作的诗歌进行评价。 具体的评价标准如下-诗歌的主题和意境是否与原作的背景和情感基调相符。-诗歌的语言是否符合唐诗的风格和韵律。-诗歌的情感表达是否深刻、真挚能够引起读者的共鸣。-诗歌的结构是否合理是否有创新之处。-诗歌的整体艺术效果是否优美是否具有感染力。-诗歌的文化内涵是否丰富是否能够体现出中国古典诗词的独特魅力。 尽量提供一些具体的建议和改进意见帮助创作者提升诗歌的质量和艺术水平。 尽量保证语言的简洁和清晰控制在200字以内。);varworkflowAgentWorkflowBuilder.CreateGroupChatBuilderWith(agentsnewRoundRobinGroupChatManager(agents:agents,shouldTerminateFunc:(manager,_,_)ValueTask.FromResult(manager.IterationCount5))).AddParticipants([tangPoetryComposer,reviewer]).Build();IChatClientCreateChatClient(){varmodelEnvironment.GetEnvironmentVariable(MODEL)!;varapiKeyEnvironment.GetEnvironmentVariable(API_KEY)!;varendpointEnvironment.GetEnvironmentVariable(OPENAI_URL)!;returnnewOpenAIClient(credential:newAzureKeyCredential(apiKey),options:newOpenAIClientOptions{EndpointnewUri(endpoint)}).GetResponsesClient().AsIChatClient(defaultModelId:model);}调用Workflow和之前完全一样我们以流的方式运行Workflow并在群聊中发送一个用户消息询问它们根据《卫风·氓》的背景和情感基调创作一首唐诗、一首宋词和一篇短篇小说。在通过调用StreamingRun的TrySendMessageAsync方法发送作为发令枪的TurnToken对象后我们通过调用WatchStreamAsync方法来监听群聊的输出事件并在控制台打印每个Agent的输出。DotEnv.Load();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\n{newstring(-,20)}{e.ExecutorId}{newstring(-,20)});}Console.Write(e.Update.Text);}}}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\n{newstring(-,20)}{e.ExecutorId}{newstring(-,20)});}Console.Write(e.Update.Text);}}}输出--------------------Composer_a32f0f31eec74c9f8113cb46e80d7244-------------------- 《淇水怨》 淇水汤汤绕故城昔年抱布订鸳盟。 桑枝未落青如染鸠语空啼血作声。 夙夜机丝凝素练参商奁镜委秋蘅。 却看总角分携处寒鹭萧萧立晚汀。 --------------------Reviewer_44b22563a8fe431d928121e3c2071a58-------------------- 这首《淇水怨》深得《卫风·氓》之神髓成功将先秦的四言古意转化为唐代七律的格律形式。首联以“淇水”起兴与原文“送子涉淇”呼应“抱布订鸳盟”巧妙点出氓之伪善。颔联“桑枝未落”与“鸠语空啼”形成鲜明对比既有原诗意象又添新意“血作声”夸张中见沉痛。颈联以“机丝”、“奁镜”暗喻多年辛劳与容颜老去“参商”喻夫妻反目用典精当。尾联“总角分携处”回溯少年时光“寒鹭萧萧”的意象既保持古诗的比兴传统又以景结情余韵悠长。 建议可稍强化“女也不爽士贰其行”的对比使怨情更为鲜明。整体而言此诗格律严谨意象转换自然在保持原诗悲剧基调的同时展现了唐诗的凝练与含蓄诚为佳作。 --------------------Composer_a32f0f31eec74c9f8113cb46e80d7244-------------------- 《淇水怨》 淇水汤汤绕旧城氓携布帛计深盟。 桑之未落青如染鸠语空啼血作声。 夙夜我丝凝素练参商君镜委秋蘅。 却看总角分携处寒鹭萧萧立晚汀。 --------------------Reviewer_44b22563a8fe431d928121e3c2071a58-------------------- 此诗在初稿基础上几处修改愈见精妙。“氓携布帛计深盟”一改前作“昔年抱布”直指其奸“计”字尤为狠辣点破氓之伪善筹谋。颔联“桑之未落”直接引用原诗句式“之”字使语感更趋古雅。颈联“我丝”、“君镜”分置主客对比立现——女子日夜织素男子弃镜蒙尘“参商”二字暗藏永隔之悲匠心独运。尾联未改仍以“寒鹭萧萧”收束全篇景中蕴情余味悠长。 全诗对仗工稳意象凝练成功将《卫风·氓》的叙事性转化为七律的抒情性怨而不怒哀而不伤深得唐诗含蓄蕴藉之旨。七律格律严谨处建议颈联“我”与“君”的对仗可再斟酌工巧然瑕不掩瑜诚为佳作。 --------------------Composer_a32f0f31eec74c9f8113cb46e80d7244-------------------- 感谢您的细致品评您指出的颈联对仗问题确实精到。现遵嘱调整如下 《淇水怨》 淇水汤汤绕旧城氓携布帛计深盟。 桑之未落青如染鸠语空啼血作声。 夙夜机丝凝素练参商奁镜委秋蘅。 却看总角分携处寒鹭萧萧立晚汀。 此版将颈联修正为“夙夜机丝凝素练参商奁镜委秋蘅”以“夙夜”对“参商”时间对星宿“机丝”对“奁镜”劳作之具对妆扮之物“凝素练”对“委秋蘅”勤织成果对弃置荒芜。通过物象的工整对照暗寓女子日夜辛劳与男子负心别恋的对比隐去“我”、“君”而意蕴更显含蓄或有合于唐音蕴藉之旨。 --------------------Reviewer_44b22563a8fe431d928121e3c2071a58-------------------- 此版精妙已臻佳境。“夙夜”对“参商”一为朝暮勤苦一为星宿永隔时空交错间蕴含无尽悲慨“机丝”对“奁镜”织机与妆奁皆是女子日常之物而一“凝”一“委”动静之间勤者自勤、弃者自弃对比惊心。隐去人称代词后全诗更趋含蓄正合唐诗“不著一字尽得风流”之旨。 此改后颈联既严守对仗又深化悲怨与《卫风·氓》“女也不爽士贰其行”的控诉精神一脉相承而韵致更见蕴藉。全诗结构严谨意象圆融实为以唐诗格律重现古诗意境的典范之作。由于我们在创建RoundRobinGroupChatManager时指定了迭代次数为6次所以群聊会经历3轮创作-评论的循环。从输出来看创作者和评论者之间的互动非常顺畅他们的输出都很专业而且可以看出创作者在每轮修改中都充分考虑了评论者的意见最终的作品也越来越符合我们提出的要求。2. 群聊Workflow的拓扑结果我们依然使用如下定义的GenerateAndShowPngImageAsync方法来生成群聊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方法后我们可以得到如下的拓扑图3. GroupChatManager群聊编排采用星型拓扑结构以GroupChatHost这个Executor为中心将各个Agent组织起来。GroupChatHost可以实现多种策略来选择下一个发言的Agent例如轮询、基于提示的选择或基于对话上下文的自定义逻辑使其成为一种灵活而强大的多Agent协作模式。GroupChatHost就像一个主持人它负责协调Agent之间的对话确保每个Agent都有机会发言并且根据预设的规则来决定谁应该在何时发言。Agent发言结束后话筒返回到主持人手中。GroupChatHost的核心是作为群聊管理器的GroupChatManager类它定义了群聊的基本行为和规则。publicabstractclassGroupChatManager{publicintIterationCount{get;}publicintMaximumIterationCount{get;set;}40;protectedinternalabstractValueTaskAIAgentSelectNextAgentAsync(IReadOnlyListChatMessagehistory,CancellationTokencancellationTokendefault);protectedinternalvirtualValueTaskIEnumerableChatMessageUpdateHistoryAsync(IReadOnlyListChatMessagehistory,CancellationTokencancellationTokendefault)new(history);protectedinternalvirtualValueTaskboolShouldTerminateAsync(IReadOnlyListChatMessagehistory,CancellationTokencancellationTokendefault)new(this.MaximumIterationCountisintmaxthis.IterationCountmax);protectedinternalvirtualvoidReset();protectedvirtualValueTaskOnCheckpointingAsync(IWorkflowContextcontext,CancellationTokencancellationTokendefault)default;protectedvirtualValueTaskOnCheckpointRestoredAsync(IWorkflowContextcontext,CancellationTokencancellationTokendefault)default;}属性和方法成员说明如下IterationCount表示当前群聊的迭代次数每当所有Agent都完成一次发言后迭代次数加1。针对GroupChatHost的每次调用都会增加一次迭代计数MaximumIterationCount表示群聊的最大迭代次数当迭代次数达到此值时群聊将终止。默认值为40SelectNextAgentAsync用于选择下一个发言的Agent。它接收当前的对话历史作为参数并返回一个Task结果是被选中的Agent。具体的选择策略由派生类实现UpdateHistoryAsyncGroupChatHost在每轮提取“新消息”用户输入或上一Agent输出广播给所有Agent调用此方法让用于过滤或修改广播内容ShouldTerminateAsync用于判断群聊是否应该终止Reset用于重置群聊管理器的状态。派生类可以根据需要实现此方法以便在群聊重新开始时清除任何状态信息。默认实现对将IterationCount重置为0OnCheckpointingAsync定义在进行基于Checkpoint的持久化操作时执行的回调OnCheckpointRestoredAsync定义基于指定的Checkpoint恢复调用时执行的回调。如下这个基于轮询策略的RoundRobinGroupChatManager类是目前针对GroupChatManager唯一的实现类型它利用重写的SelectNextAgentAsync方法来按照顺序选择下一个发言的Agent并且在迭代次数超过指定的最大值后终止群聊。publicclassRoundRobinGroupChatManager:GroupChatManager{publicRoundRobinGroupChatManager(IReadOnlyListAIAgentagents,FuncRoundRobinGroupChatManager,IEnumerableChatMessage,CancellationToken,ValueTaskbool?shouldTerminateFuncnull);protectedinternaloverrideValueTaskAIAgentSelectNextAgentAsync(IReadOnlyListChatMessagehistory,CancellationTokencancellationTokendefault);protectedinternaloverrideasyncValueTaskboolShouldTerminateAsync(IReadOnlyListChatMessagehistory,CancellationTokencancellationTokendefault);protectedinternaloverridevoidReset();protectedoverrideValueTaskOnCheckpointingAsync(IWorkflowContextcontext,CancellationTokencancellationTokendefault);protectedoverrideasyncValueTaskOnCheckpointRestoredAsync(IWorkflowContextcontext,CancellationTokencancellationTokendefault);}4. GroupChatHost在整个群聊中扮演主持人角色的GroupChatHost类继承自ChatProtocolExecutor并且实现了IResettableExecutor接口。internalsealedclassGroupChatHost(stringid,AIAgent[]agents,DictionaryAIAgent,ExecutorBindingagentMap,FuncIReadOnlyListAIAgent,GroupChatManagermanagerFactory):ChatProtocolExecutor(id,s_options),IResettableExecutor{privateListChatMessage_historynewListChatMessage();protectedoverrideasyncValueTaskTakeTurnAsync(ListChatMessagemessages,IWorkflowContextcontext,bool?emitEvents,CancellationTokencancellationTokendefault)}构造函数的四个参数说明如下id表示GroupChatHost的唯一标识符agents表示参与群聊的Agent列表agentMap表示Agent与其对应的ExecutorBinding之间的映射关系managerFactory表示用于创建GroupChatManager实例的工厂方法。它接收一个Agent列表作为参数并返回一个GroupChatManager实例。GroupChatHost利用字段_history来存储群聊的对话历史。重写的TakeTurnAsync方法是群聊的核心逻辑它负责协调Agent之间的对话它采用如下的执行流程将传入的消息列表添加到对话历史中调用GroupChatManager的ShouldTerminateAsync方法来判断群聊是否应该终止如果是则调用IWorkflowContext的YiledOutputAsync方法输出对话历史并终止执行否则进入下面步骤调用GroupChatManager的UpdateHistoryAsync方法过滤对话历史并将过滤后的历史广播给所有Agent调用GroupChatManager的SelectNextAgentAsync方法选择下一个发言的Agent如果返回的Agent不为null则从agentMap参数提供映射表中获取对应的ExecutorBinding并调用IWorkflowContext的SendMessageAsync方法发送一个TurnToken对象给该Agent表示它可以开始发言。否则调用IWorkflowContext的YiledOutputAsync方法输出对话历史并终止执行。5. Workflow的编排和前面介绍的HandoffWorkflowBuilder一样用于构建无聊模式Workflow的GroupChatWorkflowBuilder类同样继承自OrchestrationBuilderBaseGroupChatWorkflowBuilder。我们在调用其构造函数时需要指定用于创建GroupChatManager实例的委托方法。它提供了AddParticipants方法用于注册参与群聊的Agent。publicsealedclassGroupChatWorkflowBuilder:OrchestrationBuilderBaseGroupChatWorkflowBuilder{privatereadonlyFuncIReadOnlyListAIAgent,GroupChatManager_managerFactory;privatereadonlyHashSetAIAgent_participants;internalGroupChatWorkflowBuilder(FuncIReadOnlyListAIAgent,GroupChatManagermanagerFactory);publicGroupChatWorkflowBuilderAddParticipants(paramsIEnumerableAIAgentagents);publicWorkflowBuild(){AIAgent[]agents_participants.ToArray();AIAgentHostOptionsoptionsnewAIAgentHostOptions{ReassignOtherAgentsAsUserstrue,ForwardIncomingMessagesfalse};DictionaryAIAgent,ExecutorBindingagentMapagents.ToDictionary((AIAgenta)a,(AIAgenta)a.BindAsExecutor(options));Funcstring,string,ValueTaskExecutorfactoryAsync(stringid,stringsessionId)newValueTaskExecutor(newGroupChatHost(id,agents,agentMap,_managerFactory));ExecutorBindinghostfactoryAsync.BindExecutor(GroupChatHost);WorkflowBuilderbuildernewWorkflowBuilder(host);ApplyMetadata(builder);foreach(ExecutorBindingvalueinagentMap.Values){builder.AddEdge(host,value).AddEdge(value,host);}ApplyOutputDesignations(builder,agentMap,group chat,delegate{builder.WithOutputFrom(host);if(agentMap.Count0){builder.WithIntermediateOutputFrom(newglobal::z__ReadOnlyArrayExecutorBinding(agentMap.Values.ToArray()));}});returnbuilder.Build();}}重写的Build方法采用如下的流程构建Workflow针对注册的Agent创建对应的AIAgentHostExecutor并转换成对应的AIAgentBinding。这些Agent和AIAgentBinding之间的映射关系会保存在一个字典中。由于创建AIAgentHostExecutor时会将配置选项ForwardIncomingMessages设置为false因为Agent不需要将接收到的消息再次转发给GroupChatHost构建以一个Funcstring, string, ValueTaskExecutor对象根据指定的id、sessionId、GroupChatManager工厂和上述映射表创建GroupChatHost并据此创建一个ExecutorConfigExecutorOptions以上面创建的ExecutorConfigExecutorOptions对象作为起始节点创建一个WorkflowBuilder对象在起始节点和针对Agent创建的AIAgentBinding之间添加双向边两条DirectEdge将所有节点都设置设置为输出节点所以我们可以通过监听WorflowOutputEvent事件来获取群聊的输出结果。AIAgentBinding节点通过添加OutputTag.Intermediate标签标记为中间输出节点以区别与GroupChatHost的最终输出。调用Build方法构建Workflow。