✅Agent常用架构:Human in the Loop







Human-in-the-Loop(简称 HITL) 指的是在 AI 系统中的自动决策或执行过程中,引入人类用户作为“必要参与者”,在关键节点对 AI 的行为和结果进行审查、确认或修正,而不是让模型完全自动完成端到端的执行。

在 Agent 场景下,HITL 的核心并不是用户参与推理,而是:

在用户允许的边界内,让 Agent 自动运行;一旦即将执行高风险或高不确定性的动作,必须经过人工确认。

HITL 本质上是一种 流程控制机制

HITL 的应用场景

高风险工具调用

当 Agent 需要调用具备一些重要或敏感的工具时,例如:

  • 写文件、删除资源

  • 执行 SQL / 运维指令

  • 调用外部系统接口(下单、转账、封禁用户等)

这类操作一旦执行,往往成本或者影响面较大,因此不适合完全由模型自动决定。

合规与审计要求

在金融、安全、企业 IT 等场景中,系统通常要求:

  • 关键操作必须有人类确认

  • 决策过程可回溯、可审计

HITL 可以天然满足“人工审批 + 自动执行”的合规要求。

模型不确定性较高的场景

当模型能力比较有限,对上下文理解不充分、信息不完整,或者输出存在多种合理路径时,通过人工介入可以显著降低错误率。



开箱即用 HITL

Spring AI Alibaba 已经提供了对 Agent HITL 的支持,通过HumanInTheLoopHook实现,这其实是一种我们前面介绍过的Hook机制。



整体流程大致可以分为三个阶段。

  • 配置中断:在创建 Agent 时,配置哪些工具需要人工审批;

  • 响应中断:调用 Agent 运行逻辑,若触发人工中断,返回中断元数据;

  • 恢复执行:将人工决策反馈传回给 Agent,并继续执行 React 逻辑。



配置阶段,通过 HumanInTheLoopHook 明确声明 getWeather 工具需要人工审批,从而将“是否允许执行该工具”的决策权从模型侧上移到框架层,避免模型直接调用的行为。



1
2
3
4
5
6
7
8
9
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.checkpo
int.savers.MemorySaver;
// 配置检查点保存器(人工介入需要检查点来处理中断)MemorySaver memorySaver = new MemorySaver();
// 创建人工介入HookHumanInTheLoopHook humanInTheLoopHook = HumanInTheLoopHook.builder() .approvalOn("write_file", ToolConfig.builder() .description("文件写入操作需要审批") .build()) .approvalOn("execute_sql", ToolConfig.builder() .description("SQL执行操作需要审批") .build()) .build();
// 创建AgentReactAgent agent = ReactAgent.builder() .name("approval_agent") .model(chatModel) .tools(writeFileTool, executeSqlTool, readDataTool) .hooks(List.of(humanInTheLoopHook)) .saver(memorySaver) .build();



响应中断阶段,Agent 正常进行推理,当模型生成了对受控工具的调用请求后,框架在工具真正执行之前触发中断,返回 InterruptionMetadata。此时 Agent 并未失败,而是以一种“可恢复的中断状态”安全退出,将待执行的工具调用信息完整暴露给外部系统。



1
2
3
4
5
6
7
8
9
10
11
import com.alibaba.cloud.ai.graph.RunnableConfig;
import com.alibaba.cloud.ai.graph.NodeOutput;
import com.alibaba.cloud.ai.graph.action.InterruptionMetadata;
// 人工介入利用检查点机制。// 你必须提供线程ID以将执行与会话线程关联,// 以便可以暂停和恢复对话(人工审查所需)。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()); }
// 示例输出: // 工具: execute_sql // 参数: {"query": "DELETE FROM records WHERE created_at < NOW() - INTERVAL '30 days';"} // 描述: SQL执行操作需要审批}

恢复执行阶段,外部系统基于中断信息构造人工反馈(批准、修改或拒绝),并通过相同的 threadId 将反馈重新注入 Agent。Agent 利用之前保存的执行状态继续运行,在人工决策的约束下完成后续工具调用和推理流程,最终产出完整结果。



