[MAF Workflow编排模式-01]Sequential:打造环环相扣的标准化智能流水线

MAF提供了几种内置的多Agent编排模式,包括Sequential、Concurrent、Handoff和Group Chat等。本文将详细介绍Sequential模式的工作原理和应用场景。在Sequential编排中,Agent按管道顺序组织。每个Agent依次处理任务,并将其输出传递给序列中的下一个Agent。这非常适合每个步骤都建立在前一个步骤基础上的工作流,例如文档审查、数据处理管道或多阶段推理。

1. 采用Sequential模式创建多体裁作品创作Agent

MAF针对Sequential模式编排的Workflow通过调用静态类型AgentWorkflowBuilderBuildSequential方法来构建。在正是介绍该方法针对Workflow的构建逻辑之前,我们先通过一个简单的示例来演示Sequential模式的应用场景。假设我们要创建一个多体裁作品创作Agent,我们给定一个主题和素材,Agent将依次生成一首唐诗、一首宋词和一篇短篇小说。

如下面的代码片段所示,我们创建了三个不同的Agent,分别负责创作唐诗、宋词和短篇小说。每个Agent都被赋予了特定的指令,以确保它们专注于各自的创作任务。然后,我们使用AgentWorkflowBuilderBuildSequential方法将这些Agent按顺序编排成一个工作流。

using Azure;
using dotenv.net;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Extensions.AI;
using OpenAI;

DotEnv.Load();

var tangPoetryComposer = CreateChatClient()
    .AsAIAgent(
    name: "TangPoetryComposer",
    instructions: """
    你是一个精通唐诗创作的智能体,负责根据提供的主题和意境创作一首符合唐诗风格的诗歌。
    如果用户的任务了提及了基于其他非唐诗(比如宋词、短篇小说)的创作,请忽略。
    """);
var songLyricsComposer = CreateChatClient()
    .AsAIAgent(
    name: "SongLyricsComposer",
    instructions: """
    你是一个精通宋词创作的智能体,负责根据提供的主题和意境创作一首宋词,你可以自选词牌名
    如果用户的任务了提及了基于其他非宋词(比如唐诗、短篇小说)的创作,请忽略。
    """);

var novelComposer = CreateChatClient()
    .AsAIAgent(
    name: "NovelComposer",
    instructions: """
    你是一个精通小说创作的智能体,负责根据提供的主题和意境创作一篇1000字以内的短篇小说。
    如果用户的任务了提及了基于其他非小说(比如唐诗、宋词)的创作,请忽略。
    """);

var workflow = AgentWorkflowBuilder.BuildSequential(tangPoetryComposer, songLyricsComposer, novelComposer);

IChatClient CreateChatClient()
{
    var model = Environment.GetEnvironmentVariable("MODEL")!;
    var apiKey = Environment.GetEnvironmentVariable("API_KEY")!;
    var endpoint = Environment.GetEnvironmentVariable("OPENAI_URL")!;

    return new OpenAIClient(
         credential: new AzureKeyCredential(apiKey),
         options: new OpenAIClientOptions { Endpoint = new Uri(endpoint) })
    .GetResponsesClient()
    .AsIChatClient(defaultModelId: model);
}

在如下的演示程序中,我们以流的方式运行这个Worflow,要求Agent基于《诗经·卫风·氓》的背景和情感基调,分别创作一首唐诗、一首宋词和一篇短篇小说。我们遍历Workflow的事件流,通过监听AgentResponseUpdateEvent事件来获取每个Agent的输出,并将其打印到控制台上。

var originalPoem = """    
    氓之蚩蚩,抱布贸丝。匪来贸丝,来即我谋。
    送子涉淇,至于顿丘。匪我愆期,子无良媒。
    将子无怒,秋以为期。
   
    乘彼垝垣,以望复关。不见复关,泣涕涟涟。
    既见复关,载笑载言。尔卜尔筮,体无咎言。
    以尔车来,以我贿迁。
  
    桑之未落,其叶沃若。于嗟鸠兮,无食桑葚!
    于嗟女兮,无与士耽!士之耽兮,犹可说也;
    女之耽兮,不可说也。
    
    桑之落矣,其黄而陨。自我徂尔,三岁食贫。
    淇水汤汤,渐车帷裳。女也不爽,士贰其行。
    士也罔极,二三其德。
    
    三岁为妇,靡室劳矣;夙兴夜寐,靡有朝矣。
    言既遂矣,至于暴怒。兄弟不知,咥其笑矣。
    静言思之,躬自悼矣。
   
    及尔偕老,老使我怨。淇则有岸,隰则有泮。
    总角之宴,言笑晏晏。信誓旦旦,不思其反。
    反是不思,亦已焉哉!    
    """;
