[MAFWorkflow框架揭秘-12]从WorkflowEvent角度解析Workflow的执行周期

Workflow不论式采用阻塞式还是流式执行,我们都是通过事件跟踪Workflow的执行过程的,并得到Executor的输出。Workflow输出的事件以WorkflowEvent作为基类,系统提供的一系列预定义的事件类型,它们会在Workflow的不同阶段被输出,并提供相应的跟踪和输出数据。我们也可以通过继承WorkflowEvent来定义自己的事件类型,并在Workflow的执行过程中通过IWorkflowContextAddEventAsync方法来输出这些事件,从而实现自定义的跟踪和输出功能。

1. 利用事件输出跟踪Workflow的基本执行流程

接下来我们通过一个简单的实例来演示一下Workflow的执行过程中会输出哪些典型事件,以及由它们组成的事件流体现了Workflow怎样的执行流程。由于WorkflowEvent类型多样,不同的事件类型定义了不同的数据类型,我不想针对每个类型定义针对性的输出,所以我采用反射的方式结构性输出每个事件的类型和数据成员。这项操作实现在如下的扩展方法

public static void PrettyPrint(this WorkflowEvent @event);

在如下的演示程序中,我们构建了一个由两个节点构建成用来完成银行转账的Workflow。第一个节点TransactionExtractionExecutor利用调用LLM的方式从用户基于自然语言输入中提取转账信息;第二个节点MoneyTransferExecutor根据提取到的转账信息执行转账操作。我们以流的方式执行这个Workflow,并调用返回StreamingRun对象的WatchStreamAsync方法来获取事件流,最后利用上面定义的PrettyPrint方法结构化输出每个事件的类型和数据成员。

using Azure;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Extensions.AI;
using OpenAI;
using System.ComponentModel;
using System.Text.RegularExpressions;

dotenv.net.DotEnv.Load();
var endpoint = Environment.GetEnvironmentVariable("OPENAI_URL")!;
var apiKey = Environment.GetEnvironmentVariable("API_KEY")!;
var model = Environment.GetEnvironmentVariable("MODEL")!;

var chatClient = new OpenAIClient(
        credential: new AzureKeyCredential(apiKey),
        options: new OpenAIClientOptions { Endpoint = new Uri(endpoint) })
    .GetChatClient(model: model)
    .AsIChatClient();

ExecutorBinding transactionExtractor = new TransactionExtractionExecutor(chatClient);
ExecutorBinding moneyTransferExecutor = new MoneyTransferExecutor();
var workflow = new WorkflowBuilder(transactionExtractor)
    .AddEdge<Transaction>(transactionExtractor, moneyTransferExecutor, transaction=>transaction?.IsValid ?? false)
    .WithOutputFrom(transactionExtractor, moneyTransferExecutor)
    .Build();

await using(var run = await InProcessExecution.Default.RunStreamingAsync(workflow, "从账号6222020200088888888上转50元到账号6222020200099999999上"))
{
    var index = 1;
    await foreach (var @event in run.WatchStreamAsync())
    {
        Console.WriteLine($"{new string('-',20)}Event {index++}{new string('-', 20)}");
        @event.PrettyPrint();
    }
}


[Description("银行转账业务")]
public partial record Transaction(

    [Description("转出账号")] string? From,
    [Description("转入账号")] string? To,
    [Description("转账金额")] decimal Ammount)
{
    public bool IsValid => BankAccountRegex().IsMatch(From ?? "") && BankAccountRegex().IsMatch(To ?? "") && Ammount > 0;

    [GeneratedRegex(@"^\d{16,19}$")]
    private static partial Regex BankAccountRegex();
}

public partial class TransactionExtractionExecutor(IChatClient chatClient):Executor("TransactionExtract")
{
    [MessageHandler(Send = [typeof(Transaction)],Yield = [typeof(string)])]
    public async ValueTask ExtractAsync(string input, IWorkflowContext context)
    { 
        var response = await chatClient.GetResponseAsync<Transaction>(input);
        var transaction = response.Result;
        if (!transaction.IsValid)
        {
            await context.YieldOutputAsync("没有从输入中提取有效的转账信息");
        }
        else
        {
            await context.SendMessageAsync(transaction);
        }
    }
}

