当前位置: 首页 > news >正文

Spring AI Alibaba ——人工介入(Human-in-the-Loop)

Spring AI Alibaba ——人工介入(Human-in-the-Loop)

💡核心结论:一句话先记住

如果说 Agent 是个不知疲倦的打工人,那HITL(Human-in-the-Loop,人工介入)就是给它配了一个“拥有一票否决权的主管”。

大白话:当 AI 打算干一些“危险动作”(比如写文件、删数据库、发正式邮件)时,系统会自动按下暂停键,把操作卡住,等你人工看一眼。你点头了(Approve),它才继续干;你觉得不行,可以帮它改参数(Edit),或者直接打回重做(Reject)。

在 Spring AI Alibaba 中,这个机制底层依赖于检查点(Checkpoint)机制:Agent 暂停时会把当前的脑图记忆存进硬盘或内存,等你审批完,再原封不动地取出来继续跑。


🛑一、 老板批奏折的三种决策(Decision Types)

当 Agent 被拦截下来时,你需要给它一个反馈,系统内置了三种决策:

  • approve(批准):没毛病,原样执行。(比如:生成的邮件内容很好,直接发。)
  • ✏️edit(修改):思路对了,但细节有误,我帮你改改参数再执行。(比如:邮件收件人填错了,手动改对后发送。)
  • reject(拒绝):纯属胡扯,打回去并告诉它为什么拒绝,让它重新想。(比如:这封邮件语气太凶了,重写。)

📁二、 怎么给危险工具挂上“审批流”?(基础配置配置)

大白话:第一步,必须要有记忆存储(如MemorySaver),不然暂停后 Agent 就失忆了;第二步,搞一个HumanInTheLoopHook,告诉它遇到哪些工具需要卡住。

💻配置中断代码示例:

import com.alibaba.cloud.ai.graph.agent.ReactAgent; import com.alibaba.cloud.ai.graph.agent.hook.hip.HumanInTheLoopHook; import com.alibaba.cloud.ai.graph.agent.hook.hip.ToolConfig; import com.alibaba.cloud.ai.graph.checkpoint.savers.MemorySaver; // ⭐ 1. 必须:配置检查点保存器(因为人工介入必须保存案发现场) MemorySaver memorySaver = new MemorySaver(); // ⭐ 2. 核心:创建人工介入 Hook,指定哪些工具需要审批 HumanInTheLoopHook humanInTheLoopHook = HumanInTheLoopHook.builder() // 发现它调用 "write_file" 工具时,给我拦住! .approvalOn("write_file", ToolConfig.builder().description("文件写入操作需要审批").build()) // 发现它调用 "execute_sql" 时,也拦住! .approvalOn("execute_sql", ToolConfig.builder().description("SQL执行操作需要审批").build()) .build(); // 3. 把 Hook 和 Saver 挂载到 Agent 身上 ReactAgent agent = ReactAgent.builder() .name("approval_agent") .model(chatModel) .tools(writeFileTool, executeSqlTool, readDataTool) // 里面包含了各种工具 .hooks(List.of(humanInTheLoopHook)) // 注入审批流 .saver(memorySaver) // 注入记忆 .build();

三、 怎么响应中断并给反馈?(完整生命周期)

大白话:当你调用 Agent 发现它没返回答案,而是返回了一个InterruptionMetadata,这就说明“它被卡住了,在等你批示”。

💻1. 发现并查看中断(响应中断示例):

import com.alibaba.cloud.ai.graph.RunnableConfig; import com.alibaba.cloud.ai.graph.NodeOutput; import com.alibaba.cloud.ai.graph.action.InterruptionMetadata; // ⭐ 必须提供 threadId,不然系统不知道这是谁的会话 String threadId = "user-session-123"; RunnableConfig config = RunnableConfig.builder().threadId(threadId).build(); // 运行它! Optional<NodeOutput> result = agent.invokeAndGetOutput("删除数据库中的旧记录", config); // ⭐ 检查是不是被中断了 if (result.isPresent() && result.get() instanceof InterruptionMetadata) { InterruptionMetadata interruptionMetadata = (InterruptionMetadata) result.get(); // 把它打算干的坏事打印出来看看 List<InterruptionMetadata.ToolFeedback> toolFeedbacks = interruptionMetadata.toolFeedbacks(); for (InterruptionMetadata.ToolFeedback feedback : toolFeedbacks) { System.out.println("工具: " + feedback.getName()); System.out.println("参数: " + feedback.getArguments()); System.out.println("描述: " + feedback.getDescription()); } }

