[MAFWorkflow框架揭秘-11]如何将Workflow转换成AIAgent

有时,我们可能构建了一个包含多个Agent、自定义Executor和复杂逻辑的复杂Workflow,但您希望像使用其他Agent一样使用它。WorkflowHostAgent正是为此而生的。通过将您的Workflow封装成一个Agent,可以获得如下的好处:

  • 统一接口:使用与简单Agent相同的API与复杂Workflow交互;
  • API 兼容性:将Workflow与支持Agent接口的现有系统集成;
  • 可组合性:将WorkflowHostAgent用作构建块,构建更大型的Agent系统或其他Workflow;
  • 会话管理:利用Agent会话来管理对话状态和恢复;
  • 流式传输支持:在Workflow执行过程中获取实时更新。

虽然我们只需要调用Workflow的AsAgent扩展方法就能将Workflow转换成一个AIAgent,但是只有满足条件的Workflow才能成功转换成一个AIAgent。由于AIAgent采用基于ChatMessage的交互方式,所以Workflow必须满足以下条件。除此之外,Workflow和AIAgent针对人机交互的方式也不太一样,前者通过RequestPort来实现人机交互,后者指支持基于工具审批的交互方式。

  • Workflow的输入类型必需包含List<ChatMessage>TurnToken类型;
  • 如果将某个Executor添加到输出节点中,输出内容也必需封装成ChatMessage对象。

1. 将一个转账工作流转换成AIAgent

在“人机交互(HITL)的实现”中,我们构建了一个转账工作流,其中包含了一个人工审批的步骤。现在我们对它略加修改,使它满足上述条件,然后将它转换成一个AIAgent。修改后的Workflow具有如下的结构:

Alternative Text

整个Worflow由如下四个Executor组成:

  • TransactionExtractor:调用LLM来从用户输入的消息中提取转账信息,转换成一个代表转账业务的TransferTransaction对象;
  • RiskCheck:风险评估,如果转账金额>10000就认为是高风险交易,需要人工审批,否则直接执行;
  • Approval:一个RequestPort类型的Executor,对外发送审批请求,并等待审批结果;
  • MoneyTransfer:执行转账操作。

