【Spring AI Alibaba】 ⑤ 人工介入(Human-in-the-loop):关键决策点的智能审批与中断恢复

📖目录

  • 前言
  • [1. 引言:为什么需要人工介入?](#1. 引言:为什么需要人工介入?)
  • [2. 人工介入的核心概念](#2. 人工介入的核心概念)
    • [2.1 什么是人工介入?](#2.1 什么是人工介入?)
    • [2.2 三种核心决策类型](#2.2 三种核心决策类型)
  • [3. Spring AI Alibaba中的人工介入实现](#3. Spring AI Alibaba中的人工介入实现)
    • [3.1 人工介入的架构设计](#3.1 人工介入的架构设计)
    • [3.2 核心类图解析](#3.2 核心类图解析)
  • [4. 源码深度解析:HumanInTheLoopHook](#4. 源码深度解析:HumanInTheLoopHook)
    • [4.1 核心流程](#4.1 核心流程)
    • [4.2 关键代码解析](#4.2 关键代码解析)
  • [5. 实战案例:从代码到执行](#5. 实战案例:从代码到执行)
    • [5.1 诗歌创作审批示例](#5.1 诗歌创作审批示例)
    • [5.2 执行结果解析](#5.2 执行结果解析)
  • [6. 异常处理:拒绝决策的实现](#6. 异常处理:拒绝决策的实现)
    • [6.1 拒绝决策的代码实现](#6.1 拒绝决策的代码实现)
    • [6.2 执行结果与异常处理](#6.2 执行结果与异常处理)
  • [7. 完整的示例代码及执行结果](#7. 完整的示例代码及执行结果)
    • [7.1 完整代码](#7.1 完整代码)
    • [7.2 执行结果](#7.2 执行结果)
  • [8. 与Interceptor的区别:为什么需要Hook?](#8. 与Interceptor的区别:为什么需要Hook?)
  • [9. 最佳实践与未来趋势](#9. 最佳实践与未来趋势)
    • [9.1 人工介入的最佳实践](#9.1 人工介入的最佳实践)
    • [9.2 2025年主流趋势](#9.2 2025年主流趋势)
  • [10. 总结](#10. 总结)
  • [11. 推荐阅读](#11. 推荐阅读)

前言

"AI不是万能的,但AI+人工介入可以成为最可靠的决策伙伴"


1. 引言:为什么需要人工介入?

想象一下,你正在使用一个智能客服系统,它突然要执行"删除所有用户数据"的操作。你会放心让AI直接执行吗?不,你会要求人工确认。这就是"人工介入(Human-in-the-loop)"的核心价值------在关键决策点插入人工审批环节,让AI成为"智能助手"而非"决策者"。

在企业级AI应用中,人工介入不是"麻烦",而是合规性、安全性和信任度的保障 。根据Gartner 2024年报告,85%的AI失败案例源于缺乏有效的决策监督机制。人工介入正是解决这个问题的关键技术。


2. 人工介入的核心概念

2.1 什么是人工介入?

人工介入(Human-in-the-loop, HIL)是一种AI工作流设计模式,当AI系统需要执行高风险、高敏感度或关键业务操作 时,系统会暂停执行,等待人工审批后继续。这就像快递员在投递贵重物品时会要求你本人签收一样。

2.2 三种核心决策类型

决策类型 作用 业务场景 代码实现
approve(批准) 允许AI继续执行 生成客户合同、发送重要通知 FeedbackResult.APPROVED
edit(编辑) 修改AI参数后执行 SQL查询、数据处理 FeedbackResult.EDITED
reject(拒绝) 终止当前流程 删除用户数据、敏感操作 FeedbackResult.REJECTED

💡 大白话解释:就像你点外卖时,系统提示"确认要添加辣度?"(approve),"需要修改辣度吗?"(edit),"不能添加辣度"(reject)。


3. Spring AI Alibaba中的人工介入实现

3.1 人工介入的架构设计


approve
edit
reject
用户请求
Agent执行流程
需要人工介入?
暂停执行
人工审批界面
决策类型
恢复执行
修改参数
终止流程
完成执行
返回错误信息

3.2 核心类图解析

approvalOn
toolFeedbacks
result
InterruptionMetadata
-String nodeId
-OverAllState state
-List<ToolFeedback> toolFeedbacks
+Builder builder()
+build()
ToolFeedback
-String id;
-String name;
-String arguments;
-FeedbackResult result;
-String description;
+Builder builder()
+build()
FeedbackResult
APPROVED
EDITED
REJECTED
HumanInTheLoopHook
-Map<String, ToolConfig> approvalOn
+build()
ToolConfig
-String description
+builder()


4. 源码深度解析:HumanInTheLoopHook

4.1 核心流程

  1. 配置阶段 :通过approvalOn指定哪些工具需要人工介入
  2. 执行阶段:Agent执行到需要人工介入的工具时暂停
  3. 审批阶段:系统等待人工反馈
  4. 恢复阶段:根据反馈决定继续执行、编辑参数或终止

4.2 关键代码解析

java 复制代码
// HumanInTheLoopHook.java
@HookPositions(HookPosition.AFTER_MODEL)
public class HumanInTheLoopHook extends ModelHook implements AsyncNodeActionWithConfig, InterruptableAction {
	private static final Logger log = LoggerFactory.getLogger(HumanInTheLoopHook.class);

	private Map<String, ToolConfig> approvalOn;

	private HumanInTheLoopHook(Builder builder) {
		// 从Builder构造函数中复制approvalOn映射
		this.approvalOn = new HashMap<>(builder.approvalOn);
	}

	public static Builder builder() {
		// 返回一个Builder实例用于创建HumanInTheLoopHook对象
		return new Builder();
	}

	@Override
	public CompletableFuture<Map<String, Object>> apply(OverAllState state, RunnableConfig config) {
		// 调用afterModel方法处理逻辑
		return afterModel(state, config);
	}

	@Override
	public CompletableFuture<Map<String, Object>> afterModel(OverAllState state, RunnableConfig config) {
		// 从配置元数据中获取人类反馈信息
		Optional<Object> feedback = config.metadata(RunnableConfig.HUMAN_FEEDBACK_METADATA_KEY);
		// 将反馈转换为InterruptionMetadata类型
		InterruptionMetadata interruptionMetadata = (InterruptionMetadata) feedback.orElse(null);

		if (interruptionMetadata == null) {
			// 如果没有找到人类反馈,则记录日志并返回空结果
			log.info("No human feedback found in the runnable config metadata, no tool to execute or none needs feedback.");
			return CompletableFuture.completedFuture(Map.of());
		}

		// 从状态中获取消息列表
		List<Message> messages = (List<Message>) state.value("messages").orElse(List.of());
		// 获取最后一条消息
		Message lastMessage = messages.get(messages.size() - 1);

		if (lastMessage instanceof AssistantMessage assistantMessage) {

			if (!assistantMessage.hasToolCalls()) {
				// 如果最后一条消息不是工具调用,则记录日志并返回空结果
				log.info("Found human feedback but last AssistantMessage has no tool calls, nothing to process for human feedback.");
				return CompletableFuture.completedFuture(Map.of());
			}

			// 创建新的工具调用列表
			List<AssistantMessage.ToolCall> newToolCalls = new ArrayList<>();

			// 创建响应列表用于存储拒绝的工具调用
			List<ToolResponseMessage.ToolResponse> responses = new ArrayList<>();
			// 创建拒绝消息对象
			ToolResponseMessage rejectedMessage = new ToolResponseMessage(responses);

			for (AssistantMessage.ToolCall toolCall : assistantMessage.getToolCalls()) {
				// 查找当前工具调用对应的反馈
				Optional<ToolFeedback> toolFeedbackOpt = interruptionMetadata.toolFeedbacks().stream()
						.filter(tf -> tf.getName().equals(toolCall.name()))
						.findFirst();

				if (toolFeedbackOpt.isPresent()) {
					// 获取工具反馈对象
					ToolFeedback toolFeedback = toolFeedbackOpt.get();
					// 获取反馈结果(批准、编辑、拒绝)
					FeedbackResult result = toolFeedback.getResult();

					if (result == FeedbackResult.APPROVED) {
						// 如果工具调用被批准,则添加到新工具调用列表中
						newToolCalls.add(toolCall);
					}
					else if (result == FeedbackResult.EDITED) {
						// 如果工具调用被编辑,则创建新的工具调用对象并添加到列表中
						AssistantMessage.ToolCall editedToolCall = new AssistantMessage.ToolCall(toolCall.id(), toolCall.type(), toolCall.name(), toolFeedback.getArguments());
						newToolCalls.add(editedToolCall);
					}
					else if (result == FeedbackResult.REJECTED) {
						// 如果工具调用被拒绝,则创建拒绝响应并添加到响应列表中
						ToolResponseMessage.ToolResponse response = new ToolResponseMessage.ToolResponse(toolCall.id(), toolCall.name(), String.format("Tool call request for %s has been rejected by human. The reason for why this tool is rejected and the suggestion for next possible tool choose is listed as below:\n %s.", toolFeedback.getName(), toolFeedback.getDescription()));
						responses.add(response);
					}
				}
				else {
					// 如果没有为需要批准的工具提供反馈,则视为批准以继续执行
					newToolCalls.add(toolCall);
				}
			}

			// 创建更新映射
			Map<String, Object> updates = new HashMap<>();
			// 创建新消息列表
			List<Object> newMessages = new ArrayList<>();

			if (!rejectedMessage.getResponses().isEmpty()) {
				// 如果有拒绝的响应,则将拒绝消息添加到新消息列表中
				newMessages.add(rejectedMessage);
			}

			if (!newToolCalls.isEmpty()) {
				// 创建包含更新工具调用的新助手消息
				newMessages.add(new AssistantMessage(assistantMessage.getText(), assistantMessage.getMetadata(), newToolCalls, assistantMessage.getMedia()));
				// 添加RemoveByHash操作以移除原助手消息
				newMessages.add(new RemoveByHash<>(assistantMessage));
			}

			// 将新消息列表放入更新映射中
			updates.put("messages", newMessages);
			// 返回完成的更新映射
			return CompletableFuture.completedFuture(updates);
		}
		else {
			// 如果最后一条消息不是助手消息,则记录警告
			log.warn("Last message is not an AssistantMessage, cannot process human feedback.");
		}

		// 返回空的完成Future
		return CompletableFuture.completedFuture(Map.of());
	}

	@Override
	public Optional<InterruptionMetadata> interrupt(String nodeId, OverAllState state, RunnableConfig config) {
		// 从配置元数据中获取人类反馈信息
		Optional<Object> feedback = config.metadata(RunnableConfig.HUMAN_FEEDBACK_METADATA_KEY);
		if (feedback.isPresent()) {
			// 检查反馈类型是否为InterruptionMetadata
			if (!(feedback.get() instanceof InterruptionMetadata)) {
				throw new IllegalArgumentException("Human feedback metadata must be of type InterruptionMetadata.");
			}

			// 验证反馈是否有效
			if (!validateFeedback((InterruptionMetadata) feedback.get())) {
				return Optional.of((InterruptionMetadata)feedback.get());
			}
			return Optional.empty();
		}

		// 从状态中获取消息列表
		List<Message> messages = (List<Message>) state.value("messages").orElse(List.of());
		// 获取最后一条消息
		Message lastMessage = messages.get(messages.size() - 1);

		if (lastMessage instanceof AssistantMessage assistantMessage) {
			// 2. 如果最后一条消息是助手消息
			if (assistantMessage.hasToolCalls()) {
				// 标记是否需要中断
				boolean needsInterruption = false;
				// 创建中断元数据构建器
				InterruptionMetadata.Builder builder = InterruptionMetadata.builder(getName(), state);
				for (AssistantMessage.ToolCall toolCall : assistantMessage.getToolCalls()) {
					// 检查当前工具调用是否需要批准
					if (approvalOn.containsKey(toolCall.name())) {
						// 获取工具配置
						ToolConfig toolConfig = approvalOn.get(toolCall.name());
						// 获取工具描述
						String description = toolConfig.getDescription();
						// 创建人类审批的提示内容
						String content = "The AI is requesting to use the tool: " + toolCall.name() + ".\n"
								+ (description != null ? ("Description: " + description + "\n") : "")
								+ "With the following arguments: " + toolCall.arguments() + "\n"
								+ "Do you approve?";
						// TODO, create a designated tool metadata field in InterruptionMetadata?
						// 向构建器添加工具反馈信息
						builder.addToolFeedback(InterruptionMetadata.ToolFeedback.builder().id(toolCall.id())
										.name(toolCall.name()).description(content).arguments(toolCall.arguments()).build())
								.build();
						// 设置需要中断标记
						needsInterruption = true;
					}
				}
				// 如果需要中断,则返回构建的中断元数据,否则返回空
				return needsInterruption ? Optional.of(builder.build()) : Optional.empty();
			}
		}
		// 返回空表示不需要中断
		return Optional.empty();
	}

	private boolean validateFeedback(InterruptionMetadata feedback) {
		// 检查反馈是否为空或工具反馈列表为空
		if (feedback == null || feedback.toolFeedbacks() == null || feedback.toolFeedbacks().isEmpty()) {
			return false;
		}

		// 获取工具反馈列表
		List<InterruptionMetadata.ToolFeedback> toolFeedbacks = feedback.toolFeedbacks();

		// 1. 确保每个ToolFeedback的结果不为空
		for (InterruptionMetadata.ToolFeedback toolFeedback : toolFeedbacks) {
			if (toolFeedback.getResult() == null) {
				return false;
			}
		}

		// 2. 确保ToolFeedback数量与approvalOn数量匹配且所有名称都在approvalOn中
		if (toolFeedbacks.size() != approvalOn.size()) {
			return false;
		}
		for (InterruptionMetadata.ToolFeedback toolFeedback : toolFeedbacks) {
			if (!approvalOn.containsKey(toolFeedback.getName())) {
				return false;
			}
		}

		// 验证通过返回true
		return true;
	}

	@Override
	public String getName() {
		// 返回钩子的名称
		return "HIP";
	}

	@Override
	public List<JumpTo> canJumpTo() {
		// 返回可以跳转到的位置列表(当前为空)
		return List.of();
	}

	public static class Builder {
		// 存储需要批准的工具配置映射
		private Map<String, ToolConfig> approvalOn = new HashMap<>();

		public Builder approvalOn(String toolName, ToolConfig toolConfig) {
			// 将工具名和配置添加到approvalOn映射中
			this.approvalOn.put(toolName, toolConfig);
			return this;
		}

		public Builder approvalOn(String toolName, String description) {
			// 创建工具配置对象并设置描述
			ToolConfig config = new ToolConfig();
			config.setDescription(description);
			// 将工具名和配置添加到映射中
			this.approvalOn.put(toolName, config);
			return this;
		}

		public Builder approvalOn(Map<String, ToolConfig> approvalOn) {
			// 将提供的映射中的所有条目添加到当前映射中
			this.approvalOn.putAll(approvalOn);
			return this;
		}

		public HumanInTheLoopHook build() {
			// 使用当前构建器创建HumanInTheLoopHook实例
			return new HumanInTheLoopHook(this);
		}
	}

}

🔍 关键点解析

  • 检查工具是否在approvalOn配置列表中
  • 如果需要审批,构建InterruptionMetadata并返回
  • 系统会暂停执行,等待人工反馈

5. 实战案例:从代码到执行

5.1 诗歌创作审批示例

java 复制代码
// 示例2:批准(approve)决策
public void example2_approveDecision() throws Exception {
    // 1. 配置人工介入
    HumanInTheLoopHook humanInTheLoopHook = HumanInTheLoopHook.builder()
        .approvalOn("poem", ToolConfig.builder()
            .description("请确认诗歌创作操作")
            .build())
        .build();
    
    // 2. 创建Agent
    ReactAgent agent = ReactAgent.builder()
        .name("poet_agent")
        .model(chatModel)
        .tools(poetTool) // 诗歌工具
        .hooks(List.of(humanInTheLoopHook))
        .saver(new MemorySaver())
        .build();
    
    // 3. 第一次调用 - 触发中断
    Optional<NodeOutput> result = agent.invokeAndGetOutput("帮我写一首100字左右的诗", config);
    
    // 4. 人工审批(模拟)
    if (result.isPresent() && result.get() instanceof InterruptionMetadata) {
        InterruptionMetadata interruptionMetadata = (InterruptionMetadata) result.get();
        
        // 构建批准决策
        InterruptionMetadata approvalMetadata = InterruptionMetadata.builder()
            .nodeId(interruptionMetadata.node())
            .state(interruptionMetadata.state())
            .addToolFeedback(
                InterruptionMetadata.ToolFeedback.builder(interruptionMetadata.toolFeedbacks().get(0))
                    .result(InterruptionMetadata.ToolFeedback.FeedbackResult.APPROVED)
                    .build()
            )
            .build();
        
        // 5. 恢复执行
        RunnableConfig resumeConfig = RunnableConfig.builder()
            .threadId("user-session-001")
            .addMetadata(RunnableConfig.HUMAN_FEEDBACK_METADATA_KEY, approvalMetadata)
            .build();
        
        agent.invokeAndGetOutput("", resumeConfig);
    }
}

5.2 执行结果解析

复制代码
=== 第一次调用:期望中断 ===
检测到中断,需要人工审批
工具: poem
参数: "春风拂面花自开,柳绿桃红映山川。溪水潺潺绕林过,鸟语声声入梦来。田园风光无限好,心随景动意悠然。愿将此情寄明月,照亮人间万里天。"
描述: 请确认诗歌创作操作

=== 第二次调用:使用批准决策恢复 ===
执行完成批准决策示例

💡 执行逻辑 :AI请求写诗(poem工具),系统暂停并提示人工审批。人工批准后,AI继续执行并返回诗歌。


6. 异常处理:拒绝决策的实现

6.1 拒绝决策的代码实现

java 复制代码
// 示例4:拒绝(reject)决策
public void example4_rejectDecision() throws Exception {
    // 1. 配置人工介入
    HumanInTheLoopHook humanInTheLoopHook = HumanInTheLoopHook.builder()
        .approvalOn("delete_data", ToolConfig.builder()
            .description("删除操作需要审批")
            .build())
        .build();
    
    // 2. 创建Agent
    ReactAgent agent = ReactAgent.builder()
        .name("delete_agent")
        .model(chatModel)
        .tools(deleteTool)
        .hooks(List.of(humanInTheLoopHook))
        .saver(new MemorySaver())
        .build();
    
    // 3. 第一次调用 - 触发中断
    Optional<NodeOutput> result = agent.invokeAndGetOutput("删除所有用户数据", config);
    
    // 4. 人工审批(拒绝)
    if (result.isPresent() && result.get() instanceof InterruptionMetadata) {
        InterruptionMetadata interruptionMetadata = (InterruptionMetadata) result.get();
        
        // 构建拒绝决策
        InterruptionMetadata rejectMetadata = InterruptionMetadata.builder()
            .nodeId(interruptionMetadata.node())
            .state(interruptionMetadata.state())
            .addToolFeedback(
                InterruptionMetadata.ToolFeedback.builder(interruptionMetadata.toolFeedbacks().get(0))
                    .result(InterruptionMetadata.ToolFeedback.FeedbackResult.REJECTED)
                    .description("不允许删除操作,请使用归档功能代替。")
                    .build()
            )
            .build();
        
        // 5. 恢复执行(拒绝后终止流程)
        RunnableConfig resumeConfig = RunnableConfig.builder()
            .threadId("delete-session-001")
            .addMetadata(RunnableConfig.HUMAN_FEEDBACK_METADATA_KEY, rejectMetadata)
            .build();
        
        // 6. 捕获并处理业务异常
        try {
            agent.invokeAndGetOutput("", resumeConfig);
        } catch (Exception e) {
            // 捕获拒绝后的异常,转换为业务异常
            throw new BusinessException("操作被拒绝: " + e.getMessage(), e);
        }
    }
}

6.2 执行结果与异常处理

复制代码
20:37:43.133 [main] INFO com.alibaba.cloud.ai.graph.agent.hook.hip.HumanInTheLoopHook -- Found human feedback but last AssistantMessage has no tool calls, nothing to process for human feedback.
拒绝决策示例执行完成

💡 异常处理技巧 :在example5_multipleTools中,我们看到一个典型异常:

复制代码
org.springframework.ai.retry.NonTransientAiException: 400 - {"request_id":"4306879b-1794-4b70-9d4e-95688496f9ec","code":"InvalidParameter","message":"<400> InternalError.Algo.InvalidParameter: messages with role \"tool\" must be a response to a preceeding message with \"tool_calls\"."}

解决方法 :在人工介入后恢复执行时,必须确保RunnableConfig中包含HUMAN_FEEDBACK_METADATA_KEY,否则AI引擎会认为没有收到反馈,从而抛出异常。


7. 完整的示例代码及执行结果

7.1 完整代码

java 复制代码
/**
 * 人工介入(Human-in-the-Loop)示例
 *
 * 演示如何使用人工介入Hook为Agent工具调用添加人工监督,包括:
 * 1. 配置中断和审批
 * 2. 批准(approve)决策
 * 3. 编辑(edit)决策
 * 4. 拒绝(reject)决策
 * 5. 完整示例
 * 6. 实用工具方法
 *
 */
public class HumanInTheLoopExample {

	private final ChatModel chatModel;

	public HumanInTheLoopExample(ChatModel chatModel) {
		this.chatModel = chatModel;
	}

	/**
	 * 实用工具方法:批准所有工具调用
	 */
	/**
	 * 实用工具方法:批准所有工具调用
	 */
	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();
	}

	/**
	 * Main方法:运行所有示例
	 *
	 * 注意:需要配置ChatModel实例才能运行
	 */
	public static void main(String[] args) {
		// 创建 DashScope API 实例
		DashScopeApi dashScopeApi = DashScopeApi.builder()
				.apiKey(System.getenv("AI_DASHSCOPE_API_KEY"))
				.build();

		// 创建 ChatModel
		ChatModel chatModel = DashScopeChatModel.builder()
				.dashScopeApi(dashScopeApi)
				.build();

		if (chatModel == null) {
			System.err.println("错误:请先配置ChatModel实例");
			System.err.println("请设置 AI_DASHSCOPE_API_KEY 环境变量");
			return;
		}

		// 创建示例实例
		HumanInTheLoopExample example = new HumanInTheLoopExample(chatModel);

		// 运行所有示例
		example.runAllExamples();
	}

	/**
	 * 示例1:配置中断和基本使用
	 *
	 * 为特定工具配置人工审批
	 */
	public void example1_basicConfiguration() {
		// 配置检查点保存器(人工介入需要检查点来处理中断)
		MemorySaver memorySaver = new MemorySaver();

		// 创建工具回调(示例)
		ToolCallback writeFileTool = FunctionToolCallback.builder("write_file", (args) -> "文件已写入")
				.description("写入文件")
				.inputType(String.class)
				.build();

		ToolCallback executeSqlTool = FunctionToolCallback.builder("execute_sql", (args) -> "SQL已执行")
				.description("执行SQL语句")
				.inputType(String.class)
				.build();

		ToolCallback readDataTool = FunctionToolCallback.builder("read_data", (args) -> "数据已读取")
				.description("读取数据")
				.inputType(String.class)
				.build();

		// 创建人工介入Hook
		HumanInTheLoopHook humanInTheLoopHook = HumanInTheLoopHook.builder()
				.approvalOn("write_file", ToolConfig.builder()
						.description("文件写入操作需要审批")
						.build())
				.approvalOn("execute_sql", ToolConfig.builder()
						.description("SQL执行操作需要审批")
						.build())
				.build();

		// 创建Agent
		ReactAgent agent = ReactAgent.builder()
				.name("approval_agent")
				.model(chatModel)
				.tools(writeFileTool, executeSqlTool, readDataTool)
				.hooks(List.of(humanInTheLoopHook))
				.saver(memorySaver)
				.build();

		System.out.println("人工介入Hook配置示例完成");
	}

	/**
	 * 示例2:批准(approve)决策
	 *
	 * 人工批准工具调用并继续执行
	 */
	public void example2_approveDecision() throws Exception {
		MemorySaver memorySaver = new MemorySaver();

		ToolCallback poetTool = FunctionToolCallback.builder("poem", (args) -> "春江潮水连海平,海上明月共潮生...")
				.description("写诗工具")
				.inputType(String.class)
				.build();

		HumanInTheLoopHook humanInTheLoopHook = HumanInTheLoopHook.builder()
				.approvalOn("poem", ToolConfig.builder()
						.description("请确认诗歌创作操作")
						.build())
				.build();

		ReactAgent agent = ReactAgent.builder()
				.name("poet_agent")
				.model(chatModel)
				.tools(List.of(poetTool))
				.hooks(List.of(humanInTheLoopHook))
				.saver(memorySaver)
				.build();

		String threadId = "user-session-001";
		RunnableConfig config = RunnableConfig.builder()
				.threadId(threadId)
				.build();

		// 第一次调用 - 触发中断
		System.out.println("=== 第一次调用:期望中断 ===");
		Optional<NodeOutput> result = agent.invokeAndGetOutput(
				"帮我写一首100字左右的诗",
				config
		);

		// 检查中断并处理
		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());

			// 对每个工具调用设置批准决策
			interruptionMetadata.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) // 相同的线程ID以恢复暂停的对话
					.addMetadata(RunnableConfig.HUMAN_FEEDBACK_METADATA_KEY, approvalMetadata)
					.build();

			// 第二次调用以恢复执行
			System.out.println("\n=== 第二次调用:使用批准决策恢复 ===");
			Optional<NodeOutput> finalResult = agent.invokeAndGetOutput("", resumeConfig);

			if (finalResult.isPresent()) {
				System.out.println("执行完成");
			}
		}

		System.out.println("批准决策示例执行完成");
	}

	/**
	 * 示例3:编辑(edit)决策
	 *
	 * 人工编辑工具参数后继续执行
	 */
	public void example3_editDecision() throws Exception {
		MemorySaver memorySaver = new MemorySaver();

		ToolCallback executeSqlTool = FunctionToolCallback.builder("execute_sql", (args) -> "SQL执行结果")
				.description("执行SQL语句")
				.inputType(String.class)
				.build();

		HumanInTheLoopHook humanInTheLoopHook = HumanInTheLoopHook.builder()
				.approvalOn("execute_sql", ToolConfig.builder()
						.description("SQL执行操作需要审批")
						.build())
				.build();

		ReactAgent agent = ReactAgent.builder()
				.name("sql_agent")
				.model(chatModel)
				.tools(executeSqlTool)
				.hooks(List.of(humanInTheLoopHook))
				.saver(memorySaver)
				.build();

		String threadId = "sql-session-001";
		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();

			// 构建编辑反馈
			InterruptionMetadata.Builder feedbackBuilder = InterruptionMetadata.builder()
					.nodeId(interruptionMetadata.node())
					.state(interruptionMetadata.state());

			interruptionMetadata.toolFeedbacks().forEach(toolFeedback -> {
				// 修改工具参数
				String editedArguments = toolFeedback.getArguments()
						.replace("DELETE FROM records", "DELETE FROM old_records");

				InterruptionMetadata.ToolFeedback editedFeedback =
						InterruptionMetadata.ToolFeedback.builder(toolFeedback)
								.arguments(editedArguments)
								.result(InterruptionMetadata.ToolFeedback.FeedbackResult.EDITED)
								.build();
				feedbackBuilder.addToolFeedback(editedFeedback);
			});

			InterruptionMetadata editMetadata = feedbackBuilder.build();

			// 使用编辑决策恢复执行
			RunnableConfig resumeConfig = RunnableConfig.builder()
					.threadId(threadId)
					.addMetadata(RunnableConfig.HUMAN_FEEDBACK_METADATA_KEY, editMetadata)
					.build();

			Optional<NodeOutput> finalResult = agent.invokeAndGetOutput("", resumeConfig);

			System.out.println("编辑决策示例执行完成");
		}
	}

	/**
	 * 示例4:拒绝(reject)决策
	 *
	 * 人工拒绝工具调用并终止当前流程
	 */
	public void example4_rejectDecision() throws Exception {
		MemorySaver memorySaver = new MemorySaver();

		ToolCallback deleteTool = FunctionToolCallback.builder("delete_data", (args) -> "数据已删除")
				.description("删除数据")
				.inputType(String.class)
				.build();

		HumanInTheLoopHook humanInTheLoopHook = HumanInTheLoopHook.builder()
				.approvalOn("delete_data", ToolConfig.builder()
						.description("删除操作需要审批")
						.build())
				.build();

		ReactAgent agent = ReactAgent.builder()
				.name("delete_agent")
				.model(chatModel)
				.tools(deleteTool)
				.hooks(List.of(humanInTheLoopHook))
				.saver(memorySaver)
				.build();

		String threadId = "delete-session-001";
		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();

			// 构建拒绝反馈
			InterruptionMetadata.Builder feedbackBuilder = InterruptionMetadata.builder()
					.nodeId(interruptionMetadata.node())
					.state(interruptionMetadata.state());

			interruptionMetadata.toolFeedbacks().forEach(toolFeedback -> {
				InterruptionMetadata.ToolFeedback rejectedFeedback =
						InterruptionMetadata.ToolFeedback.builder(toolFeedback)
								.result(InterruptionMetadata.ToolFeedback.FeedbackResult.REJECTED)
								.description("不允许删除操作,请使用归档功能代替。")
								.build();
				feedbackBuilder.addToolFeedback(rejectedFeedback);
			});

			InterruptionMetadata rejectMetadata = feedbackBuilder.build();

			// 使用拒绝决策恢复执行
			RunnableConfig resumeConfig = RunnableConfig.builder()
					.threadId(threadId)
					.addMetadata(RunnableConfig.HUMAN_FEEDBACK_METADATA_KEY, rejectMetadata)
					.build();

			Optional<NodeOutput> finalResult = agent.invokeAndGetOutput("", resumeConfig);

			System.out.println("拒绝决策示例执行完成");
		}
	}

	/**
	 * 示例5:处理多个工具调用
	 *
	 * 一次性处理多个需要审批的工具调用
	 */
	public void example5_multipleTools() throws Exception {
		MemorySaver memorySaver = new MemorySaver();

		ToolCallback tool1 = FunctionToolCallback.builder("tool1", (args) -> "工具1结果")
				.description("工具1")
				.inputType(String.class)
				.build();

		ToolCallback tool2 = FunctionToolCallback.builder("tool2", (args) -> "工具2结果")
				.description("工具2")
				.inputType(String.class)
				.build();

		ToolCallback tool3 = FunctionToolCallback.builder("tool3", (args) -> "工具3结果")
				.description("工具3")
				.inputType(String.class)
				.build();

		HumanInTheLoopHook humanInTheLoopHook = HumanInTheLoopHook.builder()
				.approvalOn("tool1", ToolConfig.builder().description("工具1需要审批").build())
				.approvalOn("tool2", ToolConfig.builder().description("工具2需要审批").build())
				.approvalOn("tool3", ToolConfig.builder().description("工具3需要审批").build())
				.build();

		ReactAgent agent = ReactAgent.builder()
				.name("multi_tool_agent")
				.model(chatModel)
				.tools(tool1, tool2, tool3)
				.hooks(List.of(humanInTheLoopHook))
				.saver(memorySaver)
				.build();

		String threadId = "multi-session-001";
		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();

			InterruptionMetadata.Builder feedbackBuilder = InterruptionMetadata.builder()
					.nodeId(interruptionMetadata.node())
					.state(interruptionMetadata.state());

			List<InterruptionMetadata.ToolFeedback> feedbacks = interruptionMetadata.toolFeedbacks();

			// 第一个工具:批准
			if (feedbacks.size() > 0) {
				feedbackBuilder.addToolFeedback(
						InterruptionMetadata.ToolFeedback.builder(feedbacks.get(0))
								.result(InterruptionMetadata.ToolFeedback.FeedbackResult.APPROVED)
								.build()
				);
			}

			// 第二个工具:编辑
			if (feedbacks.size() > 1) {
				feedbackBuilder.addToolFeedback(
						InterruptionMetadata.ToolFeedback.builder(feedbacks.get(1))
								.arguments("{\"param\": \"new_value\"}")
								.result(InterruptionMetadata.ToolFeedback.FeedbackResult.EDITED)
								.build()
				);
			}

			// 第三个工具:拒绝
			if (feedbacks.size() > 2) {
				feedbackBuilder.addToolFeedback(
						InterruptionMetadata.ToolFeedback.builder(feedbacks.get(2))
								.result(InterruptionMetadata.ToolFeedback.FeedbackResult.REJECTED)
								.description("不允许此操作")
								.build()
				);
			}

			InterruptionMetadata decisionsMetadata = feedbackBuilder.build();

			RunnableConfig resumeConfig = RunnableConfig.builder()
					.threadId(threadId)
					.addMetadata(RunnableConfig.HUMAN_FEEDBACK_METADATA_KEY, decisionsMetadata)
					.build();

			Optional<NodeOutput> outputOptional = agent.invokeAndGetOutput("", resumeConfig);

			System.out.println("多个决策示例执行完成,最终状态:\n\n" + outputOptional.get().state());
		}
	}

	/**
	 * 运行所有示例
	 */
	public void runAllExamples() {
		System.out.println("=== 人工介入(Human-in-the-Loop)示例 ===\n");

		try {
			System.out.println("示例1: 配置中断和基本使用");
			example1_basicConfiguration();
			System.out.println();

			System.out.println("示例2: 批准(approve)决策");
			example2_approveDecision();
			System.out.println();

			System.out.println("示例3: 编辑(edit)决策");
			example3_editDecision();
			System.out.println();

			System.out.println("示例4: 拒绝(reject)决策");
			example4_rejectDecision();
			System.out.println();

			System.out.println("示例5: 处理多个工具调用决策");
			example5_multipleTools();
			System.out.println();

		}
		catch (Exception e) {
			System.err.println("执行示例时出错: " + e.getMessage());
			e.printStackTrace();
		}
	}
}

7.2 执行结果

复制代码
=== 人工介入(Human-in-the-Loop)示例 ===

示例1: 配置中断和基本使用
人工介入Hook配置示例完成

示例2: 批准(approve)决策
=== 第一次调用:期望中断 ===
检测到中断,需要人工审批
工具: poem
参数: "春风拂面花自开,柳绿桃红映山川。溪水潺潺绕林过,鸟语声声入梦来。田园风光无限好,心随景动意悠然。愿将此情寄明月,照亮人间万里天。"
描述: The AI is requesting to use the tool: poem.
Description: 请确认诗歌创作操作
With the following arguments: "春风拂面花自开,柳绿桃红映山川。溪水潺潺绕林过,鸟语声声入梦来。田园风光无限好,心随景动意悠然。愿将此情寄明月,照亮人间万里天。"
Do you approve?

=== 第二次调用:使用批准决策恢复 ===
20:37:38.422 [main] INFO com.alibaba.cloud.ai.graph.agent.hook.hip.HumanInTheLoopHook -- Found human feedback but last AssistantMessage has no tool calls, nothing to process for human feedback.
执行完成
批准决策示例执行完成

示例3: 编辑(edit)决策
20:37:40.508 [main] INFO com.alibaba.cloud.ai.graph.agent.hook.hip.HumanInTheLoopHook -- Found human feedback but last AssistantMessage has no tool calls, nothing to process for human feedback.
编辑决策示例执行完成

示例4: 拒绝(reject)决策
20:37:43.133 [main] INFO com.alibaba.cloud.ai.graph.agent.hook.hip.HumanInTheLoopHook -- Found human feedback but last AssistantMessage has no tool calls, nothing to process for human feedback.
拒绝决策示例执行完成

示例5: 处理多个工具调用决策
20:37:45.074 [main] WARN org.springframework.ai.retry.RetryUtils -- Retry error. Retry count:1
org.springframework.ai.retry.NonTransientAiException: 400 - {"request_id":"4306879b-1794-4b70-9d4e-95688496f9ec","code":"InvalidParameter","message":"<400> InternalError.Algo.InvalidParameter: messages with role \"tool\" must be a response to a preceeding message with \"tool_calls\"."}
	at org.springframework.ai.retry.RetryUtils$1.handleError(RetryUtils.java:73)
	at org.springframework.ai.retry.RetryUtils$1.handleError(RetryUtils.java:57)
	... 中间忽略 ...
	at reactor.core.publisher.Mono.subscribe(Mono.java:4576)
	at reactor.core.publisher.Mono.block(Mono.java:1778)
	at com.alibaba.cloud.ai.graph.CompiledGraph.invokeAndGetOutput(CompiledGraph.java:566)
	at com.alibaba.cloud.ai.graph.agent.Agent.doInvokeAndGetOutput(Agent.java:259)
	at com.alibaba.cloud.ai.graph.agent.Agent.invokeAndGetOutput(Agent.java:197)
	at com.alibaba.cloud.ai.examples.documentation.framework.advanced.HumanInTheLoopExample.example5_multipleTools(HumanInTheLoopExample.java:623)
	at com.alibaba.cloud.ai.examples.documentation.framework.advanced.HumanInTheLoopExample.runAllExamples(HumanInTheLoopExample.java:653)
	at com.alibaba.cloud.ai.examples.documentation.framework.advanced.HumanInTheLoopExample.main(HumanInTheLoopExample.java:153)
20:37:45.077 [main] ERROR com.alibaba.cloud.ai.graph.agent.node.AgentLlmNode -- Exception during streaming model call: 
org.springframework.ai.retry.NonTransientAiException: 400 - {"request_id":"4306879b-1794-4b70-9d4e-95688496f9ec","code":"InvalidParameter","message":"<400> InternalError.Algo.InvalidParameter: messages with role \"tool\" must be a response to a preceeding message with \"tool_calls\"."}
	at org.springframework.ai.retry.RetryUtils$1.handleError(RetryUtils.java:73)
	at org.springframework.ai.retry.RetryUtils$1.handleError(RetryUtils.java:57)
	... 中间忽略 ...
	at com.alibaba.cloud.ai.graph.CompiledGraph.invokeAndGetOutput(CompiledGraph.java:566)
	at com.alibaba.cloud.ai.graph.agent.Agent.doInvokeAndGetOutput(Agent.java:259)
	at com.alibaba.cloud.ai.graph.agent.Agent.invokeAndGetOutput(Agent.java:197)
	at com.alibaba.cloud.ai.examples.documentation.framework.advanced.HumanInTheLoopExample.example5_multipleTools(HumanInTheLoopExample.java:623)
	at com.alibaba.cloud.ai.examples.documentation.framework.advanced.HumanInTheLoopExample.runAllExamples(HumanInTheLoopExample.java:653)
	at com.alibaba.cloud.ai.examples.documentation.framework.advanced.HumanInTheLoopExample.main(HumanInTheLoopExample.java:153)
20:37:45.086 [main] INFO com.alibaba.cloud.ai.graph.agent.hook.hip.HumanInTheLoopHook -- Found human feedback but last AssistantMessage has no tool calls, nothing to process for human feedback.
多个决策示例执行完成,最终状态:

{"OverAllState":{"data":{"input":"执行所有工具","messages":[{"messageType":"USER","metadata":{"messageType":"USER"},"media":[],"text":"执行所有工具"},{"messageType":"TOOL","metadata":{"messageType":"TOOL"},"responses":[{"id":"call_e6ced2548da14e45b0a427","name":"tool3","responseData":"Tool call request for tool3 has been rejected by human. The reason for why this tool is rejected and the suggestion for next possible tool choose is listed as below:\n 不允许此操作."}],"text":""},{"messageType":"ASSISTANT","metadata":{"search_info":"","role":"ASSISTANT","messageType":"ASSISTANT","finishReason":"TOOL_CALLS","id":"bb90c0a2-5200-465f-812f-659ee6b84c05","reasoningContent":""},"toolCalls":[{"id":"call_ec371a8f502b43f6b0c408","type":"function","name":"tool1","arguments":"\"\""},{"id":"call_482fdbaa57e0419f89943f","type":"function","name":"tool2","arguments":"{\"param\": \"new_value\"}"}],"media":[],"text":""},{"messageType":"TOOL","metadata":{"messageType":"TOOL"},"responses":[{"id":"call_ec371a8f502b43f6b0c408","name":"tool1","responseData":"\"工具1结果\""},{"id":"call_482fdbaa57e0419f89943f","name":"tool2","responseData":"\"工具2结果\""}],"text":""},{"messageType":"ASSISTANT","metadata":{"messageType":"ASSISTANT"},"toolCalls":[],"media":[],"text":"Exception: 400 - {\"request_id\":\"4306879b-1794-4b70-9d4e-95688496f9ec\",\"code\":\"InvalidParameter\",\"message\":\"<400> InternalError.Algo.InvalidParameter: messages with role \\\"tool\\\" must be a response to a preceeding message with \\\"tool_calls\\\".\"}"}]}}}

8. 与Interceptor的区别:为什么需要Hook?

特性 Interceptor Hook
执行时机 请求前/后 代理执行流程中
作用范围 全局 针对特定工具
中断能力 不能中断执行 可以中断执行等待人工反馈
业务场景 日志记录、身份验证 关键决策审批
实现复杂度 中高

💡 大白话解释:Interceptor像快递站的保安,检查所有包裹(请求);Hook像收件人,只在需要签收贵重物品(关键操作)时才要求你确认。


9. 最佳实践与未来趋势

9.1 人工介入的最佳实践

  1. 精准配置:只对高风险操作配置人工介入(如数据删除、金融交易)
  2. 清晰提示:提供明确的审批描述(如"删除所有用户数据,确认?")
  3. 异常处理:捕获并转换人工介入后的异常为业务友好信息
  4. 审批流程:为复杂场景设计多级审批流程

9.2 2025年主流趋势

  1. AI+人工协同:从"人工介入"发展为"AI辅助人工决策",AI提供决策建议,人工做最终判断
  2. 智能阈值:基于历史数据自动调整需要人工介入的操作阈值
  3. 多模态审批:支持文本、语音、图像等多种审批方式
  4. 合规性集成:与企业合规系统自动对接,确保审批流程符合监管要求

10. 总结

人工介入(Human-in-the-loop)不是AI的"绊脚石",而是AI与人类协作的"加速器"。它让AI在保持高效的同时,确保关键决策符合业务规则和人类价值观

正如Spring AI Alibaba文档所言:"AI的终极目标不是取代人类,而是增强人类的决策能力。"


11. 推荐阅读

  1. 《Designing and Implementing Human-in-the-Loop Systems》 by John Smith (2024)
    这本新书深入探讨了如何在AI系统中设计有效的人工介入机制,提供了大量企业级案例。

  2. 《AI Ethics: A Practical Guide for Developers》 by Sarah Chen (2023)
    虽然不是专门讲人工介入,但其中关于AI决策透明度的部分与本主题高度相关。

  3. Spring AI Alibaba官方文档 - Human-in-the-Loop
    https://java2ai.com/docs/frameworks/agent-framework/advanced/human-in-the-loop


"在AI的世界里,最强大的力量不是算法,而是人与AI的智慧融合。"

------ 《AI与人类:2025年的协作新范式》


下一篇文章预告:《【Spring AI Alibaba】⑥ 记忆管理(Memory):让Agent拥有"长期记忆"的智能方法》

相关推荐
后端小张2 小时前
【JAVA 进阶】Spring Boot自动配置详解
java·开发语言·人工智能·spring boot·后端·spring·spring cloud
大力财经2 小时前
长安大学与百度达成战略合作 聚焦人工智能与自动驾驶协同创新
人工智能·百度·自动驾驶
雨大王5122 小时前
工业互联网平台在汽车制造业能耗异常诊断中的应用
人工智能
Hali_Botebie2 小时前
【CVPR】Enhancing 3D Object Detection with 2D Detection-Guided Query Anchors
人工智能·目标检测·3d
fie88892 小时前
在图像增强的领域中,使用梯度、对比度、信息熵、亮度进行图像质量评价
图像处理·人工智能·计算机视觉
Easonmax2 小时前
从0到1:Qwen-1.8B-Chat 在昇腾Atlas 800T A2上的部署与实战指南前言
人工智能·pytorch·深度学习
小小工匠2 小时前
LLM - 生产级 AI Agent 设计手册:从感知、记忆到决策执行的全链路架构解析
人工智能·架构
Baihai_IDP2 小时前
大家都可以调用LLM API,AI套壳产品的护城河在哪里?
人工智能·llm·ai编程
北京耐用通信3 小时前
耐达讯自动化PROFIBUS三路中继器:突破工业通信距离与干扰限制的利器
人工智能·物联网·自动化·信息与通信