💻2. 给出决策并恢复运行(完整示例,接上文):

import com.alibaba.cloud.ai.graph.agent.ReactAgent; import com.alibaba.cloud.ai.graph.agent.hook.hip.HumanInTheLoopHook; import com.alibaba.cloud.ai.graph.agent.hook.hip.ToolConfig; import com.alibaba.cloud.ai.graph.RunnableConfig; import com.alibaba.cloud.ai.graph.NodeOutput; import com.alibaba.cloud.ai.graph.action.InterruptionMetadata; import com.alibaba.cloud.ai.graph.checkpoint.savers.MemorySaver; public class HumanInTheLoopExample { public static void main(String[] args) throws Exception { // ... (此处省略前面初始化 Agent 和 Hook 的代码) ... String threadId = "user-session-001"; RunnableConfig config = RunnableConfig.builder().threadId(threadId).build(); // 🔴 第一次调用:期望它被拦住 Optional<NodeOutput> result = agent.invokeAndGetOutput("帮我写一首100字左右的诗", config); if (result.isPresent() && result.get() instanceof InterruptionMetadata interruptionMetadata) { System.out.println("检测到中断,需要人工审批"); // ⭐ 1. 模拟老板批奏折:构建审批意见(这里选择全部 APPROVED 批准) InterruptionMetadata.Builder feedbackBuilder = InterruptionMetadata.builder() .nodeId(interruptionMetadata.node()) .state(interruptionMetadata.state()); interruptionMetadata.toolFeedbacks().forEach(toolFeedback -> { InterruptionMetadata.ToolFeedback approvedFeedback = InterruptionMetadata.ToolFeedback.builder(toolFeedback) .result(InterruptionMetadata.ToolFeedback.FeedbackResult.APPROVED) // 决策:批准! .build(); feedbackBuilder.addToolFeedback(approvedFeedback); }); InterruptionMetadata approvalMetadata = feedbackBuilder.build(); // ⭐ 2. 将圣旨(反馈意见)塞进 Config 里 RunnableConfig resumeConfig = RunnableConfig.builder() .threadId(threadId) .addMetadata(RunnableConfig.HUMAN_FEEDBACK_METADATA_KEY, approvalMetadata) // 带着反馈意见 .build(); // 🟢 第二次调用:带着老板的决定,恢复执行!(注意传的 input 是空串,因为状态已经记在脑子里了) Optional<NodeOutput> finalResult = agent.invokeAndGetOutput("", resumeConfig); if (finalResult.isPresent()) { System.out.println("最终结果: " + finalResult.get()); } } } }

🌪️四、 终极复杂场景:Workflow(工作流)里的连环套

大白话:有时候 Agent 不是单打独斗,而是被嵌套在一个巨大的StateGraph(图工作流)里的一个小小节点。这时候怎么审批?

区别在于:记忆(Saver)必须注册在工作流全局的CompileConfig,中断和恢复也是对着CompiledGraph发号施令。

💻工作流嵌套审批代码:

// ... 省略 imports ... // 1. 创建工具和 Saver ToolCallback searchTool = FunctionToolCallback.builder("search", (args) -> "搜索结果...").build(); MemorySaver saver = new MemorySaver(); // ⭐ 全局共享的 Saver // 2. 创建带审批 Hook 的 Agent ReactAgent qaAgent = ReactAgent.builder() .name("qa_agent") .model(chatModel) .saver(saver) // Agent 要挂载 .hooks(HumanInTheLoopHook.builder() .approvalOn("search", ToolConfig.builder().description("搜索操作需审批").build()) .build()) .tools(searchTool).build(); // ... 省略 PreprocessorNode 和 ValidatorNode 的定义 ... // 3. 构建工作流 StateGraph workflow = new StateGraph(keyStrategyFactory); workflow.addNode("preprocess", node_async(new PreprocessorNode())); workflow.addNode("validate", node_async(new ValidatorNode())); // ⭐ 把 Agent 作为一个 Node 塞进图里 workflow.addNode(qaAgent.name(), qaAgent.asNode(true, false)); // ... 省略连接边的代码 ... // 4. 编译工作流!⭐ 关键:必须在 CompileConfig 中注册检查点保存器 CompiledGraph compiledGraph = workflow.compile(CompileConfig.builder() .saverConfig(SaverConfig.builder().register(saver).build()) .build()); // 5. 执行工作流并处理中断 String threadId = "workflow-hilt-001"; Map<String, Object> input = Map.of("input", "请解释量子计算"); // 🔴 第一次调用 Graph Optional<NodeOutput> nodeOutputOptional = compiledGraph.invokeAndGetOutput(input, RunnableConfig.builder().threadId(threadId).build()); if (nodeOutputOptional.isPresent() && nodeOutputOptional.get() instanceof InterruptionMetadata interruptionMetadata) { System.out.println("工作流被中断,等待人工审核。中断节点: " + interruptionMetadata.node()); // ⭐ 构建同意的反馈(同上) InterruptionMetadata.Builder feedbackBuilder = InterruptionMetadata.builder() .nodeId(interruptionMetadata.node()) .state(interruptionMetadata.state()); interruptionMetadata.toolFeedbacks().forEach(toolFeedback -> { feedbackBuilder.addToolFeedback(InterruptionMetadata.ToolFeedback.builder(toolFeedback) .result(InterruptionMetadata.ToolFeedback.FeedbackResult.APPROVED).build()); }); InterruptionMetadata approvalMetadata = feedbackBuilder.build(); // 🟢 第二次调用:恢复工作流执行 RunnableConfig resumableConfig = RunnableConfig.builder() .threadId(threadId) // 必须是同一个线程 .addHumanFeedback(approvalMetadata) .build(); // 传入空 Map,因为状态已保存在全局检查点中 nodeOutputOptional = compiledGraph.invokeAndGetOutput(Map.of(), resumableConfig); }

🛠️五、 官方送你的“批奏折”快捷键 (HITLHelper)

大白话:每次遇到中断,都要写一堆Builder去循环构建同意/拒绝,太啰嗦了!你可以封装一个HITLHelper实用工具类,一键审批!

💻HITLHelper 工具类代码:

public class HITLHelper { /** 批准所有工具调用 */ public static InterruptionMetadata approveAll(InterruptionMetadata interruptionMetadata) { InterruptionMetadata.Builder builder = InterruptionMetadata.builder() .nodeId(interruptionMetadata.node()) .state(interruptionMetadata.state()); interruptionMetadata.toolFeedbacks().forEach(toolFeedback -> { builder.addToolFeedback(InterruptionMetadata.ToolFeedback.builder(toolFeedback) .result(InterruptionMetadata.ToolFeedback.FeedbackResult.APPROVED).build()); }); return builder.build(); } /** 拒绝所有工具调用,并给出理由 */ public static InterruptionMetadata rejectAll(InterruptionMetadata interruptionMetadata, String reason) { InterruptionMetadata.Builder builder = InterruptionMetadata.builder() .nodeId(interruptionMetadata.node()) .state(interruptionMetadata.state()); interruptionMetadata.toolFeedbacks().forEach(toolFeedback -> { builder.addToolFeedback(InterruptionMetadata.ToolFeedback.builder(toolFeedback) .result(InterruptionMetadata.ToolFeedback.FeedbackResult.REJECTED) .description(reason).build()); }); return builder.build(); } /** 修改特定工具的参数,其他的统统批准 */ public static InterruptionMetadata editTool(InterruptionMetadata interruptionMetadata, String toolName, String newArguments) { InterruptionMetadata.Builder builder = InterruptionMetadata.builder() .nodeId(interruptionMetadata.node()) .state(interruptionMetadata.state()); interruptionMetadata.toolFeedbacks().forEach(toolFeedback -> { if (toolFeedback.getName().equals(toolName)) { builder.addToolFeedback(InterruptionMetadata.ToolFeedback.builder(toolFeedback) .arguments(newArguments) .result(InterruptionMetadata.ToolFeedback.FeedbackResult.EDITED).build()); } else { builder.addToolFeedback(InterruptionMetadata.ToolFeedback.builder(toolFeedback) .result(InterruptionMetadata.ToolFeedback.FeedbackResult.APPROVED).build()); } }); return builder.build(); } } // 实际使用起来爽多了: InterruptionMetadata approvalMetadata = HITLHelper.approveAll(interruptionMetadata); // 一键全过 InterruptionMetadata rejectMetadata = HITLHelper.rejectAll(interruptionMetadata, "操作不安全"); // 一键打回 InterruptionMetadata editMetadata = HITLHelper.editTool(interruptionMetadata, "execute_sql", "{\"query\": \"SELECT * FROM records LIMIT 10\"}"); // 手动改个分页再跑

🛡️最佳实践(避坑指南)

  1. 没脑子千万别审批:必须使用检查点(Saver),否则恢复时找不到状态。
  2. 话要说清楚:配置ToolConfig时,描述写清楚点,不然人工审核的时候看着一团代码懵圈。
  3. 不要落下任何一只手:遇到多个并发的工具中断,必须对每一个ToolFeedback都给出决策(是杀是留)。
  4. 认准唯一单号:恢复执行的时候,threadId必须和之前中断的完全一致。
  5. 处理死等:最好加个超时机制,不能让 Agent 等老板审批等一整天。

🎯终极秒记口诀

智能 Agent 爱自由,危险动作需防守;

加入 HITL 做拦截,Saver 留存防弄丢;

Approve 批准任它走,Edit 帮你修一修;

要是胡扯全 Reject,批完奏折再回头!

http://www.jsqmd.com/news/1065466/

相关文章:

  • AI 自动化工作流设计:从单次调用到多步编排的可靠性实践
  • 云原生时代Node.js微服务可观测性实践
  • 2026年知名的菏泽橡皮泥粘土/潘通色粘土/自封袋装粘土可靠供应商推荐 - 品牌宣传支持者
  • 从零到一万并发:Apipost接口压力测试全流程实战指南
  • 用GLM-5.1构建智能体工作流的内容付费系统
  • 如何甄别企业真实技术需求并避免挖掘误区?
  • 多功能便携 FM-200E 误码仪 适配油气复杂工况完成 2M 传输链路检测
  • (2026最新)惠州防水补漏正规公司甄选推荐:漏水检测维修-暗管漏水精准定位检测漏水点-卫生间/厨房/屋顶/阳台/渗漏水维修-本地人必选的正规测漏公司 - 即刻修防水
  • 2026合肥妇科医院怎么选?深耕女性健康,优选合规靠谱妇科机构-合肥长征妇科医院
  • 6款主流降AIGC工具 降痕效果拉满
  • AI购物:选品、比价、省钱、支付…… 这届“618”,谁是最强AI购物搭子?
  • Virtual-Display-Driver深度指南:高效扩展Windows虚拟显示器终极方案
  • 今日金价936,国际金价4200,白银66
  • Qwen3.7-Max:智能体时代可落地的执行引擎
  • Windows系统文件danim.dll丢失找不到问题解决
  • 从 Serper 切到 SERP API:200 行代码 diff 实战
  • 基于Python实现的网络嗅探器
  • (2026最新)德阳防水补漏正规公司甄选推荐:漏水检测维修-暗管漏水精准定位检测漏水点-卫生间/厨房/屋顶/阳台/渗漏水维修-本地人必选的正规测漏公司 - 即刻修防水
  • 讯飞版Codex+GLM-5.2=顶级世界杯AI搭子
  • Kimi K 2.5 多智能体工作流实战:可编排、可追溯的AI协同范式
  • Claude Code智能编码工作流:Agents+Commands+Skills工程实践
  • 2026年诚信的琥珀酸/青岛脱氢乙酸钠/青岛乳酸钠粉/乳酸钙定制加工厂家推荐 - 行业平台推荐
  • C语言如何上线手机App?真实C端项目实战指南
  • 毕业文稿减负新思路|okbiye 毕业论文专属创作模块,一站式搞定全流程撰写难题
  • 基于MCF51AC256的无传感器永磁同步电机FOC控制实战详解
  • 小红书数据采集终极指南:5分钟掌握XHS-Downloader完整使用教程
  • 搭建生产级AI会话应用:从本地闭环到K8s上线的工程实践
  • 抖音下载神器终极指南:从零开始掌握批量下载技巧
  • React Hooks 闭包陷阱与依赖治理:从状态陈旧到渲染优化的工程化解法
  • 如何在Windows上打造会呼吸的动态桌面:5步实现macOS级视觉体验