这个转账流程涉及如下两个数据类型,分别是代表转账业务的TransferTransaction对象和代表转账输入的TransferInput对象。TransferTransactionTransactionExtractor提取的结构化输出经过反序列化生成,IsValid属性表示转账信息是否合法。TransferInputTransaction属性保存了转账信息,IsApproved属性表示审批结果。

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

    [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 record TransferInput(TransferTransaction Transaction, bool IsApproved = false);

Workflow涉及的三个自定义Executor定义如下。我们让用于从自然语言提取结构化转账信息的TransactionExtractionExecutor继承ChatProtocolExecutor,并提供一个用于调用LLM的IChatClient对象。在重写的TakeTurnAsync方法中,我们首先检查输入消息列表中的最后一条消息是否是用户消息,在确认后调用LLM并将提取的转账信息反序列化成TransferTransaction对象。如果通过其IsValid属性确定转账信息不符合要求,我们就通过调用IWorkflowContextYieldOutputAsync方法来输出一条消息并提示用户重新输入。

public partial class TransactionExtractionExecutor(IChatClient chatClient) 
: ChatProtocolExecutor("TransactionExtract")
{
    protected override async ValueTask TakeTurnAsync(
        List<ChatMessage> messages, 
        IWorkflowContext context, 
        bool? emitEvents, 
        CancellationToken cancellationToken = default)
    {
        var lastMessage = messages.LastOrDefault();
        if (lastMessage?.Role != ChatRole.User)
        {
            return;
        }

        var prompt = $"""
            从如下的用户输入中提取转账信息,转账信息包括:转出账号、转入账号、转账金额。

            用户输入:
            {lastMessage}
            """;
        var response = await chatClient.GetResponseAsync<TransferTransaction>(prompt, cancellationToken: cancellationToken);
        var transaction = response.Result;
        if (!transaction.IsValid)
        {
            await context.YieldOutputAsync(new ChatMessage(ChatRole.Assistant, "无法提取有效的转账信息,重新输入。"), cancellationToken);
        }
        await context.SendMessageAsync(response.Result, cancellationToken: cancellationToken);
    }

    protected override ProtocolBuilder ConfigureProtocol(ProtocolBuilder protocolBuilder)
    =>base.ConfigureProtocol(protocolBuilder
            .SendsMessage<TransferTransaction>()
            .YieldsOutput<ChatMessage>());
}

public partial class RiskCheckExecutor() : Executor("RiskCheck")
{
    [MessageHandler(Send = [typeof(TransferInput), typeof(TransferTransaction)])]
    public ValueTask CheckAsync(TransferTransaction transaction, IWorkflowContext context)
    {
        if (transaction.Ammount < 1000)
        {
            return context.SendMessageAsync(new TransferInput(transaction, true));
        }
        else
        {
            return context.SendMessageAsync(transaction);
        }
    }
}

public partial class TransferExecutor() : Executor("MoneyTransfer")
{
    [MessageHandler(Yield = [typeof(ChatMessage),typeof(string)])]
    public async ValueTask Transfer(TransferInput input, IWorkflowContext context)
    {
        var transaction = input.Transaction;
        var content = input.IsApproved
            ? $"""
            转账成功
            转出账号:{transaction.From} 
            转入账号:{transaction.To}
            转账金额:{transaction.Ammount}
            """           
            : "该笔转账未被批准。";
        await context.YieldOutputAsync(new ChatMessage(ChatRole.Assistant, content));
    }
}

有效的TransferTransaction对象会被发送给RiskCheckExecutor进行评估,如果转账金额小于1000,RiskCheckExecutor会直接将其封装成一个TransferInput对象并发送给TransferExecutor来执行转账操作。如果转账金额大于等于1000,RiskCheckExecutor会将其发送给Approval节点来等待审批结果。由于Approval节点是一个RequestPort类型的Executor,所以它会将TransferTransaction对象封装成一个PortableValue对象,并将其发送给外部系统来进行审批。审批结果会被封装成一个TransferInput对象送回Workflow,然后作为TransferExecutor的输入执行转账操作。

我们利用如下的程序构建了具有上图所示结构的Workflow,并调用扩展方法AsAIAgent将它转换成一个AIAgent。然后我们在一个循环中与这个WorkflowHostAgent进行交互:用户输入转账指令,WorkflowHostAgent会根据输入的转账指令来执行相应的操作。

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 inputNormalizer = new TransactionExtractionExecutor(chatClient);
ExecutorBinding riskCheckExecutor = new RiskCheckExecutor();
ExecutorBinding transferExecutor = new TransferExecutor();
ExecutorBinding requestPort = RequestPort.Create<TransferTransaction, TransferInput>("Approval");

var agent = new WorkflowBuilder(inputNormalizer)
    .AddEdge<TransferTransaction>(
        source: inputNormalizer,
        target: riskCheckExecutor,
        condition: transaction=>transaction?.IsValid??false,
        label: "Normalize Input")
    .AddEdge(
        source: riskCheckExecutor,
        target: transferExecutor,
        condition: (object? input) => input is TransferInput,
        label: "Ammount<1000")
    .AddEdge(
        source: riskCheckExecutor,
        target: requestPort,
        condition: (object? input) => input is TransferTransaction,
        label: "Ammount>=1000")
    .AddEdge(source: requestPort, target: transferExecutor)
    .WithOutputFrom(inputNormalizer, transferExecutor)
    .Build()
    .AsAIAgent(includeWorkflowOutputsInResponse: true);

var session = await agent.CreateSessionAsync();
while (true)
{
    Console.Write("请输入转账指令:");
    var userInput = Console.ReadLine() ?? "";
    var response = await agent.RunAsync(userInput, session);
    Console.WriteLine(response);
    Console.WriteLine();
}

启动程序后,我们与Agent进行了如下两次交互,第一次输入了一个合法的转账指令,第二次输入了一个不合法的转账指令。可以看到,WorkflowHostAgent能够正确地处理用户输入,并根据输入的转账指令来执行相应的操作。

请输入转账指令:从账号6222020200088888888上转50元到账号6222020200099999999上
转账成功
转出账号:6222020200088888888
转入账号:6222020200099999999
转账金额:50.0

请输入转账指令:abcd
无法提取有效的转账信息,重新输入。

2. 审批请求的处理

上面的演示中,由于输入的金额比较小,所以没有触发审批请求的流程。如果输入了大于1000的转账指令,就会触发审批请求的流程。按照我们对Worflow运行机制的理解,此时会生成的TransferTransaction会发送给RequestPort节点,并转换成一个RequestInfoEvent。但是现在的调用方式接换成了AIAgent,我们再也不能通过遍历事件流的方式来获取这个RequestInfoEvent了。

此时转换的AIAgent会将外部请求转换成一个工具调用。该工具函数将以RequestPort节点的ID命名,原始的输入(TransferTransaction)将会被封装成一个PortableValue对象。但是我们不能通过注册一个与之匹配的工具完成审批(因为WorkflowHostAgent并不具有ChatClientAgent拥有的管道,自然也没有实现ReAct的FunctionInvokingChatClient),需要手工处理这样的响应。具体的做法为:我们试着从最后一条响应消息中提取工具函数以Approval命名的FunctionCallContent对象,并从中提取封装PortableValue对象,然后进一步将它转换成TransferTransaction对象。接下来我们就可以根据这个TransferTransaction对象的内容来决定是否批准这笔转账了。最后我们将审批结果封装成一个FunctionResultContent对象,再次调用Agent的RunAsync方法来将审批结果发送回Workflow。

var session = await agent.CreateSessionAsync();

while (true)
{
    Console.Write("请输入转账指令:");
    var userInput = Console.ReadLine() ?? "";
    var response = await agent.RunAsync(userInput, session);

    var content = response.Messages.Last().Contents.OfType<FunctionCallContent>().SingleOrDefault(it => it.Name == "Approval");
    if (content is not null)
    {
        var portableValue = (PortableValue)content.Arguments!.Values.Single()!;
        var transaction = portableValue.As<TransferTransaction>()!;
        Console.Write($"""
            如下一笔转账请求需要你的审批:
            转出账号: {transaction.From}, 
            转入账号: {transaction.To}, 
            转账金额: {transaction.Ammount} RMB

            是否批准这笔转账?(y/n)""".Trim());
        var approval = Console.ReadLine()?.Trim().ToLower() == "y";
        var transferInput = new TransferInput(transaction, approval);
        var responseContent = new FunctionResultContent(content.CallId, transferInput);
        var responseMessage = new ChatMessage(ChatRole.Tool, [responseContent]);
        response = await agent.RunAsync(responseMessage, session);
    }
    Console.WriteLine(response);
    Console.WriteLine();
}

再次运行我们的程序,并按照如下的方式输入一笔金额为1150的转账指令,我们就会触发审批请求的流程。可以看到,WorkflowHostAgent正确地将审批请求发送了出来,并且我们也能够正确地处理这个审批请求了。

请输入转账指令:从账号6222020200088888888上转1150元到账号6222020200099999999上
如下一笔转账请求需要你的审批:
转出账号: 6222020200088888888,
转入账号: 6222020200099999999,
转账金额: 1150.0 RMB

是否批准这笔转账?(y/n):y
转账成功
转出账号:6222020200088888888
转入账号:6222020200099999999
转账金额:1150.0

请输入转账指令:从账号6222020200088888888上转1150元到账号6222020200099999999上
如下一笔转账请求需要你的审批:
转出账号: 6222020200088888888,
转入账号: 6222020200099999999,
转账金额: 1150.0 RMB

是否批准这笔转账?(y/n):n
该笔转账未被批准。

3. WorkflowHostAgent

调用Workflow的AsAIAgent方法会返回一个WorkflowHostAgent对象,WorkflowHostAgent为如下所示的一个internal类型的类。

internal sealed class WorkflowHostAgent : AIAgent
{
	protected override string? IdCore { get; }
	public override string? Name { get; }
	public override string? Description { get; }

	public WorkflowHostAgent(
        Workflow workflow, 
        string? id = null, 
        string? name = null, 
        string? description = null, 
        IWorkflowExecutionEnvironment? executionEnvironment = null, 
        bool includeExceptionDetails = false, 
        bool includeWorkflowOutputsInResponse = false);

构造函数说明如下:

  • workflow:要封装成Agent的Workflow对象;
  • id:Agent的ID,如果不提供,系统会自动生成一个唯一的ID;
  • name:Agent的名称;
  • description:Agent的描述信息;
  • executionEnvironment:Workflow的执行环境,如果不提供,具体使用怎样的执行环境取决于Workflow的AllowConcurrent属性:
    • 如果AllowConcurrent为true,使用InProcessExecution.Concurrent
    • 如果AllowConcurrent为false,使用InProcessExecution.OffThread
  • includeExceptionDetails:一个布尔值,表示在Agent的响应中是否包含Workflow执行过程中抛出的异常的详细信息,默认为false;
  • includeWorkflowOutputsInResponse:一个布尔值,表示是否将Workflow的Output节点的输出包含在响应消息中,默认为false。

3.1 WorflowSession

WorkflowHostAgent具有数据自己专属的Session类型WorkflowSession,其定义如下:

internal sealed class WorkflowSession : AgentSession
{
    public CheckpointInfo? LastCheckpoint { get; set; }
	public string? LastResponseId { get; set; }
	public string SessionId { get; }
	public WorkflowChatHistoryProvider ChatHistoryProvider { get; }

    public WorkflowSession(
        Workflow workflow, 
        JsonElement serializedSession, 
        IWorkflowExecutionEnvironment executionEnvironment, 
        bool includeExceptionDetails = false, 
        bool includeWorkflowOutputsInResponse = false, 
        JsonSerializerOptions? jsonSerializerOptions = null);
}

四个属性说明如下:

  • LastCheckpoint:保存了上一次执行Workflow时生成的Checkpoint的信息;
  • LastResponseId:保存了上一次执行Workflow时生成的响应消息的ID;
  • SessionId:WorkflowSession的ID;
  • ChatHistoryProvider:WorkflowSession使用的WorkflowChatHistoryProvider对象。

WorkflowChatHistoryProvider继承自ChatHistoryProvider。它将作为对话历史的消息列表封在一个StoreState对象上(对应Messages属性),并利用_sessionState字段返回的ProviderSessionState<StoreState>对象来维护此状态。ProvideChatHistoryAsyncStoreChatHistoryAsync方法针对对话历史的读写都是借助于此对象来完成的。

internal sealed class WorkflowChatHistoryProvider : ChatHistoryProvider
{
	internal sealed class StoreState
	{
		public int Bookmark { get; set; }
		public List<ChatMessage> Messages { get; set; } = new List<ChatMessage>();
	}

    private readonly ProviderSessionState<StoreState> _sessionState;
	public override IReadOnlyList<string> StateKeys { get; }

	public WorkflowChatHistoryProvider(JsonSerializerOptions? jsonSerializerOptions = null);

	protected override ValueTask<IEnumerable<ChatMessage>> ProvideChatHistoryAsync(
        InvokingContext context, 
        CancellationToken cancellationToken = default);
	protected override ValueTask StoreChatHistoryAsync(
        InvokedContext context, 
        CancellationToken cancellationToken = default);

	public IEnumerable<ChatMessage> GetFromBookmark(AgentSession session);
	public IEnumerable<ChatMessage> GetAllMessages(AgentSession session);
	public void UpdateBookmark(AgentSession session);
}

StoreState还定义了一个Bookmark属性,可以视为上次读取对话历史时的一个书签。我们利用它实现对话历史的增量读取。每次读取对话历史时,我们都从Bookmark的位置开始读取,读取完成后再更新Bookmark的位置。这样就避免了每次都要从头读取整个对话历史了。这个书签的值就是上次读取时对话历史的总长度,当由新的消息生成,我们只需从这个书签位置开始读取新的消息即可。基于书签的增量读取实现在GetFromBookmark方法中,另一个GetAllMessages方法则是从头读取整个对话历史的实现。UpdateBookmark方法则提供了更新书签位置的功能。

WorkflowHostAgent通过重写与Session相关的三个方法来实现WorkflowSession的创建、序列化和反序列化。

internal sealed class WorkflowHostAgent : AIAgent
{
    protected override ValueTask<AgentSession> CreateSessionCoreAsync(
        CancellationToken cancellationToken = default);

	protected override ValueTask<JsonElement> SerializeSessionCoreAsync(
        AgentSession session, 
        JsonSerializerOptions? jsonSerializerOptions = null, 
        CancellationToken cancellationToken = default);

	protected override ValueTask<AgentSession> DeserializeSessionCoreAsync(
        JsonElement serializedState, 
        JsonSerializerOptions? jsonSerializerOptions = null, 
        CancellationToken cancellationToken = default);
}

3.2 调用流程

WorkflowHostAgent重写了AIAgentRunCoreAsyncRunCoreStreamingAsync方法来实现了针对Agent的阻塞式和流式调用。

internal sealed class WorkflowHostAgent : AIAgent
{
    protected override async Task<AgentResponse> RunCoreAsync(
        IEnumerable<ChatMessage> messages, 
        AgentSession? session = null, 
        AgentRunOptions? options = null, 
        CancellationToken cancellationToken = default);
    protected override IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingAsync(
        IEnumerable<ChatMessage> messages, 
        AgentSession? session = null, 
        AgentRunOptions? options = null, 
        CancellationToken cancellationToken = default)
}

RunCoreAsync方法虽然是一个阻塞式调用的方法,但是它内部依然采用流的方式执行Workflow,所以与RunCoreStreamingAsync的执行流程比较类似。RunCoreAsync大体采用如下的执行流程。

  • 如果没有通过session参数指定一个WorkflowSession,我们就创调用CreateSessionAsync方法来创建一个WorkflowSession
  • 利用WorkflowSessionChatHistoryProvider将指定的消息列表添加到对话历史中;
  • 从存储的StoreState提取出Bookmark,并从Bookmark的位置开始读取新的消息列表;
  • 根据WorkflowSessionLastCheckpoint确定针对Workflow的执行方式:
    • 如果LastCheckpoint不为null:
      • 采用流的方式从这个Checkpoint标识的为止开始执行,并得到对应的StreamingRun对象;
      • 遍历上一步得到的消息列表:
        • 如果消息包含的FunctionResultContent对象的CallId与内部维护的某个ExternalRequest相匹配,意味着这个FunctionResultContent对从Workflow发出的外部请求的响应,那么会利用其承载的内容创建针对ExternalRequestExternalResponse,并收集到一个集合中,我们将此集合表示为ExternalResponses
        • 其他AIContext常规内容并提取出来创建一个新的ChatMessage对象,添加到另一个集合中,我们将此消息集合表示为RegularMessages
      • 如果RegularMessages不为空,直接调用StreamingRunTrySendMessageAsync方法将整个消息列表发送出去;
      • 如果ExternalResponses不为空,依次调用StreamingRunSendResponseAsync方法将每个ExternalResponse发送出去;
    • 如果LastCheckpoint为null,直接将消息列表作为输入调用InprocessExecutionEnvironmentRunStreamingAsync方法来执行Workflow,并得到对应的StreamingRun对象。
  • 如果需要发送TurnToken(如果请求消息没有针对ExternalRequest的响应内容),就调用StreamingRunSendTurnTokenAsync方法发送一个新的TurnToken
  • 调用StreamingRunWatchStreamAsync方法监控事件流,并对不同的事件作相应处理。
    • 对于RequestInfoEvent,就将ExternalRequest提取出来,创建一个对应的FunctionCallContent对象,并封装成AgentResponseUpdateYield出去;
    • WorkflowOutputEvent:如果创建WorkflowHostAgent时指定了includeWorkflowOutputsInResponse参数为true,就将WorkflowOutputEvent中的输出的IEnumerable<ChatMessage>或者ChatMessage(只支持这两种类型的输出)封装成AgentResponseUpdateYield出去;
    • SuperStepCompletedEvent:将Superstep结束后创建的Checkpoint的信息保存到WorkflowSessionLastCheckpoint属性中;
    • 其他:包括异常处理等其他事件的处理。

3.3 AsAIAgent扩展方法

Workflow转换成AIAgent的扩展方法AsAIAgent的定义如下所示。它接受一个Workflow对象作为输入,并返回一个WorkflowHostAgent对象。

public static AIAgent AsAIAgent(
    this Workflow workflow, 
    string? id = null, 
    string? name = null, 
    string? description = null, 
    IWorkflowExecutionEnvironment? executionEnvironment = null, 
    bool includeExceptionDetails = false, 
    bool includeWorkflowOutputsInResponse = false)
    => new WorkflowHostAgent(workflow, id, name, description, executionEnvironment, 
        includeExceptionDetails, includeWorkflowOutputsInResponse);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值