上一篇文章人机交互HITL的实现着重介绍了MAF的Workflow基于RequestPort的人机交互HITL的设计和实现细节具体的做法就是在编排Workflow时引入RequestPortBinding节点并使用它来发送外部请求和接收外部响应。如果我们在利用AIAgentBinding将某个AIAgent作为Workflow中的一个节点时涉及敏感操作的工具审批又该如何实现呢1. 利用Agent完成转账并提供审批先说结论具体的解决方案有两种一种是采用常规的方式监听RequestInfoEvent并从中提取描述工具审批请求的ToolApprovalRequestContent对象然后利用生成对应的ToolApprovalResponseContent对象封装成ExternalResponse发送出去就可以了。另一种我们稍后再讲。在人机交互HITL的实现演示的实例中我们利用自定义的Executor实现应用转账的功能现在我们换一种解法我们在Workflow中引入一个AIAgent并利用注册的工具来完成转账的功能同时在工具中加入审批的逻辑。完整的程序如下所示我们根据OpenAIClient创建了一个AIAgent对象并注册了用于转账的工具函数Transfer这个工具函数被包装成一个ApprovalRequiredAIFunction它会在工具调用时触发一个审批请求。usingMicrosoft.Agents.AI.Workflows;usingMicrosoft.Extensions.AI;usingOpenAI;usingSystem.ClientModel;usingSystem.ComponentModel;dotenv.net.DotEnv.Load();varendpointEnvironment.GetEnvironmentVariable(OPENAI_URL)!;varmodelEnvironment.GetEnvironmentVariable(MODEL)!;varapiKeyEnvironment.GetEnvironmentVariable(API_KEY)!;vartoolAIFunctionFactory.Create(Transfer,nameof(Transfer));ExecutorBindingagentnewOpenAIClient(newApiKeyCredential(apiKey),newOpenAIClientOptions{EndpointnewUri(endpoint)}).GetChatClient(model).AsIChatClient().AsAIAgent(tools:[newApprovalRequiredAIFunction(tool)]);ExecutorBindingoutputMessagesnewFunctionExecutorIEnumerableChatMessage(id:OutputMessages,outputTypes:[typeof(ListChatMessage)],handlerAsync:async(input,context,cancellationToken){awaitcontext.YieldOutputAsync(input);});varworkflownewWorkflowBuilder(agent).AddEdge(agent,outputMessages).WithOutputFrom(outputMessages).Build();varrunawaitInProcessExecution.Default.RunStreamingAsync(workflow,newChatMessage(ChatRole.User,我想从账户123456转账1000元到账户654321));awaitrun.TrySendMessageAsync(newTurnToken(emitEvents:false));awaitforeach(vareventinrun.WatchStreamAsync()){if(eventisRequestInfoEventrequestInfoEventrequestInfoEvent.Request.TryGetDataAsToolApprovalRequestContent(outvarapprovalRequest)){vartoolCallapprovalRequest.ToolCallasFunctionCallContent;if(toolCallisnotnull){Console.WriteLine($ 有一个工具调用请求需要审批 工具名称{toolCall.Name} 参数);foreach(varargintoolCall.Arguments??newDictionarystring,object?()){Console.WriteLine(${arg.Key}:{arg.Value});}Console.Write(是否批准(y/n) );varapprovedConsole.ReadLine()?.Trim().ToLower()y;varresponseapprovalRequest.CreateResponse(approved);awaitrun.SendResponseAsync(requestInfoEvent.Request.CreateResponse(response));}}elseif(eventisWorkflowOutputEventoutputEvent){varmessage(outputEvent.DataasIEnumerableChatMessage)?.LastOrDefault();if(message?.RoleChatRole.Assistant){Console.WriteLine(message);}}}[Description(银行转账)]staticstringTransfer([Description(转出银行账户)]stringfrom,[Description(转入银行账户)]stringto,[Description(转账金额)]decimalammount)$从账户{from}转出{ammount}元到{to}账户;构建的Workflow除了基于这个AIAgent构建的AIAgentBinding节点外还包含一个FunctionExecutorIEnumerableChatMessage用来输出最终的消息列表。我们以流的方式调用了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配置选项没有被显式设置为trueAIAgentHostExecutor在内部会自动注册一个RequestPort。在调用AIAgent并得到作为响应消息的ChatMessage列表后会从该消息的内容列表中提取所有的ToolApprovalRequestContent对象如果某个没有对应的ToolApprovalResponseContent对象意味着这是一个待审批的工具调用请求。此时它会将这个ToolApprovalRequestContent对象封装成一个ExternalRequest发送给RequestPort对应的IExternalRequestSink对象后者将其转换成RequestInfoEvent并发布出来。当我们监听到这个RequestInfoEvent时就可以按照实例演示的方式从中提取ToolApprovalRequestContent对象来获取工具调用请求的详情并根据用户的输入来创建ToolApprovalResponseContent对象最终将其封装成ExternalResponse发送回Workflow以触发等待审批结果的节点继续执行。3. 自行处理审批请求上述的这一切关于AIAgentBinding自动处理工具执行审批的流程具有一个基本的前提那就是AIAgentHostOptions的InterceptUserInputRequests配置选项没有被显式设置为true。如果开启了这个开关意味着我们通过拦截审批请求自行完成审批流程。如果是这样我们可以采用如下的方式处理审批请求usingMicrosoft.Agents.AI.Workflows;usingMicrosoft.Extensions.AI;usingOpenAI;usingSystem.ClientModel;usingSystem.ComponentModel;dotenv.net.DotEnv.Load();varendpointEnvironment.GetEnvironmentVariable(OPENAI_URL)!;varmodelEnvironment.GetEnvironmentVariable(MODEL)!;varapiKeyEnvironment.GetEnvironmentVariable(API_KEY)!;vartoolAIFunctionFactory.Create(Transfer,nameof(Transfer));ExecutorBindingagentnewOpenAIClient(newApiKeyCredential(apiKey),newOpenAIClientOptions{EndpointnewUri(endpoint)}).GetChatClient(model).AsIChatClient().AsAIAgent(tools:[newApprovalRequiredAIFunction(tool)]).BindAsExecutor2(newAIAgentHostOptions{InterceptUserInputRequeststrue});ExecutorBindingagentResponseHandlernewFunctionExecutorIEnumerableChatMessage(id:OutputMessages,outputTypes:[typeof(ListChatMessage)],sentMessageTypes:[typeof(ChatMessage),typeof(TurnToken)],handlerAsync:HandleAgentResponseAsync);varworkflownewWorkflowBuilder(agent).AddEdge(agent,agentResponseHandler).AddEdge(agentResponseHandler,agent,condition:(object?input)inputisnotnull).WithOutputFrom(agentResponseHandler).Build();varrunawaitInProcessExecution.Default.RunAsync(workflow,我想从账户123456转账1000元到账户654321);varmessagesrun.NewEvents.OfTypeWorkflowOutputEvent().Last().DataasIEnumerableChatMessage;Console.WriteLine(messages?.LastOrDefault());staticasyncValueTaskHandleAgentResponseAsync(IEnumerableChatMessageresponse,IWorkflowContextcontext,CancellationTokencancellationToken){varapprovalRequestsresponse.SelectMany(itit.Contents.OfTypeToolApprovalRequestContent());if(!approvalRequests.Any()){awaitcontext.YieldOutputAsync(response);return;}Console.WriteLine(如下工具调用请求需要审批);foreach(varrequestinapprovalRequests){varfunctionCall(FunctionCallContent)request.ToolCall!;Console.WriteLine($工具名称{functionCall.Name});foreach(vararginfunctionCall.Arguments??newDictionarystring,object?()){Console.WriteLine(${arg.Key}:{arg.Value});}}Console.Write(是否批准(y/n) );varapprovedConsole.ReadLine()?.Trim().ToLower()y;varresponsesapprovalRequests.Select(requestrequest.CreateResponse(approved));varmessagenewChatMessage(ChatRole.User,responses.CastAIContent().ToList());awaitcontext.SendMessageAsync(message);awaitcontext.SendMessageAsync(newTurnToken());}[Description(银行转账)]staticstringTransfer([Description(转出银行账户)]stringfrom,[Description(转入银行账户)]stringto,[Description(转账金额)]decimalammount)$从账户{from}转出{ammount}元到{to}账户;如上面的代码所示我们在AIAgentBinding后面添加了FunctionExecutorIEnumerableChatMessage类型的节点来处理AIAgent返回的响应消息列表。在具体的处理方法HandleAgentResponseAsync中我们从响应消息列表中提取所有的ToolApprovalRequestContent对象。如果没有说明当前响应消息列表中没有工具调用审批请求我们就直接将这个响应消息列表作为输出发送出去。反之我们就打印出工具调用请求的详情并等待用户输入审批结果。根据用户的输入我们创建ToolApprovalResponseContent对象并将其封装成一个ChatMessage发送回AIAgentBinding节点以触发工具调用请求的审批流程继续执行。由于AIAgentHostExecutor针对ChatMessage的累积特性在发送了包含审批响应的ChatMessage后还需要再发送一个TurnToken来触发Agent节点继续执行。在创建FunctionExecutorIEnumerableChatMessage时我们还需要将这两个类型借助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. **权限问题** — 可能需要额外的授权或验证。 请问您需要我帮您核查具体原因吗或者您是否有其他账户想要尝试转账