详情

首页手游攻略 MAF 入门第六期:人工审核(HITL)

MAF 入门第六期:人工审核(HITL)

佚名 2026-07-23 08:58:01

MAF 入门(6):人工审核(HITL)——敏感工具调用前等待确认

写在前面

书接上回,Agent 能查订单了,但退款这种操作,你敢让它全自动吗?错了没法撤销。

HITL(Human-in-the-Loop) 就是在敏感工具执行前加一道人工确认:Agent 该查的自己查,该退钱的时候先问你,你同意了再动账。

这篇 Demo 的场景很日常——客户说包装坏了要退款。用户用口语提诉求,Agent 调工具查单,再根据金额决定走小额直退还是大额审批。一条对话线,一个 Agent,三个工具。

一、场景

1.1 用户需要退款

客户:

这句话原样交给 Agent,后面的查单、判金额、选退款工具,由大模型 + Function Tool 完成。

1.2 两笔订单,两种结果

订单金额Agent 会调用的工具人工审批
ORD-001¥99ExecuteSmallRefund不需要
ORD-8842¥299ExecuteLargeRefund控制台输入 y/n

金额分界 ¥200,写在工具 Description 和 System Instructions 里。由大模型查单后判断该走哪条退款路径。

1.3 三个工具

LookupOrder查单,只读ExecuteSmallRefund 小额退款,直接执行ExecuteLargeRefund 大额退款,需 ApprovalRequiredAIFunction 包装

1.4 流程

用户口语诉求→ Agent 调 LookupOrder→ Agent 根据金额选退款工具 ├─ 小额 → ExecuteSmallRefund → [系统] 退款已实际执行 └─ 大额 → ExecuteLargeRefund→ ToolApprovalRequestContent→ 控制台 y/n→ 批准后才执行 DoRefund

二、完整代码

2.1 Hitl/RefundTools.cs

using System.ComponentModel;namespace MafDemo.Hitl;/// <summary>退款 Demo:查单 + 小额直退 + 大额审批退。</summary>public static class RefundTools{public const decimal LargeRefundThreshold = 200m;/// <summary>退款工具是否真正执行过(区分 Agent 口头承诺)。</summary>public static bool RefundActuallyExecuted { get; private set; }public static void ResetExecutionFlag() => RefundActuallyExecuted = false;[Description("根据订单号查询金额、签收状态、商品信息")]public static string LookupOrder([Description("订单号,如 ORD-8842")] string orderId){return orderId.Trim().ToUpperInvariant() switch{"ORD-8842" => "ORD-8842 | 金额=299 | 状态=已签收 | 商品=.NET AI 课程礼盒 | 备注=外盒压损","ORD-001" => "ORD-001 | 金额=99 | 状态=已签收 | 商品=贴纸 | 备注=包装破损",_ => $"未找到订单 {orderId}(模拟数据)",};}[Description("小额退款(金额严格小于 200 元),直接办理,无需人工审批")]public static string ExecuteSmallRefund([Description("订单号")] string orderId,[Description("退款金额,须与查单结果一致")] decimal amount)=> DoRefund(orderId, amount);[Description("大额退款(金额大于等于 200 元),需人工审批通过后才执行")]public static string ExecuteLargeRefund([Description("订单号")] string orderId,[Description("退款金额,须与查单结果一致")] decimal amount)=> DoRefund(orderId, amount);private static string DoRefund(string orderId, decimal amount){RefundActuallyExecuted = true;string id = orderId.Trim().ToUpperInvariant();Console.WriteLine($"[系统] 退款已实际执行 → {id} ¥{amount:0.##}");return $"[已退款] {id} 退回 ¥{amount:0.##},预计 1-3 个工作日到账。";}}

2.2 AgentHitlDemo.cs