public partial class MoneyTransferExecutor() : Executor("MoneyTransfer")
{
    [MessageHandler(Yield = [typeof(string)])]
    public ValueTask TransferAsync(Transaction transaction, IWorkflowContext context)
   =>context.YieldOutputAsync($"成功从{transaction.From}转账{transaction.Ammount}转到{transaction.To}.");
}

输出:

--------------------Event 1--------------------
 Type: WorkflowStartedEvent
 Data: null

--------------------Event 2--------------------
 Type: SuperStepStartedEvent
 StartInfo: <SuperStepStartInfo>
   SendingExecutors: [TransactionExtract]
   HasExternalMessages: False
 StepNumber: 0
 Data: <Object>
   SendingExecutors: [TransactionExtract]
   HasExternalMessages: False

--------------------Event 3--------------------
 Type: ExecutorInvokedEvent
 ExecutorId: TransactionExtract
 Data: 从账号6222020200088888888上转50元到账号6222020200099999999上

--------------------Event 4--------------------
 Type: ExecutorCompletedEvent
 ExecutorId: TransactionExtract
 Data: null

--------------------Event 5--------------------
 Type: SuperStepCompletedEvent
 CompletionInfo: <SuperStepCompletionInfo>
   ActivatedExecutors: [TransactionExtract, MoneyTransfer]
   InstantiatedExecutors: []
   StateUpdated: False
   HasPendingMessages: True
   HasPendingRequests: False
   Checkpoint: null
 StepNumber: 0
 Data: <Object>
   ActivatedExecutors: [TransactionExtract, MoneyTransfer]
   InstantiatedExecutors: []
   StateUpdated: False
   HasPendingMessages: True
   HasPendingRequests: False
   Checkpoint: null

--------------------Event 6--------------------
 Type: SuperStepStartedEvent
 StartInfo: <SuperStepStartInfo>
   SendingExecutors: [MoneyTransfer]
   HasExternalMessages: False
 StepNumber: 1
 Data: <Object>
   SendingExecutors: [MoneyTransfer]
   HasExternalMessages: False

--------------------Event 7--------------------
 Type: ExecutorInvokedEvent
 ExecutorId: MoneyTransfer
 Data: <Object>
   From: 6222020200088888888
   To: 6222020200099999999
   Ammount: 50
   IsValid: True

--------------------Event 8--------------------
 Type: WorkflowOutputEvent
 ExecutorId: MoneyTransfer
 SourceId: MoneyTransfer
 Tags: []
 Data: 成功从6222020200088888888转账50转到6222020200099999999.

--------------------Event 9--------------------
 Type: ExecutorCompletedEvent
 ExecutorId: MoneyTransfer
 Data: null

--------------------Event 10--------------------
 Type: SuperStepCompletedEvent
 CompletionInfo: <SuperStepCompletionInfo>
   ActivatedExecutors: [MoneyTransfer]
   InstantiatedExecutors: []
   StateUpdated: False
   HasPendingMessages: False
   HasPendingRequests: False
   Checkpoint: null
 StepNumber: 1
 Data: <Object>
   ActivatedExecutors: [MoneyTransfer]
   InstantiatedExecutors: []
   StateUpdated: False
   HasPendingMessages: False
   HasPendingRequests: False
   Checkpoint: null

上面演示程序输出的10个事件基本体现了Workflow的一个完整的执行流程,接下来我们就针对这个流程分类介绍涉及的WorkflowEvent类型。

2. WorkflowEvent

我们先从基类开始。如下面的代码所示,作为基类的WorkflowEvent定义了一个Data属性来携带事件数据,并重写了ToString方法将事件类型和数据格式化成一个具有可读性的文本。

public class WorkflowEvent(object? data = null)
{
	public object? Data => data;
	public override string ToString()
	=>(Data != null) 
    ? $"{GetType().Name}(Data: {Data.GetType()} = {Data})" 
    : (GetType().Name + "()");
}

3. WorkflowStartedEvent

WorkflowStartedEvent是Workflow执行开始时输出的第一个事件,它代表了一个Workflow执行的开始。WorkflowStartedEvent并未定义额外的数据成员,所以它更多地是一个标识性的事件类型,用来标识Workflow的开始。

public sealed class WorkflowStartedEvent : WorkflowEvent
{
	public WorkflowStartedEvent(object? message = null)
		: base(message)
	{}
}

4. SuperStepEvent

