上一篇文章人机交互(HITL)的实现着重介绍了MAF的Workflow基于RequestPort的人机交互(HITL)的设计和实现细节,具体的做法就是在编排Workflow时引入RequestPortBinding节点,并使用它来发送外部请求和接收外部响应。如果我们在利用AIAgentBinding将某个AIAgent作为Workflow中的一个节点时,涉及敏感操作的工具审批又该如何实现呢?
1. 利用Agent完成转账并提供审批
先说结论,具体的解决方案有两种,一种是采用常规的方式:监听RequestInfoEvent,并从中提取描述工具审批请求的ToolApprovalRequestContent对象,然后利用生成对应的ToolApprovalResponseContent对象封装成ExternalResponse发送出去就可以了。
在人机交互(HITL)的实现演示的实例中,我们利用自定义的Executor实现应用转账的功能,现在我们换一种解法:我们在Workflow中引入一个AIAgent,并利用注册的工具来完成转账的功能,同时在工具中加入审批的逻辑。完整的程序如下所示:我们根据OpenAIClient创建了一个AIAgent对象,并注册了用于转账的工具函数Transfer,这个工具函数被包装成一个ApprovalRequiredAIFunction,它会在工具调用时触发一个审批请求。
using Microsoft.Agents.AI.Workflows;
using Microsoft.Extensions.AI;
using OpenAI;
using System.ClientModel;
using System.ComponentModel;
dotenv.net.DotEnv.Load();
var endpoint = Environment.GetEnvironmentVariable("OPENAI_URL")!;
var model = Environment.GetEnvironmentVariable("MODEL")!;
var apiKey = Environment.GetEnvironmentVariable("API_KEY")!;
var tool = AIFunctionFactory.Create(Transfer, nameof(Transfer));
ExecutorBinding agent = new OpenAIClient(
new ApiKeyCredential(apiKey),
new OpenAIClientOptions { Endpoint = new Uri(endpoint) })
.GetChatClient(model)
.AsIChatClient()
.AsAIAgent(tools: [new ApprovalRequiredAIFunction(tool)]);
ExecutorBinding outputMessages = new FunctionExecutor<IEnumerable<ChatMessage>>(
id: "OutputMessages",
outputTypes: [typeof(List<ChatMessage>)],
handlerAsync: async (input, context, cancellationToken) =>
{
await context.YieldOutputAsync(input);
});
var workflow = new WorkflowBuilder(agent)
.AddEdge(agent, outputMessages)
.WithOutputFrom(outputMessages)
.Build();
var run = await InProcessExecution.Default.RunStreamingAsync(workflow, new ChatMessage(ChatRole.User, "我想从账户123456转账1000元到账户654321"));
await run.TrySendMessageAsync(new TurnToken(emitEvents: false));
await foreach (var @event in run.WatchStreamAsync())
{
if (@event is RequestInfoEvent requestInfoEvent && requestInfoEvent.Request.TryGetDataAs<ToolApprovalRequestContent>(out var approvalRequest))
{
var toolCall = approvalRequest.ToolCall as FunctionCallContent;
if (toolCall is not null)
{
Console.WriteLine($"""
有一个工具调用请求需要审批,
工具名称:{toolCall.Name},
参数:
""");
foreach (var arg in toolCall.Arguments ?? new Dictionary<string, object?>())
{
Console.WriteLine($" {arg.Key}:{arg.Value}");
}
Console.Write("是否批准?(y/n) ");
var approved = Console.ReadLine()?.Trim().ToLower() == "y";
var response = approvalRequest.CreateResponse(approved);
await run.SendResponseAsync(requestInfoEvent.Request.CreateResponse(response));
}
}
else if (@event is WorkflowOutputEvent outputEvent)
{
var message = (outputEvent.Data as IEnumerable<ChatMessage>)?.LastOrDefault();
if (message?.Role == ChatRole.Assistant)
{
Console.WriteLine(message);
}
}
}
[Description("银行转账")]
static string Transfer(
[Description("转出银行账户")] string from,
[Description("转入银行账户")] string to,
[Description("转账金额")] decimal ammount)
=> $"从账户{from}转出{ammount}元到{to}账户!";
构建的Workflow除了基于这个AIAgent构建的AIAgentBinding节点外,还包含一个FunctionExecutor<IEnumerable<ChatMessage>>用来输出最终的消息列表。我们以流的方式调用了RunStreamingAsync方法来执行Workflow,并监听返回的事件流。由于流式调用需要显式发送一个TurnToken来触发Agent节点的执行,所以我们随后调用StreamingRun的TrySendMessageAsync方法来发送一个空的TurnToken。在随后针对事件流的监听中,我们主要关注两类事件:
- RequestInfoEvent:我们从中提取
ToolApprovalRequestContent对象并将待审批的工具名称和参数打印出来,然后等待用户输入审批结果,并根据审批结果创建一个ToolApprovalResponseContent对象,最终通过调用代表当前外部请求的ExternalRequest的CreateResponse方法将其封装成ExternalResponse。这个携带了用户审批决定的ExternalResponse最终通过调用StreamingRun的SendResponseAsync方法发送回Workflow,以触发Workflow中等待审批结果的节点继续执行; - WorkflowOutputEvent:我们从中提取最终的消息列表,并将最后一个
Assistant消息的内容打印出来;
程序运行后,会显示转账工具的审批请求,我们可以输入“y”来批准这个工具调用请求,或者输入“n”来拒绝这个工具调用请求。如下两段内容为对应的输出:
有一个工具调用请求需要审批,
工具名称:Transfer,
参数:
from:123456
to:654321
ammount:1000
是否批准?(y/n) y
好的,我来为您完成这笔转账。
转账已成功完成!以下是转账详情:
- **转出账户**:123456
- **转入账户**:654321
- **转账金额**:1,000 元
资金已从账户 **123456** 转入账户 **654321**,如需查询余额或进行其他操作,请随时告诉我。
有一个工具调用请求需要审批,
工具名称:Transfer,
参数:
from:123456
to:654321
ammount:1000
是否批准?(y/n) n
好的,我来为您处理这笔转账。
抱歉,转账操作被拒绝了。这可能由以下原因导致:
- **账户余额不足**:账户 `123456` 中的余额可能不足 1000 元。
- **账户状态异常**:转出或转入账户可能被冻结、锁定或存在其他限制。
- **权限问题**:当前可能没有足够的权限执行此转账操作。
- **风控拦截**:系统可能因安全策略拦截了该笔交易。
建议您检查账户状态和余额,或联系银行客服进一步了解拒绝原因。如果您需要我帮忙处理其他事项,请随时告诉我。
2. 实现原理
基于Agent工具审批的人机交互实现在AIAgentBinding创建的AIAgentHostExecutor中。如果AIAgentHostOptions的InterceptUserInputRequests配置选项没有被显式设置为true,AIAgentHostExecutor在内部会自动注册一个RequestPort。在调用AIAgent并得到作为响应消息的ChatMessage列表后,会从该消息的内容列表中提取所有的ToolApprovalRequestContent对象,如果某个没有对应的ToolApprovalResponseContent对象,意味着这是一个待审批的工具调用请求。此时它会将这个ToolApprovalRequestContent对象封装成一个ExternalRequest发送给RequestPort对应的IExternalRequestSink对象,后者将其转换成RequestInfoEvent并发布出来。
当我们监听到这个RequestInfoEvent时,就可以按照实例演示的方式从中提取ToolApprovalRequestContent对象来获取工具调用请求的详情,并根据用户的输入来创建ToolApprovalResponseContent对象,最终将其封装成ExternalResponse发送回Workflow,以触发等待审批结果的节点继续执行。
3. 自行处理审批请求
上述的这一切关于AIAgentBinding自动处理工具执行审批的流程具有一个基本的前提,那就是AIAgentHostOptions的InterceptUserInputRequests配置选项没有被显式设置为true。如果开启了这个开关,意味着我们通过拦截审批请求,自行完成审批流程。如果是这样,我们可以采用如下的方式处理审批请求:
using Microsoft.Agents.AI.Workflows;
using Microsoft.Extensions.AI;
using OpenAI;
using System.ClientModel;
using System.ComponentModel;
dotenv.net.DotEnv.Load();
var endpoint = Environment.GetEnvironmentVariable("OPENAI_URL")!;
var model = Environment.GetEnvironmentVariable("MODEL")!;
var apiKey = Environment.GetEnvironmentVariable("API_KEY")!;
var tool = AIFunctionFactory.Create(Transfer, nameof(Transfer));
ExecutorBinding agent = new OpenAIClient(
new ApiKeyCredential(apiKey),
new OpenAIClientOptions { Endpoint = new Uri(endpoint) })
.GetChatClient(model)
.AsIChatClient()
.AsAIAgent(tools: [new ApprovalRequiredAIFunction(tool)])
.BindAsExecutor2(new AIAgentHostOptions{ InterceptUserInputRequests = true });
ExecutorBinding agentResponseHandler = new FunctionExecutor<IEnumerable<ChatMessage>>(
id: "OutputMessages",
outputTypes: [typeof(List<ChatMessage>)],
sentMessageTypes: [typeof(ChatMessage),typeof(TurnToken)],
handlerAsync: HandleAgentResponseAsync);
var workflow = new WorkflowBuilder(agent)
.AddEdge(agent, agentResponseHandler)
.AddEdge(agentResponseHandler, agent, condition: (object? input) => input is not null)
.WithOutputFrom(agentResponseHandler)
.Build();
var run = await InProcessExecution.Default.RunAsync(
workflow, "我想从账户123456转账1000元到账户654321");
var messages = run.NewEvents.OfType<WorkflowOutputEvent>().Last().Data
as IEnumerable<ChatMessage>;
Console.WriteLine(messages?.LastOrDefault());
static async ValueTask HandleAgentResponseAsync(
IEnumerable<ChatMessage> response,
IWorkflowContext context,
CancellationToken cancellationToken)
{
var approvalRequests = response.SelectMany(it => it.Contents.OfType<ToolApprovalRequestContent>());
if (!approvalRequests.Any())
{
await context.YieldOutputAsync(response);
return;
}
Console.WriteLine("如下工具调用请求需要审批:");
foreach (var request in approvalRequests)
{
var functionCall = (FunctionCallContent)request.ToolCall!;
Console.WriteLine($"工具名称:{functionCall.Name}");
foreach (var arg in functionCall.Arguments ?? new Dictionary<string, object?>())
{
Console.WriteLine($" {arg.Key}:{arg.Value}");
}
}
Console.Write("是否批准?(y/n) ");
var approved = Console.ReadLine()?.Trim().ToLower() == "y";
var responses = approvalRequests.Select(request => request.CreateResponse(approved));
var message = new ChatMessage(ChatRole.User, responses.Cast<AIContent>().ToList());
await context.SendMessageAsync(message);
await context.SendMessageAsync(new TurnToken());
}
[Description("银行转账")]
static string Transfer(
[Description("转出银行账户")] string from,
[Description("转入银行账户")] string to,
[Description("转账金额")] decimal ammount)
=> $"从账户{from}转出{ammount}元到{to}账户!";
如上面的代码所示,我们在AIAgentBinding后面添加了FunctionExecutor<IEnumerable<ChatMessage>>类型的节点来处理AIAgent返回的响应消息列表。在具体的处理方法HandleAgentResponseAsync中,我们从响应消息列表中提取所有的ToolApprovalRequestContent对象。如果没有,说明当前响应消息列表中没有工具调用审批请求,我们就直接将这个响应消息列表作为输出发送出去。反之我们就打印出工具调用请求的详情,并等待用户输入审批结果。根据用户的输入,我们创建ToolApprovalResponseContent对象,并将其封装成一个ChatMessage发送回AIAgentBinding节点,以触发工具调用请求的审批流程继续执行。
由于AIAgentHostExecutor针对ChatMessage的累积特性,在发送了包含审批响应的ChatMessage后,还需要再发送一个TurnToken来触发Agent节点继续执行。在创建FunctionExecutor<IEnumerable<ChatMessage>>时,我们还需要将这两个类型借助sentMessageTypes参数进行显式声明。在编排工作流程时,我们在这两个节点之间添加了如下两条边:
- 从
AIAgentBinding节点到FunctionExecutor节点的边用来传递AIAgent的响应消息列表,这是一条无条件的静态边; - 从
FunctionExecutor节点到AIAgentBinding节点的边用来传递用户的审批响应消息,这条边的条件是输入不为null,确保只有在用户有审批响应时才会触发AIAgent继续执行;
运行程序后,控制台依然会输出转账工具的审批请求,我们可以输入“y”来批准这个工具调用请求,或者输入“n”来拒绝这个工具调用请求。输出结果和之前类似:
如下工具调用请求需要审批:
工具名称:Transfer
from:123456
to:654321
ammount:1000
是否批准?(y/n) y
转账已成功完成!以下是为您办理的转账详情:
- **转出账户**:123456
- **转入账户**:654321
- **转账金额**:1000 元
款项已从账户 123456 转出并到达账户 654321。如果您还需要办理其他业务,请随时告诉我!
如下工具调用请求需要审批:
工具名称:Transfer
from:123456
to:654321
ammount:1000
是否批准?(y/n) n
看起来转账请求被拒绝了。这可能是因为以下几种原因:
1. **账户余额不足** — 账户 123456 的余额可能不足 1000 元。
2. **账户信息有误** — 转入或转出账户可能不存在或已被冻结。
3. **转账限额** — 可能超出了单笔或每日转账限额。
4. **权限问题** — 可能需要额外的授权或验证。
请问您需要我帮您核查具体原因吗?或者您是否有其他账户想要尝试转账?


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