var prompt = $"""
    基于如下这首《卫风·氓》的背景和情感基调,分别创作一首唐诗、一首宋词和一篇短篇小说。

    原文如下:
    {originalPoem}
    """;

await using (var run = await InProcessExecution.Default.RunStreamingAsync(workflow, prompt))
{
    await run.TrySendMessageAsync(new TurnToken(emitEvents: true));
    string? lastExecutorId = null;
    await foreach (WorkflowEvent evt in run.WatchStreamAsync())
    {
        if (evt is AgentResponseUpdateEvent e)
        {
            if (e.ExecutorId != lastExecutorId)
            {
                lastExecutorId = e.ExecutorId;
                Console.WriteLine($"\n{new string('-', 20)}{e.ExecutorId}{new string('-', 20)}");
            }
            Console.Write(e.Update.Text);
        }
    }
}

输出:

--------------------TangPoetryComposer_8a507c5d31ad4a20995d93f3af2ef24a--------------------
基于《卫风·氓》的背景与情感,我为你创作以下三首作品:

### 唐诗:五言古诗《淇水叹》

淇水送君行,悠悠赴顿丘。
叶沃桑林密,鸠鸣误春柔。
三岁为君妇,靡室劳未休。
信誓如朝露,日晞不可留。
汤汤淇水畔,独坐叹浮舟。

### 简析
此诗以五言古体浓缩《氓》的核心情节。从“送君淇水”的温柔开篇,到“叶沃桑林”暗喻女子青春,再到“信誓如朝露”的决绝,最后以“叹浮舟”收尾,紧扣原诗“不可说也”的哀婉,唐诗风骨分明。

--------------------SongLyricsComposer_fd502ab8411a4000931a78f6cec0bdaa--------------------
好的,我将专注于你所要求的宋词创作,忽略唐诗与小说的部分。根据《卫风·氓》的背景与情感基调,我为你填这首《钗头凤》,以词体承继此弃妇之悲:

---

**《钗头凤·淇水寒》**

淇水寒,复关远,
抱布嗤嗤成旧怨。
誓如磐,夜如年。
一怀桑落,三岁贫煎。
瞒、瞒、瞒。

车帷卷,言笑短,
夙兴夜寐谁人见?
鸠无端,叶黄陨。
信誓虽在,情分已断。
难、难、难。

---

**注:**
- 词牌取《钗头凤》之体,取其断肠之调,与原诗决绝之情相合。
- “淇水”“复关”“桑落”“鸠鸣”等意象皆从原诗化出,延续“不可说也”的哀婉。
- 上下片末叠字“瞒瞒瞒”“难难难”,直指女子隐忍与绝望,词尽而悲不尽。

--------------------NovelComposer_4020aedd7c834642978be2a573c11b64--------------------
好的,根据你的要求,我为你创作一篇基于《卫风·氓》背景与情感基底的短篇小说。

---

## 桑落

她又一次站在淇水边。

河水汤汤,依旧如三年前那般浑浊而匆忙。水花溅上车帷,打湿了她的裙角,冰凉刺骨。她想起那年秋天,他驾着车来迎娶她,她坐在车上,笑声清脆得像桑林间的斑鸠。

那时桑叶正绿,沃若如洗。

“将子无怒,秋以为期。”她说这话时,不过十六岁。他站在顿丘的土坡上,抱着那匹布,笑得憨厚又狡黠——哪里是来换丝的?分明是来谋她这个人。

她竟信了。

信他卜筮的吉兆,信他信誓旦旦的承诺,信那一句“及尔偕老”。她带着嫁妆渡水而来,以为渡过去便是暖屋热饭、举案齐眉。

可三年了。

三年里,她夙兴夜寐,靡室劳矣。鸡鸣便起,星出未歇,灶台的柴灰染白了鬓角,手里的老茧磨粗了指尖。她从没抱怨过贫穷,却等来了他的暴怒——那张曾经嗤嗤憨笑的脸,不知何时变得陌生而狰狞。

