有时,我们可能构建了一个包含多个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具有如下的结构:
整个Worflow由如下四个Executor组成:
- TransactionExtractor:调用LLM来从用户输入的消息中提取转账信息,转换成一个代表转账业务的
TransferTransaction对象; - RiskCheck:风险评估,如果转账金额>10000就认为是高风险交易,需要人工审批,否则直接执行;
- Approval:一个
RequestPort类型的Executor,对外发送审批请求,并等待审批结果; - MoneyTransfer:执行转账操作。
这个转账流程涉及如下两个数据类型,分别是代表转账业务的TransferTransaction对象和代表转账输入的TransferInput对象。TransferTransaction由TransactionExtractor提取的结构化输出经过反序列化生成,IsValid属性表示转账信息是否合法。TransferInput的Transaction属性保存了转账信息,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属性确定转账信息不符合要求,我们就通过调用IWorkflowContext的YieldOutputAsync方法来输出一条消息并提示用户重新输入。
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>对象来维护此状态。ProvideChatHistoryAsync和StoreChatHistoryAsync方法针对对话历史的读写都是借助于此对象来完成的。
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重写了AIAgent的RunCoreAsync和RunCoreStreamingAsync方法来实现了针对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; - 利用
WorkflowSession的ChatHistoryProvider将指定的消息列表添加到对话历史中; - 从存储的
StoreState提取出Bookmark,并从Bookmark的位置开始读取新的消息列表; - 根据
WorkflowSession的LastCheckpoint确定针对Workflow的执行方式:- 如果
LastCheckpoint不为null:- 采用流的方式从这个Checkpoint标识的为止开始执行,并得到对应的
StreamingRun对象; - 遍历上一步得到的消息列表:
- 如果消息包含的
FunctionResultContent对象的CallId与内部维护的某个ExternalRequest相匹配,意味着这个FunctionResultContent对从Workflow发出的外部请求的响应,那么会利用其承载的内容创建针对ExternalRequest的ExternalResponse,并收集到一个集合中,我们将此集合表示为ExternalResponses; - 其他
AIContext常规内容并提取出来创建一个新的ChatMessage对象,添加到另一个集合中,我们将此消息集合表示为RegularMessages
- 如果消息包含的
- 如果
RegularMessages不为空,直接调用StreamingRun的TrySendMessageAsync方法将整个消息列表发送出去; - 如果
ExternalResponses不为空,依次调用StreamingRun的SendResponseAsync方法将每个ExternalResponse发送出去;
- 采用流的方式从这个Checkpoint标识的为止开始执行,并得到对应的
- 如果
LastCheckpoint为null,直接将消息列表作为输入调用InprocessExecutionEnvironment的RunStreamingAsync方法来执行Workflow,并得到对应的StreamingRun对象。
- 如果
- 如果需要发送
TurnToken(如果请求消息没有针对ExternalRequest的响应内容),就调用StreamingRun的SendTurnTokenAsync方法发送一个新的TurnToken; - 调用
StreamingRun的WatchStreamAsync方法监控事件流,并对不同的事件作相应处理。- 对于
RequestInfoEvent,就将ExternalRequest提取出来,创建一个对应的FunctionCallContent对象,并封装成AgentResponseUpdate被Yield出去; WorkflowOutputEvent:如果创建WorkflowHostAgent时指定了includeWorkflowOutputsInResponse参数为true,就将WorkflowOutputEvent中的输出的IEnumerable<ChatMessage>或者ChatMessage(只支持这两种类型的输出)封装成AgentResponseUpdate被Yield出去;SuperStepCompletedEvent:将Superstep结束后创建的Checkpoint的信息保存到WorkflowSession的LastCheckpoint属性中;- 其他:包括异常处理等其他事件的处理。
- 对于
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);


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