Workflow采用BSP(Bulk Synchronous Parallel)机制先前推进执行。具体来说,Workflow以Superstep为单位来推进执行。除了Superstep 0用于执行入口节点的Executor之外,后续Superstep执行的节点会在上一个Superstep的最后阶段被解析并激活。在每个Superstep开始的时候,这些Executor在提供的执行环境中并发执行,Executor可以利用IWorkflowContext相应的方法控制数据流:

  • SendMessageAsync:将消息发送给下一个节点;
  • YieldOutputAsync:将消息作为面向调用方的输出;
  • QueueStateUpdateAsync:提交状态更新。

为了在并发中保证状态的一致性,Executor只能读取上一Superstep完成时被冻结的状态。它们也不能直接修改状态,只能提交自己针对状态的更新请求,这也是为什么用于更新状态的方法叫做QueueStateUpdateAsync而不是UpdateStateAsync的原因。等到所有Executor完成执行之后,Superstep进入一个叫做同步屏障的阶段。在此阶段,提交的状态以同步的方式被应用到状态上。最后根据连接当前节点的边结合路由的消息解析出下一步要执行的节点,并将它们激活。

整个Superstep以发出的SuperStepStartedEventSuperStepCompletedEvent事件作为边界,这两个事件类型继承自SuperStepEventSuperStepEvent在基类的基础上定义了一个StepNumber属性来表示当前Superstep的编号,并通过重写的ToString方法将这个编号也包含在事件的文本表示中。

public class SuperStepEvent(int stepNumber, object? data = null) : WorkflowEvent(data)
{
	public int StepNumber => stepNumber;
	public override string ToString()
	=>(base.Data != null) 
    ? $"{GetType().Name}(Step = {StepNumber}, Data: {base.Data.GetType()} = {base.Data})" 
    : $"{GetType().Name}(Step = {StepNumber})";
}

SuperStepStartedEvent定义了额外的StartInfo属性返回一个SuperStepStartInfo对象。

public sealed class SuperStepStartedEvent(int stepNumber, SuperStepStartInfo? startInfo = null) 
    : SuperStepEvent(stepNumber, startInfo)
{
	public SuperStepStartInfo? StartInfo => startInfo;
}
public sealed class SuperStepStartInfo(HashSet<string>? sendingExecutors = null)
{
	public HashSet<string> SendingExecutors { get; } = sendingExecutors ?? new HashSet<string>();
	public bool HasExternalMessages { get; init; }
}

对于SuperStepStartInfoSendingExecutors,API文档的注释中是这样描述的:The unique identifiers of <see cref="Executor"/> instances that sent messages during the previous SuperStep.,也就是说它表示在上一个Superstep中发出消息的Executor的Id集合。但是从演示程序的输出来看,它明明是当前Superstep待执行的Executor的Id集合:

  • Event 2:SendingExecutors包含TransactionExtract,而TransactionExtract是Superstep 0中待执行的Executor;
  • Event 6:SendingExecutors包含MoneyTransfer,而MoneyTransfer是Superstep 1中待执行的Executor。
--------------------Event 2--------------------
 Type: SuperStepStartedEvent
 StartInfo: <SuperStepStartInfo>
   SendingExecutors: [TransactionExtract]
   HasExternalMessages: False
 StepNumber: 0
 Data: <Object>
   SendingExecutors: [TransactionExtract]
   HasExternalMessages: False

...

--------------------Event 6--------------------
 Type: SuperStepStartedEvent
 StartInfo: <SuperStepStartInfo>
   SendingExecutors: [MoneyTransfer]
   HasExternalMessages: False
 StepNumber: 1
 Data: <Object>
   SendingExecutors: [MoneyTransfer]
   HasExternalMessages: False

所以SendingExecutors究竟如何理解呢?我目前起始还不太确定,暂且搁置这个问题。至于SuperStepStartInfo的另一个属性HasExternalMessages则比较好理解了,它表示当前Superstep中是否有来自外部的消息需要处理。Workflow中所谓的外部消息指的是发送给RequestPort旨在完成人机交互的ExternalRequest对象。

SuperStepStartedEvent利用StartInfo属性来提供Superstep开始信息类似,SuperStepCompletedEvent利用CompletionInfo属性来提供Superstep完成信息。CompletionInfo的类型是SuperStepCompletionInfo

