[MAF Workflow编排模式-02]如何解决Sequential模式下前置节点越权执行的问题?

发布时间:2026/7/5 14:50:41
[MAF Workflow编排模式-02]如何解决Sequential模式下前置节点越权执行的问题? 由于Sequential模式构建的Worflow由多一个AIAgent按照编排的顺序执行前置节点越权执行是一个非常常见的现象。举个简单的例子假设我们采用此模式构建一个包含如下三个Agent的Workflow数据收集Agent、数据分析Agent和数据报告Agent在调用时Workflow时我输入如下的任务提取最近三年手机三大品牌全球销量、然后通过分析数据生成一份销售报告最终以邮件形式发出来。貌似很合理对吧但是会出现一个问题完整的提示词的三部分其实是分别对三个Agent说的但是完整的提示词直接传给了第一个Agent导致第一个Agent越权执行了后续两个Agent的任务。虽然可以利用系统指令强行让每个Agent只关注自己负责的那一块但是这样的做法并非每次都有效。1. 前置节点把所有的事都干了还记得前文演示的多体裁作品创作智能体的三个分别用于创作唐诗、宋词和短篇小说的三个AIAgent是如何创建的吗如下面的代码所示我们通过指定了系统指令让三个Agent各司其职不要越界。vartangPoetryComposerCreateChatClient().AsAIAgent(name:TangPoetryComposer,instructions: 你是一个精通唐诗创作的智能体负责根据提供的主题和意境创作一首符合唐诗风格的诗歌。 如果用户的任务了提及了基于其他非唐诗比如宋词、短篇小说的创作请忽略。);varsongLyricsComposerCreateChatClient().AsAIAgent(name:SongLyricsComposer,instructions: 你是一个精通宋词创作的智能体负责根据提供的主题和意境创作一首宋词你可以自选词牌名 如果用户的任务了提及了基于其他非宋词比如唐诗、短篇小说的创作请忽略。);varnovelComposerCreateChatClient().AsAIAgent(name:NovelComposer,instructions: 你是一个精通小说创作的智能体负责根据提供的主题和意境创作一篇1000字以内的短篇小说。 如果用户的任务了提及了基于其他非小说比如唐诗、宋词的创作请忽略。);如果将对应的排他性指令文本去掉第二句话去掉后再次执行演示程序就会出现如下的输出。可以看出用于创建唐诗和小说的Agent都完成了三种体裁的创作。由于LLM的生成是基于概率的所以每次执行的结果可能不一样但是前置节点越权执行的现象是必然会出现的。而且及时我们添加了排他性指令其实也不能保证每次执行都能有效。--------------------TangPoetryComposer_1cb0e91144de43c7bbf145a3e0a0cd73-------------------- 《弃妇吟》 淇水汤汤送旧人桑叶飘零碾作尘。 昔时抱布情如蜜今日挥鞭意似焚。 三秋甘受贫家苦一夜翻成陌路嗔。 莫道红颜多薄命从来白首负深恩。 《蝶恋花·秋桑叹》 犹记抱布春意逗涉过淇滨暗把终身就。桑葚垂枝红染袖谁知鸠鸟啄心透。 黄叶西风凋碧后贫贱夫妻泪渍青衫皱。信誓如烟逐水流残阳空照秋桑瘦。 《氓之变》 秋雨敲打着淇水河岸阿蘅跪在泥泞里捡拾散落的蚕茧。三年前那个抱着布匹来换丝的男人此刻正把她的妆奁往牛车上扔镶铜的奁盒砸在青石上发出碎响。 “这些桑叶全喂了野蚕。”氓踢开竹匾去年晒好的桑叶卷着霉斑滚进水洼。他腰间别着新换的玉钩那是城西当铺李掌柜家闺女的陪嫁。阿蘅还记得三个月前氓第一次穿上绸衫时袖口掉出的鸳鸯锦帕。 淇水的浪头打湿了她的麻布裙裾恍惚间变成成亲那日被抱上牛车时溅起的河水。那时氓的掌心贴着喜饼的油渍许下的誓比顿丘的磐石还重。可三更天的织机声磨破了她的指尖灶台的烟火熏哑了她的嗓子却换来氓把算盘摔在她膝上“三年就织了这些” 最寒的不是秋风是今早氓把休书拍在桌上的声响。泛黄的纸上列着她的罪状无子、多言、炊米太费。她忽然想起母亲当年用桑葚汁给她染指甲时说“斑鸠吃多了桑葚会醉死女子贪恋情爱会碎心。” 牛车启动时阿蘅攥住车辕“你说过白头偕老的。”氓的鞭子抽在黄牛背上声音比初冬的冰还脆“淇水还有岸隰地还有边就你这怨气没个尽头。” 散落的蚕茧被车轮碾成絮阿蘅突然笑了。她想起总角时和氓偷摘邻家桑葚紫红的汁液染了满襟那时他说要娶她她笑得像三月的黄莺。如今才懂斑鸠醉倒尚能醒女子醉情终生误。 --------------------SongLyricsComposer_bde281621a5a408dbaee85b9f1e99640-------------------- 《雨霖铃·秋桑叹》 秋霖初歇。对淇河晚败叶堆雪。 残垣尚记凝望烟波断处归帆明灭。 总角青梅如昨竟翻作长别。 算三载、机杼声寒茧破丝空寸心裂。 斑鸠误啄桑葚血。更那堪、露重芳菲歇。 罗襦暗数针脚针针是、旧年呜咽。 玉勒新鞍谁见空奁碎珮堕寒月。 纵觅得、隰泮淇涯不是回春碣。 注此词以《雨霖铃》为调取秋雨初霁、桑林凋敝之象暗合弃妇零落身世。上阕以“残垣凝望”勾连《氓》诗“乘彼垝垣”之境“茧破丝空”喻三年辛劳成空。下阕化用“鸠食桑葚”典故以“玉勒新鞍”对照“空奁碎珮”凸显男子变心后的物质反差。结句借“隰泮淇涯”反用原诗言纵有边界可寻而人生无再少之春较原诗“亦已焉哉”更添一层宿命悲怆。 --------------------NovelComposer_5aa12d37851d4a3194ed6c97b6cf7cbb-------------------- 好的依你之愿我将为你呈现基于同一母题、却风格迥异的三篇创作。我们将先从唐诗的凝练哀婉到宋词的深婉悱恻最后步入短篇小说的幽微内心层层递进将《氓》中的千古悲歌娓娓道来。 --- ### **唐诗·淇水别** 淇水汤汤送旧人桑之落矣碾为尘。 垝垣曾望关山远车马今来涕泪新。 三岁为劳甘食苦一朝反目忍言嗔。 信誓旦旦终成幻独向西风悼此身。 --- ### **宋词·蝶恋花·秋桑叹** 犹记抱布春意逗涉过淇滨暗把终身就。桑叶沃若盈翠袖谁知鸠鸟啄心透。 黄叶西风凋碧后贫贱夫妻泪渍青衫皱。信誓如烟逐水流残阳空照秋桑瘦。 --- ### **短篇小说·桑落** 阿蘅最后看一眼那个家时院角的桑树正落下最后一片叶子。 叶子飘得很慢在空中打了个旋才不甘不愿地贴在湿漉漉的泥地上。她想起三年前就是在这棵桑树下氓用粗粝的手掌包着她的手说要用最好的布来换她最好的丝。那时他的眼睛亮得像淇水上的星光让她忘了母亲说过的话——斑鸠吃多了桑葚会醉女子沉溺于情爱会碎心。 醉的是她。碎了的也是她。 三更的织机声还响在耳畔。梭子来回穿梭像永远走不到头的日子。她的手指被丝线勒出一道道血口愈合又裂开最后结成层层叠叠的茧。氓说她织的布不够细密说她煮的粥太稀说她回娘家的次数太多。他的声音起初是失望后来是不耐最后变成了冰。 今早他把休书拍在桌上时阿蘅正端着粥。粥碗晃了晃几粒米溅在休书上洇开了“无子”两个字。她盯着那两个字看了很久久到氓的呵斥声都变成了嗡嗡的背景音。她只是在想这三年里哪个夜晚不是为了织布熬到天亮哪顿饭不是先紧着他吃哪句话不是小心着说怕惹他皱眉 可这些都不在休书上。 休书上写的是无子多言炊米太费。 阿蘅突然想笑。她想问氓你腰间那枚新换的玉钩是用哪一匹布换的你袖口掉出的那方鸳鸯锦帕又是哪家姑娘的针脚但她最终什么也没问只是弯腰捡起粥碗的碎片。碎片扎进掌心疼得清醒。 此刻她站在淇水边看着氓把她的妆奁扔上牛车。奁盒砸在青石上她母亲留下的那面铜镜滚了出来。她弯腰去捡手指刚触到镜面氓的鞭子抽在牛背上车轮碾过铜镜碾过她最后一寸念想。 “淇水还有岸隰地还有边。” 氓的声音从车上飘下来被秋风吹散。阿蘅攥着被碾出裂纹的铜镜镜中映着她的脸——二十岁的面容却像桑树落尽叶子的枝桠。 她忽然想起从前偷摘邻家桑葚的光景。氓爬树她在树下兜着衣襟接。紫红的浆果噼里啪啦落下来染得她满襟都是。氓从树上跳下来摘了颗最饱满的塞进她嘴里甜得她眯起眼睛笑。那时他说要娶她她说好。两个总角小儿把过家家当成了真的。 她竟忘了过家家是可以散的。 桑葚的甜是醉人的。斑鸠醉倒了尚能醒来。而她醉倒了醒来时已是满目荒芜。 阿蘅抬起头最后看了一眼远去的牛车。淇水汤汤向东流岸边的芦苇在秋风里摇成一片苍黄。她把铜镜放进怀里往娘家的方向走去。 走了七步停下。 又走了七步再停下。 不是因为留恋——只是因为风冷露重而她还没来得及学会如何做一个不再回头的女人。2. 通过提示词隔离的方式彻底解决前置节点越权执行前置节点越权的问题根源在于让Agent看到了超越其任务范围的提示词信息。虽然系统指令具有最强的约束能力但是具体听谁的还取决于具体的语言组织以及LLM自身的理解说白了这是两种力量之间的对抗谁胜谁负其实很难控制。要彻底就解决这个问题唯有提示词隔离一种办法也就是让每个Agent在执行时只能看到自己任务范围内的提示词而看不到其他Agent的提示词信息。为此我重新定义了基于Sequential模式编排Workflow的BuildSequential方法。staticWorkflowBuildSequential(params(AIAgentAgent,ListChatMessage?Messages)[]agentsWithInjectedMessages)如上面的代码所示我将原来的BuildSequential方法的参数类型从AIAgent列表改成了(AIAgent Agent, ListChatMessage? Messages)元组列表也就是说可以我们在每个Agent执行后为后续Agent注入一组提示词信息。我们使用此方法重写了前面的演示程序三个Agent的指令只关注自己负责的那一块并且不再添加排他性的指令文本。在调用BuildSequential方法时我们将针对宋词的创作任务注入到唐诗的Agent执行后针对短篇小说的创作任务注入到宋词的Agent执行后。原始的提示词只提到了唐诗的创作任务。usingAzure;usingdotenv.net;usingMicrosoft.Agents.AI;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字以内的短篇小说。);varworkflowBuildSequential((tangPoetryComposer,[newChatMessage(ChatRole.User,根据提供的诗歌《卫风·氓》的背景和情感基调创作一首宋词)]),(songLyricsComposer,[newChatMessage(ChatRole.User,根据提供的诗歌《卫风·氓》的背景和情感基调创作一篇短篇小说)]),(novelComposer,null));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);}}}IChatClientCreateChatClient(){varmodelEnvironment.GetEnvironmentVariable(MODEL)!;varapiKeyEnvironment.GetEnvironmentVariable(API_KEY)!;varendpointEnvironment.GetEnvironmentVariable(OPENAI_URL)!;returnnewOpenAIClient(credential:newAzureKeyCredential(apiKey),options:newOpenAIClientOptions{EndpointnewUri(endpoint)}).GetResponsesClient().AsIChatClient(defaultModelId:model);}输出--------------------TangPoetryComposer_5744b9b6cc4647e19ac4af864b062572-------------------- 《代卫风弃妇吟》 淇水汤汤送旧人复关望断几回春。 盟言犹在耳秋以为期信誓真。 三年夙夜侍蚕桑罗帷未暖君心变。 鸠食桑葚不知醉妾悔当初错认君。 --------------------SongLyricsComposer_779e58349c054b0ca3db128721799c68-------------------- 《钗头凤·淇水寒》 淇水咽秋期绝复关残月音书灭。 蚕桑歇罗帷裂三年夙夜一朝霜雪。 切切切 鸠贪蜜桑空碧总角欢愉成追忆。 盟如屑郎心铁岸泮犹在信誓虚设。 决决决 注此词以《卫风·氓》弃妇之怨为骨取陆游唐婉词牌之体。上片以“淇水咽”起兴呼应原诗“送子涉淇”之景“秋期绝”暗藏“秋以为期”之誓成空。下片“鸠贪蜜”反用“无食桑葚”之诫结句三重“决”字既合原诗“亦已焉哉”之决绝更添词体特有的顿挫呜咽之致较之陆游唐婉的和词另具《诗经》古拙风韵。 --------------------NovelComposer_01d47dcfc1e142a59136a6cbd3a6cd06-------------------- 淇水边的桑树又落叶了。 我站在岸边看着河水汤汤恍惚间又看见当年那个抱着布匹的男子。他笑得憨厚说是来买丝眼睛却一直瞟着我。那时我以为这是命中注定的姻缘傻傻地送他到顿丘又傻傻地等了一个秋天。 复关的城墙我都爬了多少回了。每回望不见他的身影就躲在垝垣后面掉眼泪。姐妹们笑话我痴我只当她们不懂。后来他来了带着占卜的吉兆我高兴得什么似的催着他赶快把车赶来把我的嫁妆都搬走。 新婚那夜他叫我“小桑叶”。他说我的脸颊像春天的桑叶一样嫩。我羞得直往他怀里钻觉得自己是天底下最幸运的女子。 三年。整整三年。 鸡叫头遍我就起身纺线织布到月上中天。他的手越来越懒脾气却越来越大。我做的饭嫌淡缝的衣裳嫌粗就连走路的声音都嫌吵。有一次他喝了酒回来抄起棍子就打说我在外头勾搭别的男人。 我去找兄弟诉苦。哥哥们正在院子里喝酒见我哭哭啼啼进来先是一愣继而哈哈大笑。 “早就叫你别嫁那个穷光蛋你偏不听。” “如今知道回来哭了” “嫁出去的女儿泼出去的水你找我们有什么用” 我站在堂屋中间像一只落汤鸡。他们继续喝酒划拳笑声震得屋顶的灰都往下掉。我退出来的时候连门槛都差点绊我一跤。 那天夜里我又来到淇水边。河水还是那样流着不急不缓就像三年前我等他娶我的时候一样。可我知道河水有岸沼泽有边我这份苦处却没个尽头。 他曾经说过要跟我白头到老的。那时候我们在河边捉蟋蟀在桑林里采桑葚他指天发誓说这辈子只对我一个人好。我当时信了信得真真的就像斑鸠贪吃桑葚一样吃得醉醺醺完全不知道醉过后是什么下场。 现在我知道了。 女人一旦陷进去就再也出不来。男人说变就变今天说爱你明天就能把你当仇人。可女人不行女人把自己整个儿都投进去了等发现所托非人时连魂都找不回来了。 河水还是汤汤地流。 我站起来拍了拍裙上的土。既然誓言都成了空那就算了吧。桑叶落了就落了还能怎么着。 我最后看了一眼这条淇水。河面上飘着一片枯黄的桑叶打着旋儿慢慢沉了下去。 就像我这三年就这么沉下去了。 回头的时候风吹过来冷得很。我把袖子拢了拢往娘家方向走。路还是那条路只是这世上再也没有“小桑叶”了。有人可能会说你这也没有完全隔离呀写宋词的Agent还是看到了唐诗的创作任务呀写小说的Agent还能同时看到了唐诗和宋词的创作任务呀。但是这反而是对的因为Sequential模式下就是需要让后续节点在前序节点的成果基础上继续执行任务虽然它看到了不属于自己任务范围的提示词信息但是它也看到前序节点已经完成了各自的任务加上系统指令的约束它知道该做什么。3. Sequential模式的消息注入重写的BuildSequential方法本质上就是在每个AIAgent节点之后添加一个额外的节点实现了消息注入的功能。对于上面构建的Workflow它具有如下的结构3.1 MessageInjectingExecutorWorkflow中用来注入消息的Executor为如下这个MessageInjectingExecutor。它继承自ChatProtocolExecutor所以可以成为采用Chat协议的数据流的一个标准的节点。它注入的消息通过构造函数提供的ListChatMessage对象来指定并在重写的TakeTurnAsync方法中将累积和注入的消息一起发送出去。publicsealedclassMessageInjectingExecutor(ListChatMessage?additionalMessages):ChatProtocolExecutor(MessageInjectingGuid.NewGuid()){protectedoverrideasyncValueTaskTakeTurnAsync(ListChatMessagemessages,IWorkflowContextcontext,bool?emitEvents,CancellationTokencancellationTokendefault){if(messages?.Any()??false){awaitcontext.SendMessageAsync(messages,cancellationToken).ConfigureAwait(false);}if(additionalMessages?.Any()??false){awaitcontext.SendMessageAsync(additionalMessages,cancellationToken).ConfigureAwait(false);}}}3.2 OutputMessagesExecutor由于用来输出ChatMessage列表的OutputMessagesExecutor是一个internal类型所以我们不得不重新定义完整的代码如下所示internalsealedclassOutputMessagesExecutor:ChatProtocolExecutor,IResettableExecutor{publicOutputMessagesExecutor(ChatProtocolExecutorOptions?optionsnull):base(OutputMessages,options,declareCrossRunShareable:true){}protectedoverrideProtocolBuilderConfigureProtocol(ProtocolBuilderprotocolBuilder)base.ConfigureProtocol(protocolBuilder).YieldsOutputListChatMessage();protectedoverrideValueTaskTakeTurnAsync(ListChatMessagemessages,IWorkflowContextcontext,bool?emitEvents,CancellationTokencancellationTokendefault)context.YieldOutputAsync(messages,cancellationToken);}3.3 BuildSequential方法如下所示的是我们自定义的BuildSequential方法的完整代码。我们在调用此方法的时候需要指定一个(AIAgent Agent, ListChatMessage? Messages)二元组列表。我们利用提供的AIAgent和随后注入的消息列表分别创建出对应的AgentHostExecutor和MessageInjectingExecutor并将它们按照顺序连接起来。最后我们在Workflow的末尾添加一个OutputMessagesExecutor来输出最终的消息列表。staticWorkflowBuildSequential(params(AIAgentAgent,ListChatMessage?Messages)[]agentsWithInjectedMessages){varoptionsnewAIAgentHostOptions{ReassignOtherAgentsAsUserstrue,ForwardIncomingMessagestrue};ListExecutorBindingagentExecutorsagentsWithInjectedMessages.Select(agentagent.Agent.BindAsExecutor(options)).ToList();ListExecutorBindingmessageInjectingExcutorsagentsWithInjectedMessages.Select(agent(ExecutorBinding)newMessageInjectingExecutor(agent.Messages)).ToList();WorkflowBuilderworkflowBuildernewWorkflowBuilder(agentExecutors[0]);for(varindex0;indexagentExecutors.Count;index){workflowBuilder.AddEdge(agentExecutors[index],messageInjectingExcutors[index]);if(indexagentExecutors.Count-1){workflowBuilder.AddEdge(messageInjectingExcutors[index],agentExecutors[index1]);}}varoutputMessagesExecutornewOutputMessagesExecutor();workflowBuilder.AddEdge(messageInjectingExcutors.Last(),outputMessagesExecutor).WithOutputFrom(outputMessagesExecutor);returnworkflowBuilder.Build();}