涉及敏感操作的工具执行都需要引入基于人机交互HITL, Human-In-The-Loop的审批机制来确保安全。按照安全级别由高到低我们可以采用如下三种常见的审批模式只要工具调用被装饰了ApprovalRequiredAIFunction中间件就需要人工介入审批如果当前工具调用审批通过后者针对同一工具针对相同参数列表的调用可以免审批直接执行;如果当前工具调用审批通过后者针对同一工具的调用可以免审批直接执行不管参数列表是否相同。其中第一种为默认的审批模式虽然安全级别最高但在实际使用中会带来大量重复审批的烦恼尤其是当Agent需要频繁调用同一个工具时。第二种和第三种模式则通过引入记忆机制来减少重复审批的次数从而提升Agent的效率和用户体验。这种don’t ask again的审批需要借助ToolApprovalAgent中间件来实现。1. 基于don’t ask again的审批如下的程序演示了如何利用ToolApprovalAgent来实现基于don’t ask again的审批我们定义了一个转账工具TransferMoney它被ApprovalRequiredAIFunction装饰表示它需要人工审批。然后我们创建了一个OpenAIClient并通过AsAIAgent方法将其转换为一个Agent实例在这个过程中我们把转账工具注册到了Agent上。接着我们在Agent的构建器中调用了UseToolApproval方法来启用基于don’t ask again的审批机制。usingAzure;usingdotenv.net;usingMicrosoft.Agents.AI;usingMicrosoft.Extensions.AI;usingOpenAI;usingOpenAI.Responses;usingSystem.ComponentModel;DotEnv.Load();varmodelEnvironment.GetEnvironmentVariable(MODEL)!;varapiKeyEnvironment.GetEnvironmentVariable(API_KEY)!;varendpointEnvironment.GetEnvironmentVariable(OPENAI_URL)!;AIFunctiontransferAIFunctionFactory.Create(TransferMoney,nameof(TransferMoney));transfernewApprovalRequiredAIFunction(transfer);varagentnewOpenAIClient(credential:newAzureKeyCredential(apiKey),options:newOpenAIClientOptions{EndpointnewUri(endpoint)}).GetChatClient(model:model).AsIChatClient().AsAIAgent(tools:[transfer]).AsBuilder().UseToolApproval().Build();string?promptTemplatenull;varsessionawaitagent.CreateSessionAsync();while(true){Console.Write(请输入转账金额);stringpromptdefault!;if(promptTemplateisnull){promptTemplate再次请将{0}元从账户4242 4242 4242 4242转账到账户5555 5555 5555 4444, 请将转账金额均分为两部分执行两次转账操作;prompt$请将{Console.ReadLine()}元从账户4242 4242 4242 4242转账到账户5555 5555 5555 4444, 请将转账金额均分为两部分执行两次转账操作;}else{promptstring.Format(promptTemplate,Console.ReadLine());}varresponseawaitagent.RunAsync(prompt,session);varmessageresponse.Messages.Last();varapprovalRequestContentsmessage.Contents.OfTypeToolApprovalRequestContent();if(!approvalRequestContents.Any()){Console.WriteLine($\n{response}\n\n);continue;}foreach(varcontentinapprovalRequestContents){vartoolCall(FunctionCallContent)content.ToolCall;Console.WriteLine($待执行操作{toolCall.Name}需要你的审批 :);foreach(var(k,v)intoolCall.Arguments!){Console.WriteLine($ -{k}:{v});}Console.WriteLine();}Console.Write(-批准针对工具的本次调用并且在未来自动批准同样参数的调用请输入:1-批准针对工具的本次调用并且在未来自动批准同类调用不论参数请输入:2-拒绝针对工具的本次调用请输入:3你的审批决定:);vardecisionConsole.ReadLine()?.Trim();varapprovalResponsesapprovalRequestContents.Select(itdecisionswitch{1(AIContent)it.CreateAlwaysApproveToolWithArgumentsResponse(),2it.CreateAlwaysApproveToolResponse(),3it.CreateResponse(false),_thrownewInvalidOperationException(无效的输入)}).ToArray();responseawaitagent.RunAsync(newChatMessage(ChatRole.User,approvalResponses),session);Console.WriteLine($\n{response}\n\n);}[Description(银行转账)]staticstringTransferMoney([Description(转出账户)]stringfromAccount,[Description(转入账户)]stringtoAccount,[Description(转账金额)]decimalamount)$已成功将{amount}元从{fromAccount}转账到{toAccount};我们在一个循环中通过调用Agent多次在两个账户之间转账但是转账的金额由用户在控制台输入。我们通过提示词引导Agent将用户输入的金额均分为两部分来执行两次转账操作。我们通过响应消息是否携带ToolApprovalRequestContent来判断当前工具调用是否需要审批并根据用户的输入来生成不同的审批响应。并给用户提供三种审批选项批准针对工具的本次调用并且在未来自动批准同样参数的调用此时我们会调用扩展方法CreateAlwaysApproveToolWithArgumentsResponse方法来生成审批响应这会告诉Agent对于同样参数的调用都自动批准批准针对工具的本次调用并且在未来自动批准同类调用不论参数此时我们会调用扩展方法CreateAlwaysApproveToolResponse方法来生成审批响应这会告诉Agent对于同类调用都自动批准无论参数如何变化拒绝针对工具的本次调用此时我们会调用CreateResponse(false)方法来生成审批响应这会告诉Agent拒绝本次调用。我们来看看选择不同的审批模式时的效果。如下为选择第一种审批模式时的交互内容我们第一次输入转账金额100Agent会将其均分为两笔50元的转账来执行。但是ToolApprovalAgent只会提交给用户第一个审批请求。我们选择了第一个选项后两笔相同金额的转账会成功执行。请输入转账金额100 待执行操作TransferMoney需要你的审批 : - fromAccount: 4242 4242 4242 4242 - toAccount: 5555 5555 5555 4444 - amount: 50 - 批准针对工具的本次调用并且在未来自动批准同样参数的调用请输入:1 - 批准针对工具的本次调用并且在未来自动批准同类调用不论参数请输入:2 - 拒绝针对工具的本次调用请输入:3 你的审批决定:1 转账已完成以下是转账详情 | 项目 | 详情 | |------|------| | **转出账户** | 4242 4242 4242 4242 | | **转入账户** | 5555 5555 5555 4444 | | **总金额** | 100 元 | | **第一次转账** | 50 元 ✅ | | **第二次转账** | 50 元 ✅ | 两笔转账均已成功执行共计100元已从您的账户转入目标账户。 请输入转账金额100 转账再次完成以下是转账详情 | 项目 | 详情 | |------|------| | **转出账户** | 4242 4242 4242 4242 | | **转入账户** | 5555 5555 5555 4444 | | **总金额** | 100 元 | | **第一次转账** | 50 元 ✅ | | **第二次转账** | 50 元 ✅ | 两笔转账均已成功执行共计再次转账100元至目标账户。 请输入转账金额200 如下待执行工具需要你的审批 待执行操作TransferMoney需要你的审批 : - fromAccount: 4242 4242 4242 4242 - toAccount: 5555 5555 5555 4444 - amount: 100 - 批准针对工具的本次调用并且在未来自动批准同样参数的调用请输入:1 - 批准针对工具的本次调用并且在未来自动批准同类调用不论参数请输入:2 - 拒绝针对工具的本次调用请输入:3 你的审批决定:1 转账完成以下是本次转账详情 | 项目 | 详情 | |------|------| | **转出账户** | 4242 4242 4242 4242 | | **转入账户** | 5555 5555 5555 4444 | | **总金额** | 200 元 | | **第一次转账** | 100 元 ✅ | | **第二次转账** | 100 元 ✅ | 两笔各100元的转账均已成功执行共计200元已转入目标账户。接下来我们再次提交相同金额100的转账请求后Agent不会再提交审批请求而是直接执行转账操作了。由于我们选择的审批模式是针对相同参数的调用自动批准所以只有当我们提交完全相同参数的转账请求时才会免审批直接执行。如果我们提交了不同金额比如200的转账请求Agent还是会提交审批请求的。但是如何我们选择了第二种审批模式那么无论我们提交什么金额的转账请求Agent都会免审批直接执行了具体交互如下所示请输入转账金额100 待执行操作TransferMoney需要你的审批 : - fromAccount: 4242 4242 4242 4242 - toAccount: 5555 5555 5555 4444 - amount: 50 - 批准针对工具的本次调用并且在未来自动批准同样参数的调用请输入:1 - 批准针对工具的本次调用并且在未来自动批准同类调用不论参数请输入:2 - 拒绝针对工具的本次调用请输入:3 你的审批决定:2 转账已全部完成以下是转账摘要 | 项目 | 详情 | |------|------| | **转出账户** | 4242 4242 4242 4242 | | **转入账户** | 5555 5555 5555 4444 | | **转账总金额** | 100 元 | **两次转账明细** | 次数 | 金额 | 状态 | |------|------|------| | 第一次 | 50 元 | ✅ 成功 | | 第二次 | 50 元 | ✅ 成功 | 两次转账均已成功执行总共从账户 4242 4242 4242 4242 转出 100 元到账户 5555 5555 5555 4444。 请输入转账金额200 转账已全部完成以下是转账摘要 | 项目 | 详情 | |------|------| | **转出账户** | 4242 4242 4242 4242 | | **转入账户** | 5555 5555 5555 4444 | | **转账总金额** | 200 元 | **两次转账明细** | 次数 | 金额 | 状态 | |------|------|------| | 第一次 | 100 元 | ✅ 成功 | | 第二次 | 100 元 | ✅ 成功 | 两次转账均已成功执行总共从账户 4242 4242 4242 4242 转出 200 元到账户 5555 5555 5555 4444。2. AlwaysApproveToolApprovalResponseContent在完成了针对ToolApprovalAgent中间件的注册前提下两种基于don’t ask again的审批模式由用户根据审批请求内容创建的审批响应内容来决定。这个特殊的AIContent类型为AlwaysApproveToolApprovalResponseContent它包含了一个ToolApprovalResponseContent类型的属性InnerResponse以及两个布尔属性AlwaysApproveTool和AlwaysApproveToolWithArguments来分别表示两种审批模式。Agent会根据这两个属性的值来决定是否启用基于don’t ask again的审批机制。publicsealedclassAlwaysApproveToolApprovalResponseContent:AIContent{publicToolApprovalResponseContentInnerResponse{get;}publicboolAlwaysApproveTool{get;}publicboolAlwaysApproveToolWithArguments{get;}internalAlwaysApproveToolApprovalResponseContent(ToolApprovalResponseContentinnerResponse,boolalwaysApproveTool,boolalwaysApproveToolWithArguments);}AlwaysApproveToolApprovalResponseContent是对另一个ToolApprovalResponseContent的包装后者体现在它的InnerResponse属性上。该对象通过调用ToolApprovalRequestContent的CreateResponse方法来创建。由于真正实现工具审批机制的FunctionInvokingChatClient只认识ToolApprovalResponseContent类型的审批响应内容所以在ToolApprovalAgent向后传递的消息里表达审评响应的内容是被AlwaysApproveToolApprovalResponseContent包装的ToolApprovalResponseContent对象。由于它只包含一个internal构造函数所以我们需要按照演示实例那样通过调用ToolApprovalRequestContent类型如下两个扩展方法来创建针对不同审批模式的AlwaysApproveToolApprovalResponseContent对象。publicstaticclassToolApprovalRequestContentExtensions{publicstaticAlwaysApproveToolApprovalResponseContentCreateAlwaysApproveToolResponse(thisToolApprovalRequestContentrequest,string?reasonnull)newAlwaysApproveToolApprovalResponseContent(request.CreateResponse(approved:true,reason),alwaysApproveTool:true,alwaysApproveToolWithArguments:false);publicstaticAlwaysApproveToolApprovalResponseContentCreateAlwaysApproveToolWithArgumentsResponse(thisToolApprovalRequestContentrequest,string?reasonnull)newAlwaysApproveToolApprovalResponseContent(request.CreateResponse(approved:true,reason),alwaysApproveTool:false,alwaysApproveToolWithArguments:true);}3. 审批状态的维持ToolApprovalAgent中间件为了一个由ToolApprovalState对象承载的状态。ToolApprovalAgent利用_sessionState字段返回的ProviderSessionStateToolApprovalState来维护这ToolApprovalState这个状态对象它在Session中对应的键名为toolApprovalState。我们可以在构造函数中提供一个JsonSerializerOptions对象来指定ToolApprovalState对象来控制该状态的序列化。publicsealedclassToolApprovalAgent:DelegatingAIAgent{privatereadonlyProviderSessionStateToolApprovalState_sessionState;privatereadonlyJsonSerializerOptions_jsonSerializerOptions;publicToolApprovalAgent(AIAgentinnerAgent,JsonSerializerOptions?jsonSerializerOptionsnull):base(innerAgent){_jsonSerializerOptionsjsonSerializerOptions??AgentJsonUtilities.DefaultOptions;_sessionStatenewProviderSessionStateToolApprovalState(_newToolApprovalState(),toolApprovalState,_jsonSerializerOptions);}}ToolApprovalState类型定义如下它包含三个集合类型的状态成员internalsealedclassToolApprovalState{publicListToolApprovalRequestContentQueuedApprovalRequests{get;set;}newListToolApprovalRequestContent();publicListToolApprovalRuleRules{get;set;}newListToolApprovalRule();publicListToolApprovalResponseContentCollectedApprovalResponses{get;set;}newListToolApprovalResponseContent();}internalsealedclassToolApprovalRule{publicstringToolName{get;set;}string.Empty;publicIDictionarystring,string?Arguments{get;set;}}三个属性成员说明如下QueuedApprovalRequests: 对于ToolApprovalAgent中间件为了收集审批响应与用户的每次交互它只会提交一个审批请求给用户来审批其余的审批请求会存在这个ToolApprovalRequestContent集合表示的队列中Rules: 用来记录基于don’t ask again的审批规则每当用户创建一个AlwaysApproveToolApprovalResponseContent类型的审批响应时ToolApprovalAgent都会从中提取工具名称和参数信息来创建一个ToolApprovalRule对象并添加到Rules列表中以记录这个审批规则。队列中的审批请求和后续的审批请求都会与Rules列表中的规则进行比较来判断是否满足免审批条件CollectedApprovalResponses: 由于FunctionInvokingChatClient只认可ToolApprovalResponseContent所以任何类型的审批响应用户手工创建的或者ToolApprovalState自动创建的都需要被转换成ToolApprovalResponseContent类型的内容来提交给内层Agent继续执行。它的成员包含如下三个类别用于没有采用基于don’t ask again的审批模式直接调用扩展方法CreateAlwaysApproveToolResponse创建的ToolApprovalResponseContent对象用户针对提交的请求创建了了一个AlwaysApproveToolApprovalResponseContent其InnerResponse属性返回的ToolApprovalResponseContent对象审批请求满足Rules列表里记录的某条规则的免审批条件ToolApprovalAgent自动创建的ToolApprovalResponseContent对象4. ToolApprovalAgent从上面的给出的定义可以看出除了用来控制状态序列化的JsonSerializerOptions对象之外ToolApprovalAgent并不需要额外的配置选项。它针对don’t ask again审批模式实现在重写的RunCoreAsync和RunCoreStreamingAsync方法中。publicsealedclassToolApprovalAgent:DelegatingAIAgent{protectedoverrideasyncTaskAgentResponseRunCoreAsync(IEnumerableChatMessagemessages,AgentSession?sessionnull,AgentRunOptions?optionsnull,CancellationTokencancellationTokendefault);protectedoverrideasyncIAsyncEnumerableAgentResponseUpdateRunCoreStreamingAsync(IEnumerableChatMessagemessages,AgentSession?sessionnull,AgentRunOptions?optionsnull,CancellationTokencancellationTokendefault)}RunCoreAsync/RunCoreStreamingAsync方法执行流程大致如下从请求消息中提取表达审批回复的内容如果类型为ToolApprovalResponseContent直接将其添加到ToolApprovalState的CollectedApprovalResponses列表中如果类型为AlwaysApproveToolApprovalResponseContent提取其工具名称和参数列表来创建ToolApprovalRule对象并添加到ToolApprovalState的Rules列表中将其InnerResponse添加到ToolApprovalState的CollectedApprovalResponses列表中从ToolApprovalState的QueuedApprovalRequests队列中找到满足免审批条件的请求并创建对应的ToolApprovalResponseContent添加到CollectedApprovalResponses列表中如果ToolApprovalState的QueuedApprovalRequests队列里没有满足免审批条件的请求了则提取第一个ToolApprovalRequestContent封装成响应消息提交给用户继续审批。如此循环直至所有审批请求都具有对应的审批响应。提供ToolApprovalState的CollectedApprovalResponses封装成请求消息提交给内层Agent继续执行等待内层Agent的响应。如果响应消息中不包含ToolApprovalRequestContent类型的内容则直接将响应返回给上层调用者如果响应消息中包含ToolApprovalRequestContent类型的内容则继续按照上面的步骤处理这些审批请求形成一个循环直到所有审批请求都得到处理为止。