public sealed class SuperStepCompletedEvent(
    int stepNumber, 
    SuperStepCompletionInfo? completionInfo = null) 
    : SuperStepEvent(stepNumber, completionInfo)
{
	public SuperStepCompletionInfo? CompletionInfo => completionInfo;
}

public sealed class SuperStepCompletionInfo
{
	public HashSet<string> ActivatedExecutors { get; }
	public HashSet<string> InstantiatedExecutors { get; }
	public bool StateUpdated { get; init; }
	public bool HasPendingMessages { get; init; }
	public bool HasPendingRequests { get; init; }
	public CheckpointInfo? Checkpoint { get; init; }
}

SuperStepCompletionInfo定义了如下几个属性:

  • ActivatedExecutors:当前Superstep中被激活的Executor的Id集合;
  • InstantiatedExecutors:当前Superstep中被实例化的Executor的Id集合;
  • StateUpdated:当前Superstep中是否有状态更新被提交;
  • HasPendingMessages:当前Superstep完成时是否有待处理的消息;
  • HasPendingRequests:当前Superstep完成时是否有待处理的请求;
  • Checkpoint:当前Superstep完成时是否创建了Checkpoint,如果有的话CheckpointInfo对象会提供Checkpoint的相关信息。

5. ExecutorEvent

ExecutorEvent表示与Executor执行相关的事件,所它定义了一个ExecutorId属性来表示这个事件关联的Executor的Id。重写的ToString方法也将这个ExecutorId包含在事件的文本表示中。

public class ExecutorEvent(string executorId, object? data) : WorkflowEvent(data)
{
	public string ExecutorId => executorId;
	public override string ToString()
        =>(base.Data != null) 
        ? $"{GetType().Name}(Executor = {ExecutorId}, Data: {base.Data.GetType()} = {base.Data})" 
        : $"{GetType().Name}(Executor = {ExecutorId})";
}

定义在Executor上的处理函数开始执行之前,会输出一个ExecutorInvokedEvent事件,其Data属性携带了这个处理函数的输入数据;处理函数调用之后,如果没有抛出异常,则会输出一个ExecutorCompletedEvent事件,其Data属性为执行的结果。如果处理函数调用过程中抛出了异常,则会输出一个ExecutorFailedEvent事件,其Data属性携带了这个异常对象。

public sealed class ExecutorInvokedEvent : ExecutorEvent
{
	public ExecutorInvokedEvent(string executorId, object message)
		: base(executorId, message)
	{}
}
public sealed class ExecutorCompletedEvent : ExecutorEvent
{
	public ExecutorCompletedEvent(string executorId, object? result)
		: base(executorId, result)
	{}
}
public sealed class ExecutorFailedEvent(string executorId, Exception? err) : ExecutorEvent(executorId, err)
{
	public new Exception? Data => err;
}

6. WorkflowOutputEvent

如果在构建Workflow的时候将某个Executor设置为输出节点,那么这个Executor在执行时针对IWorkflowContextYieldOutputAsync方法的调用会输出一个WorkflowOutputEvent事件,其Data属性携带了YieldOutputAsync方法中传递的输出数据。WorkflowOutputEvent定义了一个SourceId属性来表示这个输出数据来源于哪个Executor。为了方便提取输出的输出据,WorkflowOutputEvent定义了一系列类型验证和转换的方法。

public class WorkflowOutputEvent : WorkflowEvent
{
	public string ExecutorId { get; }
	public WorkflowOutputEvent(object data, string executorId);

	public bool Is<T>();
	public bool Is<T>([NotNullWhen(true)] out T? maybeValue);
	public bool IsType(Type type);
	public T? As<T>();
	public object? AsType(Type type);
}

在我的文章“Worflow功能节点的多种定义方式”,我介绍过用于将AIAgent作为Workflow的功能节点的AIAgentHostExecutor类。如果对应AIAgentHostOptionsEmitAgentUpdateEvents或者EmitAgentResponseEvents配置选项被设置为true,内部会在调用IWorkflowContextYieldOutputAsync输出对应的AgentResponseUpdate或者AgentResponse对象,此方法调用会进一步输出类型分别为AgentResponseEventAgentResponseUpdateEvent的事件。这两个事件的Data属性分别携带了AgentResponseAgentResponseUpdate对象,属性ResponseUpdate属性返回的也是这两个对象。

