MAF提供了几种内置的多Agent编排模式,包括Sequential、Concurrent、Handoff和Group Chat等。本文将详细介绍Sequential模式的工作原理和应用场景。在Sequential编排中,Agent按管道顺序组织。每个Agent依次处理任务,并将其输出传递给序列中的下一个Agent。这非常适合每个步骤都建立在前一个步骤基础上的工作流,例如文档审查、数据处理管道或多阶段推理。
1. 采用Sequential模式创建多体裁作品创作Agent
MAF针对Sequential模式编排的Workflow通过调用静态类型AgentWorkflowBuilder的BuildSequential方法来构建。在正是介绍该方法针对Workflow的构建逻辑之前,我们先通过一个简单的示例来演示Sequential模式的应用场景。假设我们要创建一个多体裁作品创作Agent,我们给定一个主题和素材,Agent将依次生成一首唐诗、一首宋词和一篇短篇小说。
如下面的代码片段所示,我们创建了三个不同的Agent,分别负责创作唐诗、宋词和短篇小说。每个Agent都被赋予了特定的指令,以确保它们专注于各自的创作任务。然后,我们使用AgentWorkflowBuilder的BuildSequential方法将这些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的结构
要了解AgentWorkflowBuilder的BuildSequential方法构建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方法中调用IWorkflowContext的YieldOutputAsync方法将收集到的消息列表输出。
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将最后一个AIAgentBinding和OutputMessagesExecutor连接起来。
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时指定的AIAgentHostOptions将ForwardIncomingMessages都设置true,所以每个Agent在接收到消息时都会将其转发给下一个Agent。这意味着后面执行的Agent会将前面创建的整个对话历史作为输入,这样才能保证原始的输入可以抵达每个Agent。由于最后一个OutputMessagesExecutor会作为Workflow的输出节点,所以最后一个Agent输出的响应消息列表会作为整个Workflow的输出。


被折叠的 条评论
为什么被折叠?



