我们知道Workflow本质上是一个有向有环图,天生适合表达这种条件循环。所以在这篇文章中,我们将试着将LoopAgent中间件的循环策略采用Workflow来实现。为此我们定义了为Workflow定义了两种类型的Executor,AgentExecutor用来调用指定的AIAgent,EvaludationExecutor用来对每次迭代的输出进行评估,并决定循环是否继续。如果决定继续,还需要提供反馈已服务于后续的迭代。
1. 采用Workflow来构建诗词创作Agent
我们现在利用自定义的AgentExecutor和EvaludationExecutor来构建一个Workflow。我们会创建分别用来创作和评估的两个AIAgent对象,如下三个字符串常量分别表示它们的系统指令和最终调用Workflow使用的提示词。
conststringComposerInstructions="你是一个精通宋词创作的智能体,负责根据提供的主题和意境以词牌名**相见欢**创作一首词";conststringReviwerInstrucctions=""" 你是一个深谙古典文化和诗词的文化大家,请按照如下的标准对创作的这首词进行评价。评语保持200字以内,尽可能简洁。1.格律规范(MetricalAccuracy)——30分 检测AI生成的词是否真正符合该词牌的“说明书”。-词牌结构(20分):总字数、分片、每句的字数长短必须与词牌名保持一致。-平仄规范(5分):关键位置的平仄(平声、仄声)必须严格符合词牌要求。-押韵规则(5分):检查是否在规定位置押韵,是否混淆了平仄韵,有无出韵。2.意境创设(Imagery&Conception)——30分 评估词作是否具备古典美感,是否能画出**画面感**。-意象选用(15分):是否恰当使用了符合古典美学的意象。意象之间是否逻辑自洽,没有现代感违和物。-流派风格(15分):整体意境的深远程度。3.语言艺术(LinguisticArtistry)——20分 评估遣词造句的功底和文字的流畅度。-用词典雅(10分):遣词造句需有**词味**,杜绝使用现代大白话或过于生硬的拼凑词。-对仗与过片(10分):若词牌要求对仗,需检查是否工整;重点评估**过片**(上下片过渡)是否自然流转,有无断层。4.情感寄托与创新(Emotional Depth&Innovation)——20分 评估诗词的灵魂,拒绝纯粹的字词堆砌。-情感共鸣(10分):词中所表达的悲欢离合、家国情怀或羁旅之思是否真挚饱满。-陈词翻新(10分):是否只会堆砌**愁**、**泪**等陈词滥调。优秀的词作应当在传统框架下有独特的视角或新颖的构思。""";conststringPrompt=""" 基于如下这首《卫风·氓》的背景和情感基调,创作**一首**宋词。 原文如下: 氓之蚩蚩,抱布贸丝。匪来贸丝,来即我谋。 送子涉淇,至于顿丘。匪我愆期,子无良媒。 将子无怒,秋以为期。 乘彼垝垣,以望复关。不见复关,泣涕涟涟。 既见复关,载笑载言。尔卜尔筮,体无咎言。 以尔车来,以我贿迁。 桑之未落,其叶沃若。于嗟鸠兮,无食桑葚! 于嗟女兮,无与士耽!士之耽兮,犹可说也; 女之耽兮,不可说也。 桑之落矣,其黄而陨。自我徂尔,三岁食贫。 淇水汤汤,渐车帷裳。女也不爽,士贰其行。 士也罔极,二三其德。 三岁为妇,靡室劳矣;夙兴夜寐,靡有朝矣。 言既遂矣,至于暴怒。兄弟不知,咥其笑矣。 静言思之,躬自悼矣。 及尔偕老,老使我怨。淇则有岸,隰则有泮。 总角之宴,言笑晏晏。信誓旦旦,不思其反。 反是不思,亦已焉哉!""";如下所示的composer代表我们创建的用来根据指定要求进行宋词创建的Agent,reviewer是一个通过调用LLM对作品实施评估的Agent。
varapiKey=Environment.GetEnvironmentVariable("OPENAI_API_KEY")!;varendpoint=Environment.GetEnvironmentVariable("OPENAI_BASE_URL")!;varcomposer=newOpenAIClient(newApiKeyCredential(apiKey),newOpenAIClientOptions{Endpoint=newUri(endpoint)}).GetChatClient(model:"gpt-5.2-chat").AsIChatClient().AsAIAgent(instructions:ComposerInstructions);varreviewer=newOpenAIClient(newApiKeyCredential(apiKey),newOpenAIClientOptions{Endpoint=newUri(endpoint)}).GetChatClient(model:"gpt-5.2-chat").AsIChatClient().AsAIAgent(name:"SongLyricsReviwer",instructions:ReviwerInstrucctions);EvaludationExecutor节点内部使用一个Func<LoopContext, ValueTask<LoopEvaluation>>委托实施评估,不过这里的LoopContext和LoopEvaluation并非LoopAgent涉及的同名类型,而是我们自定义的类型。我们定义了如下的EvaluateAsync方法根据指定的AIAgent针对LoopContext上下文实施评估。我们使用了结构化输出返回一个Review对象,两个字段Score和Feedback代表评估得分和反馈评语。循环是否继续取决评估得到是否到达80分以上。
asyncValueTask<LoopEvaluation>EvaluateAsync(AIAgentagent,LoopContextcontext){ChatMessage[]input=[..context.InitialMessages,..context.LastResponse.Messages];varreview=(awaitagent.RunAsync<Review>(input)).Result;returnreview.Score>=80?new(false,null):new(true,review.Feeback+$"\n得分:{review.Score}");}[Description("针对创作诗词的评估结果")]publicclassReview{[Description("得分,采用百分制")]publicintScore{get;set;}[Description("评语")]publicstringFeedback{get;set;}=string.Empty;}Worflow的构建和调用体现如下的演示程序中。我们分别根据创作Agent和提供的Func<LoopContext, ValueTask<LoopEvaluation>>委托创建了AgentExecutor和EvaluationExecutor。我们将这两个节点添加到WorkflowBuilder中,并在它们之间添加了双向边实现了调用循环。具体来说,从AgentExecutor到EvaluationExecutor到之间是一条无条件的DirectEdge,反向则是一条有条件的DirectEdge,路由添加有作为路由消息的LoopSignal对象决定:如果等于LoopSignal.Reinvoke则继续调用,反之则退出。
varagentExecutor=newAgentExecutor(composer);varevaluationExecutor=newEvaluationExecutor(context=>EvaluateAsync(reviewer,context));varworkflow=newWorkflowBuilder(agentExecutor).AddEdge(agentExecutor,evaluationExecutor).AddEdge<LoopSignal>(evaluationExecutor,agentExecutor,signal=>signal==LoopSignal.Reinvoke).WithOutputFrom(agentExecutor).WithOutputFrom(evaluationExecutor).Build();varrun=awaitInProcessExecution.Default.RunStreamingAsync(workflow,Prompt);string?executorId=null;awaitforeach(varevtinrun.WatchStreamAsync()){if(evtisWorkflowOutputEvent@event){if(@event.ExecutorId!=executorId){executorId=@event.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(List<ChatMessage>History,LoopContextLoopContext){publicstaticLoopStateCreate(IEnumerable<ChatMessage>initialMessages)=>new(initialMessages.ToList(),newLoopContext(initialMessages));publicLoopStateEvaluate(AgentResponselastResponse){History.AddRange(lastResponse.Messages);returnnew(History,LoopContext.Evaluate(lastResponse));}publicLoopStateReinvoke(stringfeedback){varfeedbackMessage=newChatMessage(ChatRole.User,feedback);feedbackMessage.AdditionalProperties??=[];feedbackMessage.AdditionalProperties["OnBehalfOfMessages"]=true;History.Add(feedbackMessage);returnnew(History,LoopContext.Reinvoke(feedback));}}publicrecordstructLoopContext{publicIReadOnlyList<ChatMessage>InitialMessages{get;}=[];publicAgentResponseLastResponse{get;privateinit;}=default!;publicIReadOnlyList<string>Feedback{get;privateset;}=[];publicintIteration{get;privateset;}=0;publicLoopContext(){}publicLoopContext(IEnumerable<ChatMessage>originalMessages)=>InitialMessages=originalMessages.ToList().AsReadOnly();publicLoopContextEvaluate(AgentResponselastResponse)=>thiswith{LastResponse=lastResponse,Iteration=Iteration+1};publicLoopContextReinvoke(stringfeedback)=>thiswith{Feedback=[..Feedback,feedback]};}表示循环上下文的LoopContext包含当前迭代次数、最初消息列表、Agent最后的响应和之前的评估反馈。两个方法Evaluate和Reinvoke分别在AgentExecutor和EvaluationExecutor执行之后被调用,前者用于指定Agent的响应,后者用于设置当前的评估反馈。LoopState也定义了对应的方法返回对应的状态对象。
我们会将LoopState放在Workflow的上下文中,所以我们针对IWorkflowContext接口定义了如下两个用来读写LoopState状态的扩展方法。
publicstaticclassWorkflowContextExtensions{publicstaticValueTask<LoopState>ReadLoopStateAsync(thisIWorkflowContextcontext)=>context.ReadStateAsync<LoopState>(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){publicstaticreadonlyLoopSignalEvaluate=new(LoopAction.Evaluate);publicstaticreadonlyLoopSignalReinvoke=new(LoopAction.Reinvoke);}publicenumLoopAction{Evaluate,Reinvoke}枚举LoopAction定义的三个选项体现了三种路由场景:
- Evaluate:
AgentExecutor在完成完成Agent调用后,路由到EvaluationExecutor实施评估; - Reinvoke: 根据评估结果,要求继续调用Agent;
4. 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(IEnumerable<ChatMessage>messages,IWorkflowContextcontext){List<AgentResponseUpdate>updates=[];awaitforeach(varupdateinagent.RunStreamingAsync(messages)){awaitcontext.AddEventAsync(newAgentResponseUpdateEvent(Id,update));updates.Add(update);}varstate=LoopState.Create(messages);awaitcontext.QueueLoopStateAsync(state.Evaluate(updates.ToAgentResponse()));awaitcontext.SendMessageAsync(LoopSignal.Evaluate);}[MessageHandler(Send=[typeof(LoopSignal)])]publicasyncValueTaskReinvokeAsync(LoopSignalsignal,IWorkflowContextcontext){varstate=awaitcontext.ReadLoopStateAsync();List<AgentResponseUpdate>updates=[];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?Feedback=null){publicstaticreadonlyLoopEvaluationStop=new(false,null);publicstaticLoopEvaluationReinvoke(stringfeedback)=>new(true,feedback);}整个EvaluationExecutor定义如下。我们在构造函数中,我们指定了一个Func<LoopContext,ValueTask<LoopEvaluation>>委托用来完成真正的评估操作,另一个MaxIterations参数用于设置一个最大允许的迭代次数。
publicpartialclassEvaluationExecutor(Func<LoopContext,ValueTask<LoopEvaluation>>Evaluator,intMaxIterations=5):Executor(id:"Evaluator"){[MessageHandler(Send=[typeof(LoopSignal)],Yield=[typeof(IEnumerable<ChatMessage>)])]publicasyncValueTaskEvaluateAsync(LoopSignalsignal,IWorkflowContextcontext){varstate=awaitcontext.ReadLoopStateAsync();varevaluation=state.LoopContext.Iteration>=MaxIterations?LoopEvaluation.Stop:awaitEvaluator(state.LoopContext);if(!evaluation.Continue){varmessages=newList<ChatMessage>();varhistory=state.History;for(varindex=state.LoopContext.InitialMessages.Count;index<history.Count;index++){messages.Add(history[index]);}awaitcontext.AddEventAsync(newAgentResponseEvent(Id,newAgentResponse(messages)));}else{varfeedback=evaluation.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节点。