1
2
3
4
5
6
7
8
9
10
11
12
List<InterruptionMetadata.ToolFeedback> toolFeedbacks =              interruptionMetadata.toolFeedbacks();

InterruptionMetadata.Builder feedbackBuilder = InterruptionMetadata.builder() .nodeId(interruptionMetadata.node()) .state(interruptionMetadata.state());
toolFeedbacks.forEach(toolFeedback -> {
InterruptionMetadata.ToolFeedback approvedFeedback = InterruptionMetadata.ToolFeedback.builder(toolFeedback) .result(InterruptionMetadata.ToolFeedback.FeedbackResult.APPROVED) .build(); feedbackBuilder.addToolFeedback(approvedFeedback); });
InterruptionMetadata approvalMetadata = feedbackBuilder.build();

RunnableConfig resumeConfig = RunnableConfig.builder() .threadId(threadId) .addMetadata(RunnableConfig.HUMAN_FEEDBACK_METADATA_KEY, approvalMetadata) .build();
Optional<NodeOutput> finalResult = agent.invokeAndGetOutput("", resumeConfig);
if (finalResult.isPresent()) {
System.out.println("执行完成"); System.out.println("最终结果: " + finalResult.get());
}



整个过程中,HITL 并未改变模型推理方式,而是通过 执行拦截、状态保存与恢复机制 实现对 Agent 行为的强控制。



完整示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static void main(String[] args) throws Exception {
// 初始化 DashScopeApi DashScopeApi dashScopeApi = DashScopeApi.builder() .apiKey("sk-XXXXXXXXXXXXXXXXXXXXXXXXX") .build();
// 创建 ChatModel ChatModel chatModel = DashScopeChatModel.builder() .dashScopeApi(dashScopeApi) .defaultOptions(DashScopeChatOptions.builder() .withModel("qwen-plus") .withTemperature(0.7) .withMaxToken(2000) .withTopP(0.9) .build()) .build(); MemorySaver memorySaver = new MemorySaver();
// 1. 配置中断 HumanInTheLoopHook humanInTheLoopHook = HumanInTheLoopHook.builder() .approvalOn("getWeather", ToolConfig.builder() .description("请确认操作") .build()) .build();
ToolCallback[] toolCallbacks = ToolCallbacks.from(new WeatherService()); ReactAgent agent = ReactAgent.builder() .name("agent") .model(chatModel) .tools(toolCallbacks) .saver(memorySaver) .hooks(List.of(humanInTheLoopHook)) .build();
String threadId = "user-001"; RunnableConfig config = RunnableConfig.builder() .threadId(threadId) .build();
System.out.println("=== 第一次调用:期望中断 ==="); Optional<NodeOutput> result = agent.invokeAndGetOutput( "帮我查询南京的天气", config );
// 2. 响应中断 if (result.isPresent() && result.get() instanceof InterruptionMetadata) { InterruptionMetadata interruptionMetadata = (InterruptionMetadata) result.get();
System.out.println("检测到中断,需要人工审批");
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()); }
// 模拟人工决策(这里选择批准)实际工程中可以和前端交互 InterruptionMetadata.Builder feedbackBuilder = InterruptionMetadata.builder() .nodeId(interruptionMetadata.node()) .state(interruptionMetadata.state());
toolFeedbacks.forEach(toolFeedback -> {
InterruptionMetadata.ToolFeedback approvedFeedback = InterruptionMetadata.ToolFeedback.builder(toolFeedback) .result(InterruptionMetadata.ToolFeedback.FeedbackResult.APPROVED) .build(); feedbackBuilder.addToolFeedback(approvedFeedback); });
InterruptionMetadata approvalMetadata = feedbackBuilder.build();
// 3. 恢复执行 System.out.println("== 第二次调用:使用批准决策恢复 == = "); RunnableConfig resumeConfig = RunnableConfig.builder() .threadId(threadId) .addMetadata(RunnableConfig.HUMAN_FEEDBACK_METADATA_KEY, approvalMetadata) .build();
Optional<NodeOutput> finalResult = agent.invokeAndGetOutput("", resumeConfig);
if (finalResult.isPresent()) {
System.out.println("执行完成"); System.out.println("最终结果: " + finalResult.get()); } } }





