📖目录
- 前言
- [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 核心流程
- 配置阶段 :通过
approvalOn指定哪些工具需要人工介入 - 执行阶段:Agent执行到需要人工介入的工具时暂停
- 审批阶段:系统等待人工反馈
- 恢复阶段:根据反馈决定继续执行、编辑参数或终止
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 人工介入的最佳实践
- 精准配置:只对高风险操作配置人工介入(如数据删除、金融交易)
- 清晰提示:提供明确的审批描述(如"删除所有用户数据,确认?")
- 异常处理:捕获并转换人工介入后的异常为业务友好信息
- 审批流程:为复杂场景设计多级审批流程
9.2 2025年主流趋势
- AI+人工协同:从"人工介入"发展为"AI辅助人工决策",AI提供决策建议,人工做最终判断
- 智能阈值:基于历史数据自动调整需要人工介入的操作阈值
- 多模态审批:支持文本、语音、图像等多种审批方式
- 合规性集成:与企业合规系统自动对接,确保审批流程符合监管要求
10. 总结
人工介入(Human-in-the-loop)不是AI的"绊脚石",而是AI与人类协作的"加速器"。它让AI在保持高效的同时,确保关键决策符合业务规则和人类价值观。
正如Spring AI Alibaba文档所言:"AI的终极目标不是取代人类,而是增强人类的决策能力。"
11. 推荐阅读
-
《Designing and Implementing Human-in-the-Loop Systems》 by John Smith (2024)
这本新书深入探讨了如何在AI系统中设计有效的人工介入机制,提供了大量企业级案例。 -
《AI Ethics: A Practical Guide for Developers》 by Sarah Chen (2023)
虽然不是专门讲人工介入,但其中关于AI决策透明度的部分与本主题高度相关。 -
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拥有"长期记忆"的智能方法》