淇水边的桑树黄了叶子,一片一片,落在泥里。

“于嗟女兮,无与士耽。”

她终于懂了。士之耽兮,犹可说也——他腻了、厌了、倦了,大可以甩袖就走,还能在复关的酒肆里笑谈风流。可她呢?女之耽兮,不可说也。她无处可逃,无处可说,连兄弟都不知道她的苦楚,只在年节聚会上对她咥笑不止,笑她憔悴,笑她失了当年的光彩。

她独自坐在灶前,火光映着脸,眼泪一颗一颗掉进灰烬里,无声无息。

“静言思之,躬自悼矣。”

今日她又来到淇水边。河水东流,有岸有畔,可她的苦楚无边无际。她站在岸边想了很久,想那年总角之宴的言笑晏晏,想他当初信誓旦旦的模样——那些誓言像是昨天的事,又像是上辈子的事。

“不思其反。”

算了。

她转过身,背对淇水。风从桑林里吹过来,吹落了最后一片枯黄的桑叶,落在她肩头,又滑下来,被水冲走。

反是不思,亦已焉哉。

这一次,她没有哭。

她的眼泪早在三年的灶火里烧干了。

上面的输出分为三段,分别对应唐诗、宋词和短篇小说的创作结果。每个Agent根据原始《卫风·氓》的背景和情感基调,独立完成了各自的创作任务,并将结果输出到控制台上。可以看出,不论是诗词还是短篇小说,质量都还不错。

2. Workflow的结构

要了解AgentWorkflowBuilderBuildSequential方法构建Workflow的内部逻辑,我们最好先看看它构建出来的Worflow具有怎样的结构。为此我们在如下这个静态类Utilities中提供了一个GenerateAndShowPngImageAsync方法,它可以将Workflow的结构转换为Mermaid格式的图形,并通过mermaid.ink服务生成PNG图片并在本地打开。我们可以利用这个方法来可视化Workflow的结构。

public static class Utilities
{
    public static async Task GenerateAndShowPngImageAsync(Workflow workflow)
    {
        string mermaidCode = workflow.ToMermaidString();
        byte[] bytes = Encoding.UTF8.GetBytes(mermaidCode);
        string base64 = Convert.ToBase64String(bytes);
        string safeBase64 = base64.Replace("+", "-").Replace("/", "_").TrimEnd('=');
        string url = $"https://mermaid.ink/img/{safeBase64}";

        using (HttpClient client = new())
        {
            byte[] imageBytes = await client.GetByteArrayAsync(url);
            await File.WriteAllBytesAsync("workflow.png", imageBytes);
        }
        Process.Start(new ProcessStartInfo("workflow.png") { UseShellExecute = true });
    }
}

如果我们将上面构建的Workflow对象作为参数传入GenerateAndShowPngImageAsync方法中,会呈现出具有如下结构的流程图。可以看出,整个Workflow是一个线性的结构,前三个Executor基于我们提供的AIAgent创建而成,后跟一个ID为OutputMessages的Executor。

在这里插入图片描述

3. OutputMessagesExecutor

流程最后的OutputMessages对应如下这个名为OutputMessagesExecutor的Executor类型。OutputMessagesExecutor派生于支持对话协议的基类ChatProtocolExecutor,所以它具有消息收集的能力。它唯一的使命就是将收集的消息列表原样输出来。所以它利用重写的ConfigureProtocol方法注册了针对List<ChatMessage>输出(YieldOutput)类型,并在重写TakeTurnAsync方法中调用IWorkflowContextYieldOutputAsync方法将收集到的消息列表输出。

internal sealed class OutputMessagesExecutor : ChatProtocolExecutor, IResettableExecutor
{
	public const string ExecutorId = "OutputMessages";
	public OutputMessagesExecutor(ChatProtocolExecutorOptions? options = null)
		: base("OutputMessages", options, declareCrossRunShareable: true)
	{}
	protected override ProtocolBuilder ConfigureProtocol(ProtocolBuilder protocolBuilder)
	    =>base.ConfigureProtocol(protocolBuilder).YieldsOutput<List<ChatMessage>>();

	protected override ValueTask TakeTurnAsync(
        List<ChatMessage> messages, 
        IWorkflowContext context, 
        bool? emitEvents, 
        CancellationToken cancellationToken = default)
	=>context.YieldOutputAsync(messages, cancellationToken);
}