HumanInTheLoopHook

执行时机

了解了整体用法和流程后,那我们来具体看下 HITL 的核心类,这个HumanInTheLoopHook里面到底做了哪些工作。



首先我们可以看到@HookPositions(HookPosition.AFTER_MODEL)afterModel,也就是说它是执行在:模型输出之后,工具执行之前。这时候模型已经完成推理,Tool Call 已经生成。



interrupt

interrupt方法来自于InterruptableAction接口



**interrupt**的含义就是 Agent 执行引擎继续往下跑之前,给你一次暂停的机会。它的实际调用是在NodeExecutor类中执行的。





我们可以看到interrupt 是在“节点 action.apply() 之前”执行的。也就是说:

  • interrupt = 要不要执行这个节点

  • apply = 真正执行这个节点

也就是说,每次执行节点的时候,都会判断一下,是否要暂停执行,interrupt就是对这个判断的回答,它的返回值有两种:

  • Optional.empty()什么都不做,Graph 继续执行

  • Optional.of(InterruptionMetadata)立刻中断执行

接着我们看下HumanInTheLoopHookinterrupt 的具体实现:



先检查 RunnableConfig 中是否已携带人工反馈(HUMAN_FEEDBACK_METADATA_KEY),如果存在,说明当前不是第一次执行,而是在“人工审批之后的恢复阶段”。



接着会校验反馈是否合法,若反馈不完整或不符合审批规则,则继续返回该 InterruptionMetadata,强制 Graph 再次中断;若反馈合法,则返回 Optional.empty(),明确放行当前节点,允许执行继续向下推进。



若不存在人工反馈,则进入首次执行路径:从当前状态中取出最后一条消息,确认其为包含 Tool Call 的 AssistantMessage,并逐一检查这些 Tool Call 是否命中 approvalOn 中声明的受控工具。一旦发现任意一个受控工具调用,就构造对应的 InterruptionMetadata,将工具名称、参数和用于人工审批的描述信息封装为 ToolFeedback,并返回该中断结果。Graph 执行引擎在收到这个返回值后会立即暂停执行,将控制权交还给调用方,从而完成 HITL 中断。



afterModel

afterModel方法则是在人工反馈已给出、Graph 从中断状态恢复执行时才真正发挥作用。



它的核心职责就是消费人工反馈、改写模型上一次的 Tool Call 结果。方法首先从 RunnableConfig 中读取 HUMAN_FEEDBACK_METADATA_KEY,如果不存在,说明这是正常的非 HITL 路径,直接返回空更新;如果存在,则表明当前执行是一次人工决策的恢复运行。随后它定位状态中最后一条 AssistantMessage,因为这正是上一次被中断时模型生成、但尚未真正执行工具的那条消息。



在确认最后一条消息包含 ToolCall 后,afterModel会逐个比对 ToolCall 与人工反馈中的ToolFeedback

  • APPROVED:保留原 Tool Call,允许后续节点执行工具;

  • EDITED:用人工修改后的参数生成新的 Tool Call;

  • REJECTED:不再执行该工具,而是构造一条 ToolResponseMessage,显式告诉模型该工具被人工拒绝,并给出原因或建议。

最后插入包含更新后 Tool Call 的新 AssistantMessage,并给旧的消息打上删除标签,Graph 在下一步继续运行时,看到的将是已经被人工裁决过的工具调用结果



流程总结

HumanInTheLoopHook整体流程:

  1. interrupt(中断判定)在节点执行前检查模型输出,若发现命中受控工具且尚未有人工反馈,则生成 InterruptionMetadata,强制 Graph 暂停。

  2. 人工反馈(外部)人类基于 InterruptionMetadata 决定批准、修改或拒绝工具调用,并将结果通过 RunnableConfig 回传。

  3. afterModel(恢复与重写)在恢复执行时消费人工反馈,重写 AssistantMessage 中的 Tool Call 或生成拒绝响应,使后续执行基于人已确认的决策继续推进。