public sealed class AgentResponseEvent : WorkflowOutputEvent
{
	public AgentResponse Response { get; }
	public AgentResponseEvent(string executorId, AgentResponse response);
}
public sealed class AgentResponseUpdateEvent : WorkflowOutputEvent
{
	public AgentResponseUpdate Update { get; }
	public AgentResponseUpdateEvent(string executorId, AgentResponseUpdate update);
	public AgentResponse AsResponse();
}

7. RequestInfoEvent

在“人机交互(HITL)的实现”中,我们已经对RequestInfoEvent这个事件类型进行了详细的介绍。我们知道,当我们在编排的Workflow放入一个根据RequestPort创建的RequestPortBinding时,路由到此处的消息会被封装成一个ExternalRequest对象,并最终以RequestInfoEvent事件的形式输出。如下面的代码所示,RequestInfoEvent在基类的基础上定义了额外的属性Request来返回这个ExternalRequest对象。

public sealed class RequestInfoEvent(ExternalRequest request) : WorkflowEvent(request)
{
	public ExternalRequest Request => request;
}

public record ExternalRequest(RequestPortInfo PortInfo, string RequestId, PortableValue Data)
{
	public bool IsDataOfType<TValue>();
	public bool TryGetDataAs<TValue>([NotNullWhen(true)] out TValue? value);
	public bool TryGetDataAs(Type targetType, [NotNullWhen(true)] out object? value);

	public static ExternalRequest Create(RequestPort port, [NotNull] object data, string? requestId = null);
	public static ExternalRequest Create<T>(RequestPort port, T data, string? requestId = null);

	public ExternalResponse CreateResponse(object data);
	public ExternalResponse CreateResponse<T>(T data);
}

ExternalRequest表示从Workflow内部发出的一个对外请求,我们利用利用它得到作为人机交互端口的RequestPort对象、请求数据以及请求Id等信息。具体请求数据以PortableValue的形式返回,为了更好的读取它,ExternalRequest提供了类型检验的IsDataOfType方法,和强类型的数据读取方法TryGetDataAsExternalRequest可以通过静态的Create方法来创建,CreateResponse方法则可以基于指定的请求创建作为响应的ExternalResponse对象。

8. RequestHaltEvent

IWorkflowContext定义了一个RequestHaltAsync方法,它是一个用来控制工作流执行生命周期的核心导航与控制级方法。RequestHaltAsync充当了工作流的紧急制动刹车,允许图中的任意一个Executor显式通知引擎:“发生重大情况,请在当前周期结束后,立刻暂停或终止整个工作流的驱动”。调用 RequestHaltAsync方法的动作属于异步请求,采用受控暂停(Graceful Halt)执行方式,而不是像抛出异常或调用线程强杀那样瞬间中断代码。

public interface IWorkflowContext
{
    ValueTask RequestHaltAsync();
}

RequestHaltAsync方法被调用时,会在内部输出RequestHaltEvent事件。于此通过,事件流的终止,所以我们无法捕捉到这个事件。也正是基于这个原因,RequestHaltEvent被定义成一个internal类型。值得一提的是,当此事件被输出时,StreamingRun的状态仍然是Running

internal sealed class RequestHaltEvent : WorkflowEvent
{
	internal RequestHaltEvent(object? result = null);
}

9. WorkflowErrorEvent & WorkflowWarningEvent

WorkflowErrorEventWorkflowWarningEvent 是两个非常核心的可监听的系统级诊断事件,专门用于向最外层的宿主程序(如您的主循环控制台、应用日志服务)暴露整个工作流在运行时遭遇的不稳定状态与异常信息。通过监听这两个事件,您可以摆脱粗暴的try-catch崩溃挂起,在外部实现极其优雅的实时监控、降级容错和审计报警。

public class WorkflowErrorEvent : WorkflowEvent
{
	public Exception? Exception { get; }
	public WorkflowErrorEvent(Exception? e);
}
public class WorkflowWarningEvent : WorkflowEvent
{
	public WorkflowWarningEvent(string message)
		: base(message)
	{
	}
}

Workflow核心框架在执行过程中并未输出WorkflowWarningEvent事件,我们可以通过显式调用IWorkflowContextAddEventAsync方法来输出WorkflowWarningEvent事件,从而实现自定义的警告信息输出。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值