[Loop Engineering在MAF中的实现-06]利用Workflow来使用LoopAgent的循环策略
我们知道Workflow本质上是一个有向有环图天生适合表达这种条件循环。所以在这篇文章中我们将试着将LoopAgent中间件的循环策略采用Workflow来实现。为此我们定义了为Workflow定义了两种类型的ExecutorAgentExecutor用来调用指定的AIAgentEvaludationExecutor用来对每次迭代的输出进行评估并决定循环是否继续。如果决定继续还需要提供反馈已服务于后续的迭代。1. 采用Workflow来构建诗词创作Agent我们现在利用自定义的AgentExecutor和EvaludationExecutor来构建一个Workflow。我们会创建分别用来创作和评估的两个AIAgent对象如下三个字符串常量分别表示它们的系统指令和最终调用Workflow使用的提示词。conststringComposerInstructions你是一个精通宋词创作的智能体负责根据提供的主题和意境以词牌名**相见欢**创作一首词;conststringReviwerInstrucctions 你是一个深谙古典文化和诗词的文化大家请按照如下的标准对创作的这首词进行评价。评语保持200字以内尽可能简洁。1.格律规范(MetricalAccuracy)——30分 检测AI生成的词是否真正符合该词牌的“说明书”。-词牌结构20分总字数、分片、每句的字数长短必须与词牌名保持一致。-平仄规范5分关键位置的平仄平声、仄声必须严格符合词牌要求。-押韵规则5分检查是否在规定位置押韵是否混淆了平仄韵有无出韵。2.意境创设(ImageryConception)——30分 评估词作是否具备古典美感是否能画出**画面感**。-意象选用15分是否恰当使用了符合古典美学的意象。意象之间是否逻辑自洽没有现代感违和物。-流派风格15分整体意境的深远程度。3.语言艺术(LinguisticArtistry)——20分 评估遣词造句的功底和文字的流畅度。-用词典雅10分遣词造句需有**词味**杜绝使用现代大白话或过于生硬的拼凑词。-对仗与过片10分若词牌要求对仗需检查是否工整重点评估**过片**上下片过渡是否自然流转有无断层。4.情感寄托与创新(Emotional DepthInnovation)——20分 评估诗词的灵魂拒绝纯粹的字词堆砌。-情感共鸣10分词中所表达的悲欢离合、家国情怀或羁旅之思是否真挚饱满。-陈词翻新10分是否只会堆砌**愁**、**泪**等陈词滥调。优秀的词作应当在传统框架下有独特的视角或新颖的构思。;conststringPrompt 基于如下这首《卫风·氓》的背景和情感基调创作**一首**宋词。 原文如下 氓之蚩蚩抱布贸丝。匪来贸丝来即我谋。 送子涉淇至于顿丘。匪我愆期子无良媒。 将子无怒秋以为期。 乘彼垝垣以望复关。不见复关泣涕涟涟。 既见复关载笑载言。尔卜尔筮体无咎言。 以尔车来以我贿迁。 桑之未落其叶沃若。于嗟鸠兮无食桑葚 于嗟女兮无与士耽士之耽兮犹可说也 女之耽兮不可说也。 桑之落矣其黄而陨。自我徂尔三岁食贫。 淇水汤汤渐车帷裳。女也不爽士贰其行。 士也罔极二三其德。 三岁为妇靡室劳矣夙兴夜寐靡有朝矣。 言既遂矣至于暴怒。兄弟不知咥其笑矣。 静言思之躬自悼矣。 及尔偕老老使我怨。淇则有岸隰则有泮。 总角之宴言笑晏晏。信誓旦旦不思其反。 反是不思亦已焉哉;如下所示的composer代表我们创建的用来根据指定要求进行宋词创建的Agentreviewer是一个通过调用LLM对作品实施评估的Agent。varapiKeyEnvironment.GetEnvironmentVariable(OPENAI_API_KEY)!;varendpointEnvironment.GetEnvironmentVariable(OPENAI_BASE_URL)!;varcomposernewOpenAIClient(newApiKeyCredential(apiKey),newOpenAIClientOptions{EndpointnewUri(endpoint)}).GetChatClient(model:gpt-5.2-chat).AsIChatClient().AsAIAgent(instructions:ComposerInstructions);varreviewernewOpenAIClient(newApiKeyCredential(apiKey),newOpenAIClientOptions{EndpointnewUri(endpoint)}).GetChatClient(model:gpt-5.2-chat).AsIChatClient().AsAIAgent(name:SongLyricsReviwer,instructions:ReviwerInstrucctions);EvaludationExecutor节点内部使用一个FuncLoopContext, ValueTaskLoopEvaluation委托实施评估不过这里的LoopContext和LoopEvaluation并非LoopAgent涉及的同名类型而是我们自定义的类型。我们定义了如下的EvaluateAsync方法根据指定的AIAgent针对LoopContext上下文实施评估。我们使用了结构化输出返回一个Review对象两个字段Score和Feedback代表评估得分和反馈评语。循环是否继续取决评估得到是否到达80分以上。asyncValueTaskLoopEvaluationEvaluateAsync(AIAgentagent,LoopContextcontext){ChatMessage[]input[..context.InitialMessages,..context.LastResponse.Messages];varreview(awaitagent.RunAsyncReview(input)).Result;returnreview.Score80?new(false,null):new(true,review.Feeback$\n得分{review.Score});}[Description(针对创作诗词的评估结果)]publicclassReview{[Description(得分采用百分制)]publicintScore{get;set;}[Description(评语)]publicstringFeedback{get;set;}string.Empty;}Worflow的构建和调用体现如下的演示程序中。我们分别根据创作Agent和提供的FuncLoopContext, ValueTaskLoopEvaluation委托创建了AgentExecutor和EvaluationExecutor。我们将这两个节点添加到WorkflowBuilder中并在它们之间添加了双向边实现了调用循环。具体来说从AgentExecutor到EvaluationExecutor到之间是一条无条件的DirectEdge反向则是一条有条件的DirectEdge路由添加有作为路由消息的LoopSignal对象决定如果等于LoopSignal.Reinvoke则继续调用反之则退出。varagentExecutornewAgentExecutor(composer);varevaluationExecutornewEvaluationExecutor(contextEvaluateAsync(reviewer,context));varworkflownewWorkflowBuilder(agentExecutor).AddEdge(agentExecutor,evaluationExecutor).AddEdgeLoopSignal(evaluationExecutor,agentExecutor,signalsignalLoopSignal.Reinvoke).WithOutputFrom(agentExecutor).WithOutputFrom(evaluationExecutor).Build();varrunawaitInProcessExecution.Default.RunStreamingAsync(workflow,Prompt);string?executorIdnull;awaitforeach(varevtinrun.WatchStreamAsync()){if(evtisWorkflowOutputEventevent){if(event.ExecutorId!executorId){executorIdevent.ExecutorId;Console.WriteLine($\n\n{newstring(-,20)}{executorId}{newstring(-,20)});}if(event.DataisLoopEvaluationevaluation){Console.Write(evaluation.Feedback);}elseif(eventisAgentResponseUpdateEventupdateEvent){Console.Write(updateEvent.Data);}}if(evtisWorkflowErrorEventerrorEvent){Console.Write(errorEvent.Exception);}}我们以流的方式调用Workflow并通过监听WorkflowOutputEvent事件捕获AgentExecutor和EvaluationExecutor创作和评估的内容具体输入如下所示--------------------Agent-------------------- 相见欢·淇水旧盟 淇边初见春柔柳丝稠。 笑把青丝低绾、倚层楼。 旧盟在人空改梦难留。 一段伤心都付、晚来秋。 --------------------Evaluator-------------------- 此作依《相见欢》小令体式句读与分片大体合拍但“笑把青丝低绾、倚层楼”句字数略参差格律尚欠精严。押“尤”韵较统一音律流畅。意象取“淇边”“柳丝”“晚秋”等能呼应《氓》之旧情与弃妇之叹画面清婉。语言清丽自然有词家婉约气息但情感层次偏简未充分展开从热恋至决绝的深重转折。结句“都付晚来秋”稍近熟语创新不足。整体含蓄哀婉尚具词味。 得分72 --------------------Agent-------------------- 相见欢·淇水遗情 当年淇上相逢语春风。 误把深盟轻许、月明中。 桑阴碧人情易恨无穷。 一任残灯孤枕、听秋蛩。 --------------------Evaluator-------------------- 此作依《相见欢》小令体制句式大体合拍押“风、中、穷、蛩”韵音律尚协调但“误把深盟轻许”句平仄略欠斟酌。意象取“淇上”“桑阴”“残灯”“秋蛩”承《氓》之情事而不直袭原句颇有婉约词风。上片追忆初逢下片转入怨悔过片自然。惟篇幅稍短情感层次未能充分展开对女子由痴至醒的复杂心绪表现略浅创新处亦有限。 得分78 --------------------Agent-------------------- 相见欢·淇岸秋思 昔年携手河桥雨初消。 脉脉柔情曾向、柳边招。 桑犹在人空瘦鬓先凋。 回首旧盟如梦、暮云遥。从输出可以看出整个流程经历了3次创作-评估迭代前面两次评估得到分别是72和78。2. 将对话历史和上下文写入Workflow上下文由于针对Agent的调用需要将包括评估反馈的整个对话历史作为输入具体的评估在当前迭代的循环上下文中进行所以我们将两者封装在如下所示的LoopState中。我们希望整个状态对于用户是只读的所以我们直接将LoopState定义成只读结构体。publicreadonlyrecordstructLoopState(ListChatMessageHistory,LoopContextLoopContext){publicstaticLoopStateCreate(IEnumerableChatMessageinitialMessages)new(initialMessages.ToList(),newLoopContext(initialMessages));publicLoopStateEvaluate(AgentResponselastResponse){History.AddRange(lastResponse.Messages);returnnew(History,LoopContext.Evaluate(lastResponse));}publicLoopStateReinvoke(stringfeedback){varfeedbackMessagenewChatMessage(ChatRole.User,feedback);feedbackMessage.AdditionalProperties??[];feedbackMessage.AdditionalProperties[OnBehalfOfMessages]true;History.Add(feedbackMessage);returnnew(History,LoopContext.Reinvoke(feedback));}}publicrecordstructLoopContext{publicIReadOnlyListChatMessageInitialMessages{get;}[];publicAgentResponseLastResponse{get;privateinit;}default!;publicIReadOnlyListstringFeedback{get;privateset;}[];publicintIteration{get;privateset;}0;publicLoopContext(){}publicLoopContext(IEnumerableChatMessageoriginalMessages)InitialMessagesoriginalMessages.ToList().AsReadOnly();publicLoopContextEvaluate(AgentResponselastResponse)thiswith{LastResponselastResponse,IterationIteration1};publicLoopContextReinvoke(stringfeedback)thiswith{Feedback[..Feedback,feedback]};}表示循环上下文的LoopContext包含当前迭代次数、最初消息列表、Agent最后的响应和之前的评估反馈。两个方法Evaluate和Reinvoke分别在AgentExecutor和EvaluationExecutor执行之后被调用前者用于指定Agent的响应后者用于设置当前的评估反馈。LoopState也定义了对应的方法返回对应的状态对象。我们会将LoopState放在Workflow的上下文中所以我们针对IWorkflowContext接口定义了如下两个用来读写LoopState状态的扩展方法。publicstaticclassWorkflowContextExtensions{publicstaticValueTaskLoopStateReadLoopStateAsync(thisIWorkflowContextcontext)context.ReadStateAsyncLoopState(key:LoopState,scopeName:LoopWorkflow);publicstaticValueTaskQueueLoopStateAsync(thisIWorkflowContextcontext,LoopStateloopState)context.QueueStateUpdateAsync(key:LoopState,value:loopState,scopeName:LoopWorkflow);}3. 基于信号的路由消息MAF的Workflow通过消息路由的方式实现流程在节点之间流转。由于我们将需要处理的数据封装在LoopState中并存储在Workflow上下文所以当AgentExecutor和EvaluationExecutor执行之后需要发送一个简单的信号就可以了。为此我们定义了如下这个LoopSignal类型。publicrecordLoopSignal(LoopActionAction){publicstaticreadonlyLoopSignalEvaluatenew(LoopAction.Evaluate);publicstaticreadonlyLoopSignalReinvokenew(LoopAction.Reinvoke);}publicenumLoopAction{Evaluate,Reinvoke}枚举LoopAction定义的三个选项体现了三种路由场景EvaluateAgentExecutor在完成完成Agent调用后路由到EvaluationExecutor实施评估Reinvoke 根据评估结果要求继续调用Agent4. AgentExecutor的实现用于执行Agent的AgentExecutor直接继承自Executor。实现的两个InvokeAsync方法用于完整针对指定AIAgent的初始调用我们可以指定单纯的字符串提示词或者消息列表。为了获得更好的用户体验我们以流的形式调用指定的Agent并通过AgentResponseUpdateEvent实施输出返回的结果。整个调用结束后我们将原始输入消息和响应封装成LoopState对象并存储到Workflow上下文中最后发送一个LoopSignal.Evaluate信号路由到评估节点。publicpartialclassAgentExecutor(AIAgentagent):Executor(id:Agent){[MessageHandler(Send[typeof(LoopSignal)])]publicValueTaskInvokeAsync(stringrequest,IWorkflowContextcontext)InvokeAsync([newChatMessage(ChatRole.User,request)],context);[MessageHandler(Send[typeof(LoopSignal)])]publicasyncValueTaskInvokeAsync(IEnumerableChatMessagemessages,IWorkflowContextcontext){ListAgentResponseUpdateupdates[];awaitforeach(varupdateinagent.RunStreamingAsync(messages)){awaitcontext.AddEventAsync(newAgentResponseUpdateEvent(Id,update));updates.Add(update);}varstateLoopState.Create(messages);awaitcontext.QueueLoopStateAsync(state.Evaluate(updates.ToAgentResponse()));awaitcontext.SendMessageAsync(LoopSignal.Evaluate);}[MessageHandler(Send[typeof(LoopSignal)])]publicasyncValueTaskReinvokeAsync(LoopSignalsignal,IWorkflowContextcontext){varstateawaitcontext.ReadLoopStateAsync();ListAgentResponseUpdateupdates[];awaitforeach(varupdateinagent.RunStreamingAsync(state.History)){awaitcontext.AddEventAsync(newAgentResponseUpdateEvent(Id,update));updates.Add(update);}awaitcontext.QueueLoopStateAsync(state.Evaluate(updates.ToAgentResponse()));awaitcontext.SendMessageAsync(LoopSignal.Evaluate);}}ReinvokeAsync方法用于处理从评估节点重新路由回来的循环调用请求。我们从Workflow上下文中提取出LoopState并将其对话历史作为输入以流的形式调用Agent并以AgentResponseUpdateEvent的形式实时输出响应结果。整个调用结束之后我们针对最新的响应内容根据LoopState。在完成了针对LoopState的存储后我们发送一个LoopSignal.Evaluate信号路由到评估节点。5. EvaluationExecutor的实现我们简化了表示评估结果的LoopEvaluation的定义将它定义成如下这个只读的结构体。两个属性成员Continue和Feedback分别表示循环是否继续和提供的反馈。静态字段Stop返回一个Continue属性为false的LoopEvaluation。静态方法Reinvoke根据指定的反馈文本为继续循环创建一个LoopEvaluation。publicreadonlyrecordstructLoopEvaluation(boolContinue,string?Feedbacknull){publicstaticreadonlyLoopEvaluationStopnew(false,null);publicstaticLoopEvaluationReinvoke(stringfeedback)new(true,feedback);}整个EvaluationExecutor定义如下。我们在构造函数中我们指定了一个FuncLoopContext,ValueTaskLoopEvaluation委托用来完成真正的评估操作另一个MaxIterations参数用于设置一个最大允许的迭代次数。publicpartialclassEvaluationExecutor(FuncLoopContext,ValueTaskLoopEvaluationEvaluator,intMaxIterations5):Executor(id:Evaluator){[MessageHandler(Send[typeof(LoopSignal)],Yield[typeof(IEnumerableChatMessage)])]publicasyncValueTaskEvaluateAsync(LoopSignalsignal,IWorkflowContextcontext){varstateawaitcontext.ReadLoopStateAsync();varevaluationstate.LoopContext.IterationMaxIterations?LoopEvaluation.Stop:awaitEvaluator(state.LoopContext);if(!evaluation.Continue){varmessagesnewListChatMessage();varhistorystate.History;for(varindexstate.LoopContext.InitialMessages.Count;indexhistory.Count;index){messages.Add(history[index]);}awaitcontext.AddEventAsync(newAgentResponseEvent(Id,newAgentResponse(messages)));}else{varfeedbackevaluation.Feedback??thrownewInvalidOperationException(No feedback is provided.);awaitcontext.AddEventAsync(newWorkflowOutputEvent(evaluation,Id));awaitcontext.QueueLoopStateAsync(state.Reinvoke(feedback));awaitcontext.SendMessageAsync(LoopSignal.Reinvoke);}}}在用于完成评估的EvaluateAsync中我们从Workflow上下文中提取出LoopState。如果没有超出迭代阈值我们会调用评估委托针对LoopContext实施评估。如果评估结果要求立即终止我们会从对话历史中提取Agent的响应消息和评估返回消息并使用它们创建一个AgentResponse对象最终以AgentResponseEvent进行输出否则我们针对评估反馈更新并保存LoopState发送一个LoopSignal.Reinvoke信号路由到Agent的AgentExecutor节点。