using MafDemo.Configuration;using MafDemo.Hitl;using Microsoft.Agents.AI;using Microsoft.Extensions.AI;using Microsoft.Extensions.Configuration;namespace MafDemo;/// <summary>/// 第⑥篇 Demo:用户口语申请退款 → Agent 查单 → Agent 判大小额 → 小额直退 / 大额 HITL。/// </summary>public static class AgentHitlDemo{private const string SystemInstructions ="""你是电商客服退款助手。处理流程:1. 必须先调用 LookupOrder 查询订单,不要编造数据。2. 根据查单金额选择工具(禁止只用文字说「已提交审批」而不调工具): - 金额小于 200 元 → 必须调用 ExecuteSmallRefund - 金额大于等于 200 元 → 必须调用 ExecuteLargeRefund3. 退款金额须与查单结果一致。4. 若大额退款审批被拒绝或未执行,必须明确告知「退款未办理」。用简洁中文回复客户。""";public static async Task RunAsync(IConfiguration configuration,string? orderArg = null,CancellationToken cancellationToken = default){var llm = configuration.GetSection(LlmSettings.SectionName).Get<LlmSettings>()?? throw new InvalidOperationException($"缺少配置节 {LlmSettings.SectionName}");if (string.IsNullOrWhiteSpace(llm.ApiKey)){Console.WriteLine("请在 appsettings.Development.json 或环境变量 Llm__ApiKey 中配置 ApiKey。");return;}string orderId = orderArg?.Trim().ToUpperInvariant() switch{"small" or "ord-001" => "ORD-001","large" or "ord-8842" => "ORD-8842",null or "" => "ORD-8842",_ => orderArg!.Trim().ToUpperInvariant(),};string userMessage =$"你好,我要退订单 {orderId},收到的货包装坏了,请帮我查一下并办理退款。";IChatClient chatClient = ChatClientFactory.Create(llm);RefundTools.ResetExecutionFlag();AIAgent agent = chatClient.AsAIAgent(instructions: SystemInstructions,name: "RefundAssistant",tools:[AIFunctionFactory.Create(RefundTools.LookupOrder),AIFunctionFactory.Create(RefundTools.ExecuteSmallRefund),new ApprovalRequiredAIFunction(AIFunctionFactory.Create(RefundTools.ExecuteLargeRefund)),]);Console.WriteLine("=== MAF HITL Demo:包装损坏退款 ===");Console.WriteLine($"Provider: {llm.Provider} | Model: {llm.Model}");Console.WriteLine($"大额阈值: ¥{RefundTools.LargeRefundThreshold}(Agent 查单后自行判断)");Console.WriteLine("用法: dotnet run -- hitl [ORD-8842|ORD-001|small|large]");Console.WriteLine();Console.WriteLine($"用户: {userMessage}");AgentSession session = await agent.CreateSessionAsync(cancellationToken);await RunWithApprovalLoopAsync(agent, userMessage, session, cancellationToken);if (!RefundTools.RefundActuallyExecuted){Console.WriteLine("[系统] 本次未实际执行退款工具。");}Console.WriteLine();}private static async Task RunWithApprovalLoopAsync(AIAgent agent,string userMessage,AgentSession session,CancellationToken cancellationToken){IEnumerable<ChatMessage> input = [new ChatMessage(ChatRole.User, userMessage)];bool? lastApproved = null;while (true){Console.Write("助手: ");List<AgentResponseUpdate> updates = await CollectStreamingUpdatesAsync(agent, input, session, cancellationToken);Console.WriteLine();List<ToolApprovalRequestContent> approvals = ExtractApprovalRequests(updates);if (approvals.Count == 0){break;}var approvalMessages = new List<ChatMessage>();foreach (ToolApprovalRequestContent req in approvals){lastApproved = PromptApproval(req);approvalMessages.Add(new ChatMessage(ChatRole.User, [req.CreateResponse(approved: lastApproved.Value)]));}input = approvalMessages;}PrintExecutionSummary(lastApproved);}private static async Task<List<AgentResponseUpdate>> CollectStreamingUpdatesAsync(AIAgent agent,IEnumerable<ChatMessage> input,AgentSession session,CancellationToken cancellationToken){var updates = new List<AgentResponseUpdate>();await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(input, session, cancellationToken: cancellationToken)){updates.Add(update);if (update.Text is { Length: > 0 } text){Console.Write(text);}}return updates;}private static List<ToolApprovalRequestContent> ExtractApprovalRequests(IEnumerable<AgentResponseUpdate> updates) =>updates.SelectMany(u => u.Contents).OfType<ToolApprovalRequestContent>().DistinctBy(r => r.ToolCall.CallId).ToList();private static bool PromptApproval(ToolApprovalRequestContent req){var call = req.ToolCall as FunctionCallContent;Console.WriteLine();Console.WriteLine("⚠ 大额退款待审批");Console.WriteLine($"工具: {call?.Name ?? "(unknown)"}");Console.WriteLine($"参数: {call?.Arguments}");Console.Write("允许执行? (y/n): ");bool approved = Console.ReadLine()?.Trim().ToLowerInvariant() is "y" or "yes";Console.WriteLine(approved ? "→ 已批准" : "→ 已拒绝");return approved;}private static void PrintExecutionSummary(bool? approved){if (approved == false){Console.WriteLine(RefundTools.RefundActuallyExecuted? "[系统] ⚠ 异常:已拒绝但退款工具仍被执行": "[系统] ✓ 已拒绝,退款工具未执行(上方助手回复仅供参考)");}else if (RefundTools.RefundActuallyExecuted){Console.WriteLine("[系统] ✓ 退款工具已实际执行");}}}