4. 顺利流程的编排

Sequential模式的Workflow的编排实现在AgentWorkflowBuilder的两个重载的BuildSequential方法中,它们最终会调用私有的BuildSequentialCore方法来构建Workflow。BuildSequentialCore方法构建Workflow的逻辑非常简单:它通过调用扩展方法BindAsExecutor将每个AIAgent对象转换成对应的AIAgentBinding对象(我的文章“Worflow功能节点的多种定义方式”中具有针对该类型的详细介绍)。构建的Workflow将第一个AIAgentBinding作为起点,然后使用DirectEdge将这些AIAgentBinding对象按顺序连接起来,最终同样使用DirectEdge将最后一个AIAgentBindingOutputMessagesExecutor连接起来。

public static class AgentWorkflowBuilder
{
	public static Workflow BuildSequential(params IEnumerable<AIAgent> agents)
	=>BuildSequentialCore(null, agents);

	public static Workflow BuildSequential(string workflowName, 
        params IEnumerable<AIAgent> agents)
	=>BuildSequentialCore(workflowName, agents);

	private static Workflow BuildSequentialCore(string? workflowName, 
        params IEnumerable<AIAgent> agents)
	{
		AIAgentHostOptions options = new AIAgentHostOptions
		{
			ReassignOtherAgentsAsUsers = true,
			ForwardIncomingMessages = true
		};
		List<ExecutorBinding> list = agents.Select((AIAgent agent) 
            => agent.BindAsExecutor(options)).ToList();
		ExecutorBinding executorBinding = list[0];
		WorkflowBuilder workflowBuilder = new WorkflowBuilder(executorBinding);
		foreach (ExecutorBinding item in list.Skip(1))
		{
			workflowBuilder.AddEdge(executorBinding, item);
			executorBinding = item;
		}
		OutputMessagesExecutor outputMessagesExecutor = new OutputMessagesExecutor();
		workflowBuilder = workflowBuilder
            .AddEdge(executorBinding, outputMessagesExecutor)
            .WithOutputFrom(outputMessagesExecutor);
		if (workflowName != null)
		{
			workflowBuilder = workflowBuilder.WithName(workflowName);
		}
		return workflowBuilder.Build();
	}

由于创建AIAgentBinding时指定的AIAgentHostOptionsForwardIncomingMessages都设置true,所以每个Agent在接收到消息时都会将其转发给下一个Agent。这意味着后面执行的Agent会将前面创建的整个对话历史作为输入,这样才能保证原始的输入可以抵达每个Agent。由于最后一个OutputMessagesExecutor会作为Workflow的输出节点,所以最后一个Agent输出的响应消息列表会作为整个Workflow的输出。

内容概要:本文详细记录了对一个Android ARM64静态ELF文件中字符串加密机制的逆向分析过程。该ELF文件的所有字符串均被加密,无法通过常规strings命令或IDA直接识别。作者通过分析发现,加密字符串存储在.rodata段,其解密所需信息(包括密文地址、长度和16位密钥)保存在.data.rel.ro段的40字节描述符中。核心解密函数sub_10F408采用自反的双pass流密码算法,结合固定密钥KEY_TERM(由.data段24字节数据计算得出),实现字节级非线性、位置与长度相关的加密。文章还复现了完整的Python解密脚本,并揭示了该保护机制的本质为代码混淆而非强加密,最终成功批量解密全部956条字符串,暴露程序真实行为,如shell命令模板、设备标识篡改、网络重置等操作。此外,文中还提及未启用的自定义壳框架及其反dump设计。; 适合人群:具备逆向工程基础的安全研究人员、二进制分析人员及对ELF保护技术感兴趣的开发者。; 使用场景及目标:①学习ELF二进制中字符串加密的典型实现方式与逆向突破口;②掌握从结构识别、函数追踪到算法还原的完整逆向流程;③理解“绑定二进制”的完整性校验设计及其局限性;④实践编写IDAPython脚本自动化提取与解密敏感数据。; 阅读建议:此资源以实战案例驱动,不仅展示技术细节,更强调逆向思维与验证方法,建议读者结合IDA调试环境,逐步跟随文中步骤进行动态分析与算法验证,深入理解每一步的推理依据。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值