2010年Google发表一篇名为“Pregel: a system for large-scale graph processing”首次提出了一个专为大规模分布式图计算设计的计算框架,解决了传统MapReduce在处理迭代式图算法时效率低下的问题。LangChain和MAF的设计者都从中汲取了灵感,但是他们对这篇论文的利用不尽相同,LangChain采用论文提供的思想打造了LangGraph,确切地说应该是基于Pregel的执行引擎,成为了整个LangChain平台的基石,MAF则将它应用在Workflow上。虽然两者系出同宗,但是在设计和实现方式上有着明显的差异。在“拆解LangChain执行引擎(18篇)”,我详细剖析了LangGraph的设计和实现,在这个系列我们采用类似的形式来剖析MAF的Workflow框架。作为开篇,我们先从一个具体的实例来体验一下MAF工作流的基本编程模式。
1. 构建由Executor + Edge组成的工作流
不论是LangChain的StateGraph还是MAF的Workflow,都是由节点和边组成的有向有环图(DCG:Directed Cyclic Graph),它们由有向边连接功能节点形成数据流。StateGraph中的功能节点被称为Node,而Workflow中的功能节点被称为Executor。我们的演示程序会利用Workflow创建一个用于翻译古典诗词的Agent,并采用类似于GAN的方式来构建这个Workflow:创建一个Translator对指定的古诗词进行翻译,并将译文交给另一个Evaluator评估打分。如果翻译的分数不够高,我们会将评估的结果反馈给Translator进行优化,由此形成了一个循环。当翻译通过评估后,跳出这个循环,Workflow结束。整个流程如下所示:
1.1 定义Executor
我们为Evaluator定义了如下两个类型作为其输入和输出
public record EvaluationInput
{
[Description("原文")]
public string Original { get; set; } = default!;
[Description("译文")]
public string Translation { get; set; } = default!;
}
[Description("诗歌翻译评估结果")]
public record EvaluationOutput
{
[Description("得分")]
public int Score { get; set; }
[Description("评估理由")]
public string Reason { get; set; } = default!;
}
如下所示的是Translator这个Executor类型的定义。我们让它继承自Executor这个抽象类,并将其ID设置为Translator。我们在构造函数中提供了用来执行翻译的AIAgent对象。由于具体的翻译工作具有两种场景:一是初始翻译,二是根据评估结果进行优化翻译,所以我们在TranslatorExecutor中定义了两个重载的TranslateAsync方法。基类Executor是一个抽象类,其中定义了用于配置Executor执行协议的抽象方法,所以我们需要在两个TranslateAsync方法上标注MessageHandler特性,它会借助Source Generator自动完成针对抽象方法的实现。这也是我们需要将TranslatorExecutor定义为partial类的原因。
internal partial class TranslationExecutor(AIAgent agent) : Executor(id:"Translator")
{
[MessageHandler(Send = [typeof(EvaluationInput)], Yield = [typeof(string)])]
public async ValueTask TranslateAsync(string poem, IWorkflowContext context)
{
await context.QueueStateUpdateAsync("OriginalText", poem);
var prompt = $"请将以下诗歌翻译成英文:{poem}";
var messages = new List<AIChatMessage>{ new(ChatRole.User, prompt) };
var response = await agent.RunAsync(messages);
messages.AddRange(response.Messages);
await context.QueueStateUpdateAsync("ChatHistory", messages);
var translation = response.ToString();
await context.YieldOutputAsync(translation);
await context.SendMessageAsync(new EvaluationInput { Original = poem, Translation = translation });
}
[MessageHandler(Send = [typeof(EvaluationInput)], Yield = [typeof(string)])]
public async ValueTask TranslateAsync(EvaluationOutput evaluationResult, IWorkflowContext context)
{
var messages = await context.ReadStateAsync<List<AIChatMessage>>("ChatHistory")
?? throw new InvalidOperationException("没有找到翻译的历史记录。");
var prompt = $"""
请根据以下评估结果对翻译做进一步修正:
{evaluationResult.Reason}
""";
messages.Add(new AIChatMessage(ChatRole.User, prompt));
var response = await agent.RunAsync(messages);
messages.AddRange(response.Messages);
await context.QueueStateUpdateAsync("ChatHistory", messages);
var translation = response.ToString();
await context.YieldOutputAsync(translation);
var original = await context.ReadStateAsync<string>("OriginalText");
await context.SendMessageAsync(new EvaluationInput { Original = original!, Translation = translation });
}
}
由于Executor严格按照配置的协议执行,这些协议包括消息路由和对输出对象类型的严格约束。由于TranslateAsync方法的返回类型为ValueTask,并没有体现Send和Yield类型(前者表示发送给下游节点的数据类型,后者表示面向调用者的输出类型),所以我们利用MessageHandlerAttribute的Send和Yield属性来声明缺失的输出类型。由于下游节点为用于评估的EvaluatorExecutor,所以Send属性被设置为EvaluationInput类型,而我们希望用户直接看到翻译的文本,所以Yield属性被设置为string类型。
在第一个TranslateAsync方法中,我们利用指定的诗歌原文构建提示词并调用Agent来进行翻译,并将翻译的结果作为IWorkflowContext的YieldOutputAsync方法的参数进行输出,调用另一个SendMessageAsync方法则是为了将EvaluationInput发送给EvaluatorExecutor。为了让后续的翻译能够从之前的翻译历史中获益,我们调用了IWorkflowContext的QueueStateUpdateAsync方法来将原文和翻译历史保存到Workflow的状态中。
在第二个TranslateAsync方法中,上游节点发送过来的表示评估的结果的EvaluationOutput对象,,我们首先调用IWorkflowContext的ReadStateAsync方法来读取之前保存的翻译历史记录,然后根据评估结果创建了一个新的User消息并添加到消息列表中,最后将消息列表作为输入调用Agent来进行优化翻译。这样做的目的很明确:我们希望Agent能够根据之前的翻译历史和评估结果来优化翻译的结果。最后我们同样将优化后的翻译结果通过YieldOutputAsync方法输出,并将新的EvaluationInput类型的消息发送给EvaluatorExecutor。
EvaluationExecutor采用用类似的方式来定义。它同样继承自Executor,并将ID设置为Evaluator。用于评估的EvaluateAsync方法接受一个EvaluationInput类型的参数,并返回一个EvaluationOutput类型的结果,所以标注的MessageHandlerAttribute无需要设置Send和Yield属性,因为框架会自动推断出输入输出类型。我们采用构建提示词的方式来让Agent进行评估,并采用结构化输出将评估的结果作为EvaluateAsync方法的返回值。
internal partial class EvaluationExecutor(AIAgent agent) : Executor("Evaluator")
{
[MessageHandler]
public async ValueTask<EvaluationOutput> EvaluateAsync(EvaluationInput input, IWorkflowContext context)
{
var prompt = $"""
请评估以下英文翻译是否满足要求.
**原诗**
{input.Original}
**翻译**
{input.Translation}
""";
var messages = new List<AIChatMessage> { new(ChatRole.User, prompt) };
var response = await agent.RunAsync<EvaluationOutput>(messages);
return response.Result;
}
}
1.2 编排工作流
有了上述定义的两个Executor,我们就可以利用WorkflowBuilder将它们编排成一个Workflow了。为了避免左右大脑互博,我们为Translator和Evaluator分别创建了两个Agent对象,并且使用了不同的模型(gpt-5.2-chat和DeepSeek-V4-Pro),这样的对抗更加有效。
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Extensions.AI;
using OpenAI;
using OpenAI.Chat;
using System.ClientModel;
using System.ComponentModel;
using AIChatMessage = Microsoft.Extensions.AI.ChatMessage;
dotenv.net.DotEnv.Load();
var endpoint = Environment.GetEnvironmentVariable("OPENAI_URL")!;
var apiKey = Environment.GetEnvironmentVariable("API_KEY")!;
var translationAgent = new OpenAIClient(
new ApiKeyCredential(apiKey),
new OpenAIClientOptions { Endpoint = new Uri(endpoint) })
.GetChatClient(model: "gpt-5.2-chat")
.AsAIAgent();
var evaluationAgent = new OpenAIClient(
new ApiKeyCredential(apiKey),
new OpenAIClientOptions { Endpoint = new Uri(endpoint) })
.GetChatClient(model: "DeepSeek-V4-Pro")
.AsAIAgent(instructions: """
你是一位资深的诗歌翻译评审专家,专门评估诗歌翻译的质量。评估标准如下:
- 韵律保持:翻译后的诗歌是否保持了原诗的韵律和节奏感。
- 意境传达:翻译后的诗歌是否成功传达了原诗的意境和情感。
- 准确表达:翻译后的诗歌是否准确表达了原诗的意思和内容。
请根据以上标准对翻译进行评估,满分10分,并给出分数和评估理由(尽量提供不足之处以利于改进)。
"""
);
var translator = new TranslationExecutor(translationAgent);
var evaluator = new EvaluationExecutor(evaluationAgent);
var workflow = new WorkflowBuilder(translator)
.AddEdge(translator, evaluator)
.AddEdge<EvaluationOutput>(evaluator, translator, condition: result =>(result?.Score ?? 0) < 9, label: "score < 9")
.WithOutputFrom(translator)
.WithOutputFrom(evaluator)
.Build();
我们针对两个AIAgent创建了对应的TranslatorExecutor和EvaluatorExecutor对象,然后将前者作为起始节点创建了WorkflowBuilder对象。为了实现翻译-评估循环,我们调用了AddEdge方法为两个节点添加了两条边:
- 第一条边连接
TranslatorExecutor和EvaluatorExecutor,这条边没有设置条件,所以每次TranslatorExecutor输出翻译结果后都会触发EvaluatorExecutor进行评估; - 第二条边连接
EvaluatorExecutor和TranslatorExecutor,这条边设置了条件,只有当评估结果的分数小于9分时才会触发TranslatorExecutor进行优化翻译。意味着当翻译结果的评估分数达到9分或以上时,Workflow就会跳出这个循环,进入结束流程。
我们同时将TranslatorExecutor和EvaluatorExecutor设置为Workflow的输出节点,这样在Workflow执行结束后我们就可以同时获取到翻译结果和评估结果。最终调用Build方法返回的就是构建好的Workflow对象。
1.3 查看工作流程
Workflow不仅仅是交给Runner执行的对象,而且还可以转换成可视化的流程图来帮助我们理解和分析Workflow的执行流程,这一点与LangChain的StateGraph非常类似。我们定义了如下这个辅助方法将指定的Workflow对象转换成.png图片,并直接将它呈现出来。
public static class Utilities
{
public static async Task GenerateAndShowPngImageAsync(Workflow workflow, string outputPath)
{
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(outputPath, imageBytes);
}
Process.Start(new ProcessStartInfo(outputPath) { UseShellExecute = true });
}
}
如上面的代码片段所示,我们首先调用Workflow对象的ToMermaidString方法来获取Workflow的Mermaid代码表示。我们将这个字符串转换成Base64编码,并将其提交给Mermaid在线渲染服务生成图片内容的字节流。在将字节内容保存为.png文件后,我们调用Process.Start方法来打开这个图片文件:
2. 运行工作流
LangGraph的StateGraph编译后会生成一个CompiledStateGraph对象,这是一个可以直接执行的Runnable对象。MAF的Workflow则不同,Workflow对象不具有自我执行的能力,需要借助一个Runner来提供执行的环境,目前的Runner只有InProcessExecutionEnvironment这个实现。
public sealed class InProcessExecutionEnvironment : IWorkflowExecutionEnvironment
{
public async ValueTask<Run> RunAsync<TInput>(
Workflow workflow,
TInput input,
string? sessionId = null,
CancellationToken cancellationToken = default);
public async ValueTask<StreamingRun> OpenStreamingAsync(
Workflow workflow,
string? sessionId = null,
CancellationToken cancellationToken = default);
public async ValueTask<StreamingRun> RunStreamingAsync<TInput>(
Workflow workflow,
TInput input,
string? sessionId = null,
CancellationToken cancellationToken = default);
public InProcessExecutionEnvironment WithCheckpointing(CheckpointManager? checkpointManager);
public bool IsCheckpointingEnabled => this.CheckpointManager != null;
public async ValueTask<StreamingRun> ResumeStreamingAsync(
Workflow workflow,
CheckpointInfo fromCheckpoint,
CancellationToken cancellationToken = default);
public async ValueTask<Run> ResumeAsync(
Workflow workflow,
CheckpointInfo fromCheckpoint,
CancellationToken cancellationToken = default);
}
InProcessExecutionEnvironment利用RunAsync<TInput>、OpenStreamingAsync和RunStreamingAsync<TInput>方法提供两种不同的执行模式:
- 阻塞式执行:调用
RunAsync<TInput>方法来执行Workflow,这个方法会一直阻塞直到Workflow执行完成。它会返回一个Run对象,代表了这次执行的结果; - 流式执行:调用
OpenStreamingAsync和RunStreamingAsync<TInput>方法来执行Workflow,这个方法会立即返回一个StreamingRun对象,这是一个异步迭代器,我们可以通过迭代这个对象来获取Workflow执行过程中产生的实时事件;
和LangGraph的StateGraph一样,Workflow也支持基于Checkpointing的持久化机制,与此相关的方法包括:
- WithCheckpointing方法:用于启用Checkpointing功能,并指定一个
CheckpointManager对象来管理Checkpoint的存储和恢复; - IsCheckpointingEnabled属性:用于检查当前的
InProcessExecutionEnvironment是否启用了Checkpointing功能; - ResumeStreamingAsync和ResumeAsync方法:用于从指定的
CheckpointInfo对象恢复Workflow的执行,这些方法会返回一个StreamingRun对象或Run对象,代表了恢复后的执行结果;
我们可以通过静态类型InProcessExecution相应的属性来获取InProcessExecutionEnvironment的不同实例来执行Workflow。
public static class InProcessExecution
{
public static InProcessExecutionEnvironment Default => OffThread;
public static InProcessExecutionEnvironment OffThread { get; }
public static InProcessExecutionEnvironment Concurrent { get; }
public static InProcessExecutionEnvironment Lockstep { get; }
}
三种类型的InProcessExecutionEnvironment分别适用于不同的执行场景:
- OffThread:Workflow在后台线程执行,事件实时流式推送给调用者,不阻塞调用线程。适合 ReAct、循环、工具调用、Agent 推理适用于大多数常规的工作流执行场景,提供了良好的性能和资源利用率;
- Concurrent:适用于需要高并发执行的工作流场景,允许多个工作流实例同时运行,适合处理大量独立的工作流任务;
- Lockstep:Workflow在事件监听线程执行(不是后台线程)每一步的事件会先累积,整个流程执行完成后再一次性推送事件,不实时流式输出。适用于需要严格顺序执行的工作流场景,确保工作流中的每个步骤都按照定义的顺序执行,适合处理有严格依赖关系的工作流任务;
2.1 流式执行
由于流式执行能够实施得到Workflow输出的结果,具有更好的用户体验,所以我们先来演示这种执行方式。当我们调用OpenStreamingAsync和RunStreamingAsync<TInput>方法以流式方式执行Workflow时,返回的StreamingRun对象提供了一系列属性和方法来获取Workflow执行过程中的状态和事件,以及与Workflow进行交互。
public sealed class StreamingRun : CheckpointableRunBase, IAsyncDisposable
{
public string SessionId {get;}
public ValueTask<RunStatus> GetStatusAsync(CancellationToken cancellationToken = default);
public ValueTask SendResponseAsync(ExternalResponse response);
public ValueTask<bool> TrySendMessageAsync<TMessage>(TMessage message);
public IAsyncEnumerable<WorkflowEvent> WatchStreamAsync(CancellationToken cancellationToken = default);
public ValueTask CancelRunAsync();
public ValueTask DisposeAsync() ;
}
public enum RunStatus
{
NotStarted,
Idle,
PendingRequests,
Ended,
Running
}
StreamingRun提供的属性和方法说明如下:
- SessionId属性:表示当前
Workflow执行的唯一标识符,可以用于跟踪和管理Workflow的执行; - GetStatusAsync方法:用于获取当前
Workflow执行的状态,返回一个RunStatus枚举值,表示Workflow当前的执行状态; - SendResponseAsync方法:当
Workflow正在等待输入时,用它把下一条必须处理的消息送进去,强制触发下一轮推理。主要用于聊天UI、HITL和用户回复的用户输入; TrySendMessageAsync<TMessage>方法:用于系统自动产生的消息(工具结果、内部事件、自动流程),如果Workflow正好能接收就送进去,否则直接返回false,不抛异常。用于工具结果、自动重试、后台事件、Agent内部消息为主的系统输入;- WatchStreamAsync方法:用于获取一个异步枚举器,通过迭代这个枚举器可以获取
Workflow执行过程中产生的实时事件,这些事件包括Executor的执行状态变化、输出结果、错误信息等; - CancelRunAsync方法:用于取消当前
Workflow的执行,如果Workflow正在运行或者处于待处理请求的状态,调用这个方法会尝试取消Workflow的执行; - DisposeAsync方法:用于释放
StreamingRun对象占用的资源,当Workflow执行完成或者不再需要时,应该调用这个方法来进行清理;
回到前面演示的例子。我们调用InProcessExecution.Default.RunStreamingAsync方法来以流式方式执行Workflow,并传入一个古诗作为输入。这个方法会立即返回一个StreamingRun对象,我们可以通过迭代这个对象来获取Workflow执行过程中产生的事件。当Workflow中的TranslatorExecutor和EvaluatorExecutor执行完成后,它们的输出结果会被封装成WorkflowOutputEvent事件推送到事件流中,我们可以通过判断事件类型来获取翻译结果和评估结果,并将它们打印出来。
var run = await InProcessExecution.Default.RunStreamingAsync(workflow, """
千江同一月,万户尽皆春。
千江有水千江月,万户无云万里天。
""");
await foreach(var evt in run.WatchStreamAsync())
{
if(evt is WorkflowOutputEvent outputEvent)
{
if (outputEvent.ExecutorId == translator.Id)
{
var translation = outputEvent.Data;
Console.WriteLine($"""
译文:
{outputEvent.Data}
""");
}
if(outputEvent.ExecutorId == evaluator.Id) {
var evaluationResult = outputEvent.Data as EvaluationOutput;
Console.WriteLine($"""
得分:{evaluationResult?.Score}
评语:
{evaluationResult?.Reason}
{new string('-', 100)}
""");
}
}
}
输出:
译文:
Here is an elegant English rendering of the poem:
> Over a thousand rivers shines the selfsame moon;
> In ten thousand homes, it is wholly spring.
>
> Where a thousand rivers flow, a thousand moons appear;
> In ten thousand homes without clouds, stretches ten thousand miles of sky.
If you would like, I can also provide a more literal translation or a more lyrical adaptation.
得分:8
评语:
翻译准确传达了原诗的意思与意境,如‘千江同一月’译为‘Over a thousand rivers shines the selfsame moon’体现了月影千江的禅意,‘万户尽皆春’译为‘In ten thousand homes, it is wholly spring’完整保留了万物春意的意象。整体用词清晰、对仗工整,基本再现了节奏感。不足之处在于原诗‘千江有水千江月’是平仄和叠词的节奏,译文‘Where a thousand rivers flow, a thousand moons appear’在韵律上略有流失,未能完全再现原文回环叠唱的音韵美感,且‘stretches ten thousand miles of sky’在表达‘万里天’时稍显冗长。
----------------------------------------------------------------------------------------------------
译文:
根据评估意见,我在保持原意与意境的基础上,加强了回环叠唱的节奏感,并使结句更加凝练:
> Over a thousand rivers, one moon shines;
> In ten thousand homes, all is spring.
>
> A thousand rivers with water—each holds a moon;
> Ten thousand homes without clouds—ten thousand miles of sky.
修订说明:
- 将 “the selfsame moon” 改为 “one moon”,更简洁有力,也增强回环感。
- 用 “each holds a moon” 强化“千江有水千江月”的对称与叠唱效果,使结构更贴近原诗的重复节奏。
- 将 “stretches ten thousand miles of sky” 凝练为 “ten thousand miles of sky”,保留开阔意境,同时避免冗长。
如果需要,我也可以进一步改写成更具诗歌韵律(如加入头韵或尾韵)的版本。
得分:9
评语:
该翻译在韵律保持、意境传达和准确表达三方面均达到了较高水准。结构上采用‘A thousand rivers/ten thousand homes’的排比与重复,完美还原了原诗的回环往复与音乐性;同时成功传达了‘水月相映、春满人间’的开阔而宁静的禅意。准确表达上,‘each holds a moon’和‘ten thousand miles of sky’紧扣原文,没有任何意义的偏离。如果吹毛求疵,‘万户尽皆春’译为‘all is spring’虽然通顺,但相较于原文‘家家户户’的具象感,略微抽象了一些,这是仅有的可以进一步提升之处。
整个流程经历了两轮翻译-评估循环,最终得到了一个评估分数为9分的译文。
2.2 阻塞式执行
由于LangGraph采用面向状态的执行方式,所以它的非流式阻塞执行可以得到一个具体的结果,但MAF的阻塞式执行只能得到累积的事件。RunAsync方法返回的Run对象提供了与StreamingRun类似的属性和方法来获取Workflow执行的状态和事件,但它没有提供实时事件流的功能,而是需要等到Workflow执行完成后才能获取到所有的事件。
public sealed class Run : CheckpointableRunBase, IAsyncDisposable
{
public string SessionId {get;}
public ValueTask<RunStatus> GetStatusAsync(CancellationToken cancellationToken = default);
public IEnumerable<WorkflowEvent> OutgoingEvents {get;}
public int NewEventCount {get;}
public IEnumerable<WorkflowEvent> NewEvents {get;}
public async ValueTask<bool> ResumeAsync(IEnumerable<ExternalResponse> responses, CancellationToken cancellationToken = default);
public async ValueTask<bool> ResumeAsync<T>(CancellationToken cancellationToken = default, params IEnumerable<T> messages);
public ValueTask DisposeAsync();
}
三个与事件相关的属性说明如下:
- OutgoingEvents:表示
Workflow执行过程中产生的所有事件的集合,这些事件包括Executor的执行状态变化、输出结果、错误信息等; - NewEventCount:表示自上次获取事件以来产生的新事件的数量,可以用于判断是否有新的事件需要处理;
- NewEvents:表示自上次获取事件以来产生的新事件的集合,可以用于获取这些新事件的详细信息;
我们可以按照如下的方式将前面演示的例子中的流式执行方式替换成阻塞式执行方式。由于RunAsync方法会一直阻塞直到Workflow执行完成,所以我们只能在Workflow执行完成后才能获取到所有的事件,这些事件会被封装成NewEvents属性返回给我们,我们需要遍历这些事件来获取翻译结果和评估结果,并将它们打印出来。
var run = await InProcessExecution.Default.RunAsync(workflow, """
千江同一月,万户尽皆春。
千江有水千江月,万户无云万里天。
""");
foreach (var evt in run.NewEvents)
{
if (evt is WorkflowOutputEvent outputEvent)
{
if (outputEvent.ExecutorId == translator.Id)
{
var translation = outputEvent.Data;
Console.WriteLine($"""
译文:
{outputEvent.Data}
""");
}
if (outputEvent.ExecutorId == evaluator.Id) {
var evaluationResult = outputEvent.Data as EvaluationOutput;
Console.WriteLine($"""
得分:{evaluationResult?.Score}
评语:
{evaluationResult?.Reason}
{new string('-', 100)}
""");
}
}
}
3. 基于BSP推进模式
虽然MAF的Workflow与LangGraph的设计和实现不尽相同,但是采用Workflow基于BSP(Bulk Synchronous Parallel)向前推进的方式基本一致,BSP是“Pregel: a system for large-scale graph processing”这篇论文的核心所在。我曾经在“Channel——驱动Node执行的原力”中对BSP模式进行了详细的介绍,在这里简答概述Workflow是如何采用BSP模式来不断向前推进的。
Agent调用作为一种典型的长耗时操作,比如采用并行计算提供执行效率。但是在另一方面,Agent执行过程中会伴随状态的更新,不加控制的并行执行会导致状态不一致的问题。为了解决这个问题,将并行与同步这个看似矛盾的机制结合起来,并在此基础上引入超步(Superstep)和同步屏障(Synchronization Barrier)的概念。一个SupperStep代表Workflow向前推进的一个单步,跨越如下的几个阶段:
- 并行执行当前Superstep中所有满足条件的
Executor,它们统一从上一Superstep结束时冻结的状态中读取输入,这样保证了状态读取的一致性,更新的状态会被缓存在本地; - 当所有满足条件的
Executor都执行完成后,进入同步屏障阶段,在这个阶段会将所有Executor的状态更新以同步的方式应用到Workflow的全局状态中,这样就保证了状态写入的一致性; - 根据连接当前所有节点的边的条件来判断哪些节点满足执行条件,并将这些节点加入到下一个SuperStep中,进入下一个SuperStep的执行;
在每个Superstep开始和结束的时候,分别会发出类型为SuperStepStartedEvent和SuperStepEndedEvent的事件,我们可以利用它们跟踪每个Superstep的边界。在如下所示的演示程序中,我们定义了用于转发消息的ForwardingExecutor。我们创建了四个Id分别为"foo"、“bar”、"baz"和"qux"的ForwardingExecutor,并将它们通过边连接成一个Workflow。
using Microsoft.Agents.AI.Workflows;
var executors = new string[] { "foo", "bar", "baz", "qux", "quux" }
.ToDictionary(it => it, it => new ForwardingExecutor(it));
var workflow = new WorkflowBuilder(executors["foo"])
.AddEdge(executors["foo"], executors["bar"])
.AddEdge(executors["foo"], executors["baz"])
.AddEdge(executors["bar"], executors["qux"])
.AddFanInBarrierEdge([executors["baz"], executors["qux"]], executors["quux"])
.Build();
await Utilities.GenerateAndShowPngImageAsync(workflow,"worflow.png");
var run = await InProcessExecution.Default.RunStreamingAsync(workflow, "");
await foreach (var evt in run.WatchStreamAsync())
{
var log = evt switch
{
ExecutorInvokedEvent invokedEvent => $" Executor {invokedEvent.ExecutorId} 's execution starts.",
ExecutorCompletedEvent completedEvent => $" Executor {completedEvent.ExecutorId}'s execution completes.",
SuperStepStartedEvent superStepStartedEvent => $"Superstep {superStepStartedEvent.StepNumber}",
_ => null
};
if (log is not null)
{
Console.WriteLine(log);
}
}
internal partial class ForwardingExecutor(string id): Executor(id)
{
[MessageHandler]
string Handle(string input, IWorkflowContext context) => input;
}
当Utilities.GenerateAndShowPngImageAsync方法被调用时,显式的图片会反映Workflow的结构,如下所示:
我们以流的方式执行这个Workflow,并通过迭代StreamingRun对象来跟踪三类事件:ExecutorInvokedEvent、ExecutorCompletedEvent和SuperStepStartedEvent。前两类事件分别表示Executor的执行开始和完成,后者表示一个新的SuperStep的开始。我们通过输出确定每个Executor分别是在哪个SuperStep中执行的,以及它们的执行顺序。输出结果如下所示:
Superstep 0
Executor foo 's execution starts.
Executor foo's execution completes.
Superstep 1
Executor bar 's execution starts.
Executor bar's execution completes.
Executor baz 's execution starts.
Executor baz's execution completes.
Superstep 2
Executor qux 's execution starts.
Executor qux's execution completes.
Superstep 3
Executor quux 's execution starts.
Executor quux's execution completes.
Executor quux 's execution starts.
Executor quux's execution completes.
从输出结果可以看出,foo节点在第一个SuperStep中执行,bar和baz节点在第二个SuperStep中并行执行,qux节点在第三个SuperStep中执行,最后quux节点在第四个SuperStep中执行两次。这样的执行顺序完全符合我们预期的BSP推进模式。
4. 工作流事件
不论是采用那种执行方式,我们总是通过事件迭代的方式跟踪Workflow的执行过程,并得到输出的结果,所以我们有必要来看看在Workflow的执行过程中都有哪些事件,以及这些事件分别代表了什么含义。WorkflowEvent是所有Workflow事件的基类,所有的事件都继承自这个类。
[JsonDerivedType(typeof(ExecutorEvent))]
[JsonDerivedType(typeof(SuperStepEvent))]
[JsonDerivedType(typeof(WorkflowStartedEvent))]
[JsonDerivedType(typeof(WorkflowErrorEvent))]
[JsonDerivedType(typeof(WorkflowWarningEvent))]
[JsonDerivedType(typeof(WorkflowOutputEvent))]
[JsonDerivedType(typeof(RequestInfoEvent))]
[JsonDerivedType(typeof(MagenticOrchestratorEvent))]
public class WorkflowEvent(object? data = null)
{
public object? Data => data;
}
WorkflowEvent只有一个Data属性,用于携带事件相关的数据。该类型上标注了多个JsonDerivedType特性,每一个特性都表示一个WorkflowEvent的派生类型:
- ExecutorEvent:Executor相关的事件,包含
ExecutorInvokedEvent和ExecutorCompletedEvent两种类型,分别表示Executor的执行开始和完成; - SuperStepEvent:SuperStep相关的事件,包含
SuperStepStartedEvent和SuperStepEndedEvent两种类型,分别表示SuperStep的开始和结束; - WorkflowStartedEvent:Workflow的开始事件,当
Workflow开始执行时会触发这个事件; - WorkflowErrorEvent:Workflow的错误事件,当
Workflow执行过程中发生错误时会触发这个事件; - WorkflowWarningEvent:Workflow的警告事件,当
Workflow执行过程中发生警告时会触发这个事件; - WorkflowOutputEvent:Workflow的输出事件,当
Workflow中的Executor执行完成并产生输出时会触发这个事件; - RequestInfoEvent:通过向
RequestPort发送请求让用户介入时发出这个事件; - MagenticOrchestratorEvent:为**多智能体动态协同(Magentic 编排模式)**设计的核心可观测性与生命周期事件基类;
5. IWorkflowConext
在Workflow的执行过程中,Executor需要通过IWorkflowContext来与Workflow进行交互,IWorkflowContext提供了一系列的方法来支持Executor在执行过程中进行状态管理、消息传递和输出结果等操作。
public interface IWorkflowContext
{
IReadOnlyDictionary<string, string>? TraceContext { get; }
bool ConcurrentRunsEnabled { get; }
ValueTask AddEventAsync(WorkflowEvent workflowEvent, CancellationToken cancellationToken = default);
ValueTask SendMessageAsync(object message, string? targetId, CancellationToken cancellationToken = default);
ValueTask YieldOutputAsync(object output, CancellationToken cancellationToken = default);
ValueTask RequestHaltAsync();
ValueTask<T?> ReadStateAsync<T>(
string key,
string? scopeName = null,
CancellationToken cancellationToken = default);
ValueTask<T> ReadOrInitStateAsync<T>(
string key,
Func<T> initialStateFactory,
string? scopeName = null,
CancellationToken cancellationToken = default);
ValueTask<T?> ReadStateAsync<T>(
string key,
CancellationToken cancellationToken);
ValueTask<T> ReadOrInitStateAsync<T>(
string key,
Func<T> initialStateFactory, CancellationToken cancellationToken);
ValueTask<HashSet<string>> ReadStateKeysAsync(
string? scopeName = null,
CancellationToken cancellationToken = default);
ValueTask QueueStateUpdateAsync<T>(
string key,
T? value,
string? scopeName = null,
CancellationToken cancellationToken = default);
ValueTask QueueStateUpdateAsync<T>(
string key,
T? value,
CancellationToken cancellationToken) ;
ValueTask QueueClearScopeAsync(
string? scopeName = null,
CancellationToken cancellationToken = default);
ValueTask QueueClearScopeAsync(CancellationToken cancellationToken);
}
IWorkflowContext提供的属性和方法说明如下:
- TraceContext属性:表示当前Workflow执行的跟踪上下文,可以用于记录和分析Workflow的执行过程;
- ConcurrentRunsEnabled属性:表示当前Workflow是否支持并发执行,如果为true则表示Workflow支持多个实例同时执行;
- AddEventAsync方法:手工向Workflow中添加一个事件,这些事件会被封装成WorkflowEvent对象,并在Workflow执行过程中被触发和处理;
- SendMessageAsync方法:用于向Workflow中的下游或者指定节点发送一条消息,这些消息可以是任何类型的对象,目标节点通过targetId参数指定,如果targetId为null则表示发送给所有节点;
- YieldOutputAsync方法:用于向Workflow中产出一个输出结果,这些输出结果会被封装成WorkflowOutputEvent事件,并在Workflow执行过程中被触发和处理;
- RequestHaltAsync方法:用于请求Workflow停止执行,当Workflow接收到这个请求时会尝试停止当前的执行并进入结束流程;
- ReadStateAsync方法:用于读取Workflow中的状态数据,这些状态数据是以键值对的形式存储的,可以通过key参数指定要读取的状态数据的键,scopeName参数用于指定状态数据的范围,起到了一个命名空间的作用;
- ReadOrInitStateAsync方法:用于读取Workflow中的状态数据,如果指定的状态数据不存在则使用
initialStateFactory参数提供的工厂方法来初始化这个状态数据,并返回初始化后的值; - ReadStateKeysAsync方法:用于读取Workflow中指定作用域内的所有状态数据的键,这些键以
HashSet<string>的形式返回; - QueueStateUpdateAsync方法:用于将一个状态更新操作加入到Workflow的状态更新队列中,这些状态更新操作会在Workflow的同步阶段被应用到Workflow的全局状态中;
- QueueClearScopeAsync方法:用于将一个清除状态作用域的操作加入到Workflow的状态更新队列中,这些操作会在Workflow的同步阶段被应用到Workflow的全局状态中;
656

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