三、代码解析

3.1 注册 Agent 与工具

用户消息是自然语言:

string userMessage =$"你好,我要退订单 {orderId},收到的货包装坏了,请帮我查一下并办理退款。";

三个工具里,只有大额退款包了审批:

tools:[AIFunctionFactory.Create(RefundTools.LookupOrder),AIFunctionFactory.Create(RefundTools.ExecuteSmallRefund),new ApprovalRequiredAIFunction(AIFunctionFactory.Create(RefundTools.ExecuteLargeRefund)),]

ApprovalRequiredAIFunction 是 HITL 的开关:模型想调 ExecuteLargeRefund 时,框架不直接执行 C# 方法,而是抛出 ToolApprovalRequestContent,等你回复 CreateResponse(approved: true/false)

System Instructions 规定流程:先 LookupOrder,再按金额选 ExecuteSmallRefundExecuteLargeRefund

3.2 为什么拆两个退款工具

HITL 绑定在单个工具上。小额要直退、大额要审批,所以拆成两个入口,底层共用 DoRefund

  • ExecuteSmallRefund — 普通 AIFunction,直接执行
  • ExecuteLargeRefund — 包 ApprovalRequiredAIFunction,等人批

大模型通过工具 Description(「小于 200」「大于等于 200」)和 Instructions 学会该调哪个。

RefundActuallyExecuted 和日志 [系统] 退款已实际执行 标记工具是否真的跑过,用来和 Agent 的自然语言回复区分。

3.3 审批循环 RunWithApprovalLoopAsync

① RunStreamingAsync(用户消息 或 审批回复)② 从 update.Contents 提取 ToolApprovalRequestContent③ 无审批 → 结束④ 有审批 → 问 y/n → CreateResponse 作为下一轮 input → 回到 ①

一轮对话里 Agent 可能连续调多个工具(先查单、再退款),审批请求出现在流式 update.Contents 中,因此用 RunStreamingAsync 收集 updates,再用 DistinctBy(CallId) 去重。

审批回复按 MAF 推荐方式,每个请求单独一条 User 消息:

new ChatMessage(ChatRole.User, [req.CreateResponse(approved: lastApproved.Value)])

approved: true 时框架执行 DoRefundfalse 时跳过。

3.4 框架内的 HITL 链路

模型发起 ExecuteLargeRefund 调用→ ApprovalRequiredAIFunction 拦截→ ToolApprovalRequestContent 写入流式 updates→ 你的代码 PromptApproval → CreateResponse→ Agent 继续 → 批准则 DoRefund 执行

Python 侧等价写法是 @tool(approval_mode="always_require");C# 侧用 ApprovalRequiredAIFunction 包装。

3.5 运行效果

小额 ORD-001:Agent 查单后直接调 ExecuteSmallRefund,控制台出现 [系统] 退款已实际执行,无 y/n 提示。

大额 ORD-8842:Agent 查单后调 ExecuteLargeRefund,弹出审批;输入 y 后执行退款,输入 n 则不出现 [系统] 退款已实际执行

判断是否真的退款,以 [系统] 退款已实际执行 为准,不以 Agent 口头回复为准。

四、小结

概念代码位置
HITLApprovalRequiredAIFunction 包装 ExecuteLargeRefund
大小额路由大模型查单后选择两个退款工具之一
审批工具ToolApprovalRequestContent
审批RunWithApprovalLoopAsync
流式RunStreamingAsync + ExtractApprovalRequests
相关资讯
点击查看更多
游戏推荐
推荐专题
热门阅读
推荐下载