文章目录
- [1. HumanInTheLoopHook](#1. HumanInTheLoopHook)
-
- [1.1 概述](#1.1 概述)
- [1.2 afterModel()](#1.2 afterModel())
- [1.3 apply()](#1.3 apply())
- [1.4 interrupt()](#1.4 interrupt())
- [2. 核心数据结构](#2. 核心数据结构)
-
- [2.1 ToolConfig](#2.1 ToolConfig)
- [2.2 InterruptionMetadata](#2.2 InterruptionMetadata)
- [2.3 ToolFeedback](#2.3 ToolFeedback)
- [2.4 FeedbackResult](#2.4 FeedbackResult)
- [3. 执行流程](#3. 执行流程)
1. HumanInTheLoopHook
1.1 概述
HumanInTheLoopHook是 Spring AI Alibaba Agent Framework 提供的 HITL(Human-in-the-Loop)人工介入钩子实现。它允许在 Agent 执行过程中,对敏感工具调用进行暂停并等待人类审批,从而实现安全可控的 Agent 自主执行。
核心特性:
- 支持三种审批决策:
APPROVED(批准)、REJECTED(拒绝)、EDITED(编辑) - 支持工具白名单配置,只有配置的工具需要审批
- 自动批准未配置的工具,保证流程顺畅
- 支持多工具同时请求的审批场景
执行时机:
- 钩子位置:
AFTER_MODEL(模型调用之后) - 中断检查:在节点
action执行之前(interrupt方法) - 反馈处理:在模型返回之后(
afterModel方法)
实现了接口/类:
ModelHook:模型钩子抽象基类,提供beforeModel()和afterModel()方法AsyncNodeActionWithConfig:异步节点动作接口,定义apply(state, config)方法InterruptableAction:可中断动作接口,定义interrupt()方法
java
@HookPositions(HookPosition.AFTER_MODEL) // 标注钩子执行位置:模型调用之后
public class HumanInTheLoopHook extends ModelHook implements AsyncNodeActionWithConfig, InterruptableAction {
private static final Logger log = LoggerFactory.getLogger(HumanInTheLoopHook.class);
/**
* HITL 节点名称,用于在 Graph 中标识该钩子
*/
public static final String HITL_NODE_NAME = "HITL";
/**
* 需要人工审批的工具配置映射表
* Key: 工具名称
* Value: 工具配置(包含描述信息)
*/
private Map<String, ToolConfig> approvalOn;
//..................
1.2 afterModel()
重写了 ModelHook#afterModel() ,在模型调用返回结果后执行,主要逻辑:
- 检查是否存在人类反馈元数据
- 获取最后一个
AssistantMessage(包含工具调用) - 根据人类反馈处理每个工具调用:
APPROVED: 保留原始工具调用,正常执行EDITED: 使用修改后的参数替换REJECTED: 添加拒绝响应消息
- 构建状态更新,移除旧消息并添加新消息
java
/**
* 模型调用后的核心处理方法
*
* @param state 当前 agent 状态
* @param config 运行配置(包含人类反馈元数据)
* @return 状态更新的 CompletableFuture
*/
@Override
public CompletableFuture<Map<String, Object>> afterModel(OverAllState state, RunnableConfig config) {
// 从配置中获取并移除人类反馈元数据
// getMetadataAndRemove 确保反馈只被消费一次
Optional<InterruptionMetadata> feedback = config.getMetadataAndRemove(
RunnableConfig.HUMAN_FEEDBACK_METADATA_KEY,
new TypeRef<InterruptionMetadata>() { }
);
InterruptionMetadata interruptionMetadata = feedback.orElse(null);
// 如果没有人类反馈,说明是首次调用或无需处理,直接返回空结果
if (interruptionMetadata == null) {
log.debug("No human feedback found in the runnable config metadata, no tool to execute or none needs feedback.");
return CompletableFuture.completedFuture(Map.of());
}
// 获取最后一个 AssistantMessage(包含待审批的工具调用)
AssistantMessage assistantMessage = getLastAssistantMessage(state);
if (assistantMessage != null) {
// 检查是否有工具调用
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 = ToolResponseMessage.builder().responses(responses).build();
// 遍历所有工具调用,根据人类反馈进行处理
for (AssistantMessage.ToolCall toolCall : assistantMessage.getToolCalls()) {
// 查找当前工具调用对应的反馈
Optional<ToolFeedback> toolFeedbackOpt = interruptionMetadata.toolFeedbacks().stream()
.filter(tf -> tf.getId().equals(toolCall.id())) // 通过工具调用 ID 匹配
.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(), // 保持 ID 不变
toolCall.type(), // 保持类型不变
toolCall.name(), // 保持名称不变
toolFeedback.getArguments() // 使用编辑后的参数
);
newToolCalls.add(editedToolCall);
}
else if (result == FeedbackResult.REJECTED) {
// 【拒绝】保留工具调用但添加拒绝响应
// 这样模型会知道工具被拒绝了,并可能选择其他工具
newToolCalls.add(toolCall);
// 构建拒绝响应消息
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 (!newToolCalls.isEmpty()) {
// 创建新的 AssistantMessage,使用更新后的工具调用列表
newMessages.add(AssistantMessage.builder()
.content(assistantMessage.getText()) // 保持原始内容
.properties(assistantMessage.getMetadata()) // 保持元数据
.toolCalls(newToolCalls) // 使用更新后的工具调用
.media(assistantMessage.getMedia()) // 保持媒体信息
.build());
// 添加删除标记,移除旧的 AssistantMessage
newMessages.add(new RemoveByHash<>(assistantMessage));
}
// 如果有被拒绝的工具,添加拒绝响应消息
// ToolResponseMessage 必须放在 AssistantMessage 之后
if (!rejectedMessage.getResponses().isEmpty()) {
newMessages.add(rejectedMessage);
}
// 更新 messages 状态
updates.put("messages", newMessages);
return CompletableFuture.completedFuture(updates);
}
else {
// 异常情况:最后一条消息不是 AssistantMessage
log.warn("Last message is not an AssistantMessage, cannot process human feedback.");
}
return CompletableFuture.completedFuture(Map.of());
}
1.3 apply()
实现了 AsyncNodeActionWithConfig#apply() 方法,作为节点执行时,直接委托给 afterModel 方法处理:
java
/**
* 异步节点动作接口的实现方法
* 委托给 afterModel 方法处理
*
* @param state 当前 agent 状态
* @param config 运行配置
* @return 状态更新的 CompletableFuture
*/
@Override
public CompletableFuture<Map<String, Object>> apply(OverAllState state, RunnableConfig config) {
return afterModel(state, config);
}
1.4 interrupt()
实现了 InterruptableAction#interrupt() 方法,在节点 action 的 apply() 方法之前调用,用于判断是否需要中断执行等待人类审批。
检查逻辑:
- 获取最后一个
AssistantMessage - 检查是否有工具调用
- 检查是否已有反馈元数据(恢复执行场景)
- 验证反馈的有效性
- 如果没有反馈或验证失败,构建中断元数据
java
/**
* 中断检查方法
*
* @param nodeId 当前节点 ID
* @param state 当前 agent 状态
* @param config 运行配置
* @return 中断元数据的 Optional,空表示不中断
*/
@Override
public Optional<InterruptionMetadata> interrupt(String nodeId, OverAllState state, RunnableConfig config) {
// 获取最后一个 AssistantMessage
AssistantMessage lastMessage = getLastAssistantMessage(state);
// 如果没有消息或没有工具调用,不需要中断
if (lastMessage == null || !lastMessage.hasToolCalls()) {
return Optional.empty();
}
// 检查配置中是否包含人类反馈元数据(恢复执行场景)
Optional<Object> feedback = config.metadata(RunnableConfig.HUMAN_FEEDBACK_METADATA_KEY);
if (feedback.isPresent()) {
// 验证元数据类型
if (!(feedback.get() instanceof InterruptionMetadata)) {
throw new IllegalArgumentException("Human feedback metadata must be of type InterruptionMetadata.");
}
// 验证反馈的有效性
// 如果验证失败,说明反馈不完整,继续中断
if (!validateFeedback((InterruptionMetadata) feedback.get(), lastMessage.getToolCalls())) {
return buildInterruptionMetadata(state, lastMessage);
}
// 验证通过,不再中断,继续执行
return Optional.empty();
}
// 没有反馈元数据,需要中断等待人类输入
// 2. If last message is AssistantMessage
return buildInterruptionMetadata(state, lastMessage);
}
获取最后一个 AssistantMessage 方法:
java
/**
* 获取最后一个 AssistantMessage
*
* <p>遍历消息历史,找到最后一个未执行的 AssistantMessage。</p>
* <p>如果 AssistantMessage 后面紧跟 ToolResponseMessage,说明工具已执行,不返回。</p>
*
* @param state 当前 agent 状态
* @return 最后一个 AssistantMessage,如果没有返回 null
*/
private static AssistantMessage getLastAssistantMessage(OverAllState state) {
// 从状态中获取消息列表
List<Message> messages = (List<Message>) state.value("messages").orElse(List.of());
AssistantMessage lastMessage = null;
// 从后向前遍历消息历史
for (int i = messages.size() - 1; i >= 0; i--) {
Message msg = messages.get(i);
if (msg instanceof AssistantMessage assistantMessage) {
// 如果下一个消息是 ToolResponseMessage,说明工具已执行完成
// 此时不应该再处理这个 AssistantMessage
if (i + 1 < messages.size() && messages.get(i + 1) instanceof ToolResponseMessage) {
break;
}
// 找到目标消息
lastMessage = assistantMessage;
break;
}
}
return lastMessage;
}
构建中断元数据方法:
java
/**
* 构建中断元数据
*
* <p>遍历 AssistantMessage 中的所有工具调用,根据 approvalOn 配置
* 判断哪些需要审批,哪些自动批准。</p>
*
* @param state 当前 agent 状态
* @param lastMessage 最后一个 AssistantMessage
* @return 中断元数据的 Optional
*/
private Optional<InterruptionMetadata> buildInterruptionMetadata(OverAllState state, AssistantMessage lastMessage) {
boolean needsInterruption = false;
// 创建 InterruptionMetadata 构建器
// Hook.getFullHookName(this) 返回 "agent.hook.HITL"
InterruptionMetadata.Builder builder = InterruptionMetadata.builder(Hook.getFullHookName(this), state);
// 遍历所有工具调用
for (AssistantMessage.ToolCall toolCall : lastMessage.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?";
// 添加工具反馈到构建器
builder.addToolFeedback(ToolFeedback.builder()
.id(toolCall.id())
.name(toolCall.name())
.description(content)
.arguments(toolCall.arguments())
.build())
.build();
needsInterruption = true;
} else {
// 该工具不需要审批,自动批准
builder.addToolsAutomaticallyApproved(toolCall);
}
}
// 只有当有工具需要审批时才返回中断元数据
return needsInterruption ? Optional.of(builder.build()) : Optional.empty();
}
验证人类反馈的有效性方法:
java
/**
* 验证人类反馈的有效性
*
* <p>验证逻辑:</p>
* <ol>
* <li>检查反馈是否为空</li>
* <li>检查每个需要审批的工具都有对应的反馈</li>
* <li>检查反馈结果不为 null</li>
* <li>记录意外的反馈条目(不匹配任何待审批工具)</li>
* </ol>
*
* @param feedback 人类反馈元数据
* @param toolCalls 工具调用列表
* @return true 表示验证通过,false 表示验证失败
*/
private boolean validateFeedback(InterruptionMetadata feedback, List<AssistantMessage.ToolCall> toolCalls) {
// 空值检查
if (feedback == null || feedback.toolFeedbacks() == null || feedback.toolFeedbacks().isEmpty()) {
return false;
}
List<InterruptionMetadata.ToolFeedback> toolFeedbacks = feedback.toolFeedbacks();
// 1. 获取实际需要审批的工具列表(在 approvalOn 中配置的工具)
List<AssistantMessage.ToolCall> toolCallsNeedingApproval = toolCalls.stream()
.filter(tc -> approvalOn.containsKey(tc.name()))
.toList();
// 如果没有工具需要审批,验证通过
if (toolCallsNeedingApproval.isEmpty()) {
return true;
}
// 2. 对每个需要审批的工具,确保有对应的反馈且结果不为空
for (AssistantMessage.ToolCall call : toolCallsNeedingApproval) {
InterruptionMetadata.ToolFeedback matchedFeedback = toolFeedbacks.stream()
.filter(tf -> tf.getName().equals(call.name())
// 同时验证 ID,确保精确匹配
&& call.id().equals(tf.getId()))
.findFirst()
.orElse(null);
if (matchedFeedback == null) {
log.warn("Missing feedback for tool {} (id={}); waiting for human input.",
call.name(), call.id());
return false; // 缺少反馈
}
// 确保反馈结果已提供
if (matchedFeedback.getResult() == null) {
log.warn("Feedback result for tool {} (id={}) is null; waiting for human input.",
call.name(), call.id());
return false; // 反馈结果为空
}
}
// 3. 记录意外的反馈条目(不匹配任何待审批工具)
// 这种情况通常是前端传入了错误的工具 ID 或名称
for (InterruptionMetadata.ToolFeedback tf : toolFeedbacks) {
boolean matched = toolCallsNeedingApproval.stream()
.anyMatch(call -> call.name().equals(tf.getName()) && call.id().equals(tf.getId()));
if (!matched) {
log.warn("Ignoring unexpected tool feedback: name={}, id={}", tf.getName(), tf.getId());
}
}
return true; // 验证通过
}
2. 核心数据结构
2.1 ToolConfig
配置需要审批的工具及其描述信息。
java
public class ToolConfig {
private String description; // 工具描述,用于审批界面显示
// Builder 模式构建
public static Builder builder() {
return new Builder();
}
public static class Builder {
private String description;
public Builder description(String description) {
this.description = description;
return this;
}
public ToolConfig build() {
return new ToolConfig(this);
}
}
}
2.2 InterruptionMetadata
中断元数据继承自 NodeOutput ,用于记录图执行过程中的中断信息 和工具审批状态 ,是 human-in-the-loop 机制的核心数据结构。
核心职责:
- 中断记录:记录图在哪个节点中断
- 工具审批:记录需要人工审批的工具调用及审批结果
- 状态保存:保存中断时的图状态
- 元数据:支持携带自定义元数据
使用示例:
java
InterruptionMetadata metadata = InterruptionMetadata.builder("tool_node", state)
.addToolFeedback(new ToolFeedback("call_123", "deleteFile", "{}", APPROVED, "允许删除"))
.build();
// 检查审批结果
boolean allApproved = metadata.toolFeedbacks().stream()
.allMatch(fb -> fb.getResult() == FeedbackResult.APPROVED);
}
核心属性:
java
public final class InterruptionMetadata extends NodeOutput implements HasMetadata {
// ========== 继承自 NodeOutput ==========
private final String node; // 中断发生的节点 ID
private final OverAllState state; // 中断时的状态快照
/**
* 自定义元数据
*
* <p>用于携带业务相关的自定义信息,如:
* <ul>
* <li>中断原因</li>
* <li>用户 ID</li>
* <li>时间戳</li>
* <li>其他业务数据</li>
* </ul>
*/
private final Map<String, Object> metadata;
/**
* 自动批准的工具调用列表
*
* <p>记录不需要人工审批即可执行的工具调用。
* 通常用于已经过用户信任的工具或低风险操作。
*/
private List<AssistantMessage.ToolCall> toolsAutomaticallyApproved;
/**
* 工具反馈列表 - 核心属性
*
* <p>记录需要人工审批的工具调用及其审批结果。
* 每个反馈包含:
* <ul>
* <li>工具调用 ID</li>
* <li>工具名称</li>
* <li>工具参数</li>
* <li>审批结果 (APPROVED/REJECTED/EDITED)</li>
* <li>描述信息</li>
* </ul>
*/
private List<ToolFeedback> toolFeedbacks;
工具反馈内部类:
java
// ========== 内部类:工具反馈 ==========
public static class ToolFeedback {
String id; // 工具调用唯一 ID
String name; // 工具名称
String arguments; // 工具参数(JSON 字符串)
FeedbackResult result; // 审批结果
String description; // 描述/拒绝原因
public enum FeedbackResult {
APPROVED, // 批准执行
REJECTED, // 拒绝执行
EDITED // 编辑后执行
}
}
}
Builder 创建方法:
java
/**
* 私有构造函数 - 通过 Builder 创建
*
* @param builder Builder 实例
*/
private InterruptionMetadata(Builder builder) {
super(builder.nodeId, builder.state);
this.metadata = builder.metadata();
this.toolFeedbacks = new ArrayList<>(builder.toolFeedbacks);
if (builder.toolsAutomaticallyApproved != null) {
this.toolsAutomaticallyApproved = builder.toolsAutomaticallyApproved;
} else {
this.toolsAutomaticallyApproved = new ArrayList<>();
}
}
// ==================== Builder 工厂方法 ====================
/**
* 创建新的 Builder 实例
*
* @param nodeId 中断节点 ID
* @param state 中断时的状态
* @return 新的 Builder 实例
*/
public static Builder builder(String nodeId, OverAllState state) {
return new Builder(nodeId, state);
}
/**
* 创建新的空 Builder 实例
*
* @return 新的 Builder 实例
*/
public static Builder builder() {
return new Builder();
}
/**
* 从现有的 InterruptionMetadata 创建 Builder (复制)
*
* <p>用于修改现有的中断元数据。
*
* @param interruptionMetadata 现有的中断元数据
* @return 新的 Builder 实例,包含原有数据
*/
public static Builder builder(InterruptionMetadata interruptionMetadata) {
Builder builder = new Builder(interruptionMetadata.metadata().orElse(Map.of()))
.nodeId(interruptionMetadata.node())
.state(interruptionMetadata.state());
if (interruptionMetadata.getToolsAutomaticallyApproved() != null) {
builder.toolsAutomaticallyApproved(interruptionMetadata.getToolsAutomaticallyApproved());
}
return builder;
}
// ==================== Builder 类 ====================
/**
* InterruptionMetadata 的 Builder 类
*
* <p>支持链式调用的构建器,用于创建 InterruptionMetadata 实例。
*
* <p>使用示例:
* <pre>{@code
* InterruptionMetadata metadata = InterruptionMetadata.builder("tool_node", state)
* .addToolFeedback(feedback1)
* .addToolFeedback(feedback2)
* .addMetadata("reason", "waiting_for_approval")
* .build();
* }</pre>
*/
public static class Builder extends HasMetadata.Builder<Builder> {
/** 工具反馈列表 */
List<ToolFeedback> toolFeedbacks;
/** 自动批准的工具调用列表 */
List<AssistantMessage.ToolCall> toolsAutomaticallyApproved;
/** 中断节点 ID */
String nodeId;
/** 中断时的状态 */
OverAllState state;
/**
* 默认构造函数
*/
public Builder() {
this.toolFeedbacks = new ArrayList<>();
}
/**
* 构造函数 - 指定节点 ID 和状态
*
* @param nodeId 中断节点 ID
* @param state 中断时的状态
*/
public Builder(String nodeId, OverAllState state) {
this.nodeId = nodeId;
this.state = state;
this.toolFeedbacks = new ArrayList<>();
}
/**
* 构造函数 - 指定初始元数据
*
* @param metadata 初始元数据
*/
public Builder(Map<String, Object> metadata) {
super(metadata);
this.toolFeedbacks = new ArrayList<>();
}
/**
* 设置节点 ID
*
* @param nodeId 节点 ID
* @return Builder 实例 (支持链式调用)
*/
public Builder nodeId(String nodeId) {
this.nodeId = nodeId;
return this;
}
/**
* 设置状态
*
* @param state OverAllState 实例
* @return Builder 实例 (支持链式调用)
*/
public Builder state(OverAllState state) {
this.state = state;
return this;
}
/**
* 添加单个工具反馈
*
* @param toolFeedback 工具反馈
* @return Builder 实例 (支持链式调用)
*/
public Builder addToolFeedback(ToolFeedback toolFeedback) {
this.toolFeedbacks.add(toolFeedback);
return this;
}
/**
* 设置工具反馈列表 (覆盖现有列表)
*
* @param toolFeedbacks 工具反馈列表
* @return Builder 实例 (支持链式调用)
*/
public Builder toolFeedbacks(List<ToolFeedback> toolFeedbacks) {
this.toolFeedbacks = new ArrayList<>(toolFeedbacks);
return this;
}
/**
* 添加单个自动批准的工具调用
*
* @param toolCall 工具调用
* @return Builder 实例 (支持链式调用)
*/
public Builder addToolsAutomaticallyApproved(AssistantMessage.ToolCall toolCall) {
if (this.toolsAutomaticallyApproved == null) {
this.toolsAutomaticallyApproved = new ArrayList<>();
}
this.toolsAutomaticallyApproved.add(toolCall);
return this;
}
/**
* 设置自动批准的工具调用列表 (覆盖现有列表)
*
* @param toolsAutomaticallyApproved 工具调用列表
* @return Builder 实例 (支持链式调用)
*/
public Builder toolsAutomaticallyApproved(List<AssistantMessage.ToolCall> toolsAutomaticallyApproved) {
this.toolsAutomaticallyApproved = new ArrayList<>(toolsAutomaticallyApproved);
return this;
}
/**
* 构建 InterruptionMetadata 实例
*
* @return 新的 InterruptionMetadata 实例
*/
public InterruptionMetadata build() {
return new InterruptionMetadata(this);
}
}
2.3 ToolFeedback
ToolFeedback (工具反馈)内部类记录工具调用的审批结果, 是 HITL 机制的核心数据载体,连接中断请求和用户决策。
包含的信息:
id: 工具调用ID,唯一标识一次工具调用请求,来自AssistantMessage.ToolCall#id()name: 工具名称,如 "delete_file"、"execute_sql"arguments: 工具参数 (JSON字符串),如 "{\"path\": \"/data/file.txt\"}"result: 审批结果枚举FeedbackResult,决定工具如何执行description: 描述信息,中断时为提示信息,拒绝时为拒绝原因
使用示例:
java
// 从中断元数据获取待审批列表
List<ToolFeedback> pendingApprovals = interruptionMetadata.toolFeedbacks();
// 批准工具调用
ToolFeedback approved = ToolFeedback.builder(pendingApprovals.get(0))
.result(FeedbackResult.APPROVED)
.build();
// 编辑参数后批准
ToolFeedback edited = ToolFeedback.builder(pendingApprovals.get(0))
.arguments("{\"path\": \"/safe/location/file.txt\"}") // 替换原参数
.result(FeedbackResult.EDITED)
.build();
// 拒绝并提供原因(LLM 会收到此反馈)
ToolFeedback rejected = ToolFeedback.builder(pendingApprovals.get(0))
.result(FeedbackResult.REJECTED)
.description("不允许删除此文件,请使用归档功能")
.build();
源码如下:
java
public static class ToolFeedback {
/**
* 工具调用 ID - 唯一标识一次工具调用请求
*
* <p>来自 {@link AssistantMessage.ToolCall#id()},用于在恢复执行时匹配原始工具调用。
* 格式通常为 "call_xxxxx",由 LLM 生成。
*
* <p><b>重要性:</b> 在 multiple tool calls 场景中,通过 id 确保反馈匹配正确的工具调用。
*/
String id;
/**
* 工具名称 - 被请求调用的工具标识
*
* <p>如 "delete_file"、"execute_sql"、"shell" 等。
* 用于日志记录和用户界面展示。
*/
String name;
/**
* 工具参数 - JSON 格式的参数字符串
*
* <p>来自 {@link AssistantMessage.ToolCall#arguments()},包含工具执行所需的参数。
* 格式如: "{\"path\": \"/data/file.txt\", \"force\": true}"
*
* <p><b>EDITED 场景:</b> 用户可修改此参数,替换原参数执行工具。
*/
String arguments;
/**
* 审批结果 - 用户对工具调用的决策
*
* <p>{@link FeedbackResult#APPROVED}: 执行原参数
* <p>{@link FeedbackResult#EDITED}: 执行修改后的参数
* <p>{@link FeedbackResult#REJECTED}: 返回拒绝反馈给 LLM
*
* <p><b>中断状态:</b> result=null 表示等待用户决策
*/
FeedbackResult result;
/**
* 描述信息 - 提示信息或拒绝原因
*
* <p><b>中断时:</b> 包含工具审批提示信息,如 "文件删除操作需要审批,请确认文件路径是否正确"
* <p><b>拒绝时:</b> 包含拒绝原因,将作为 ToolResponseMessage 内容返回给 LLM
*/
String description;
/**
* 构造函数 - 创建 ToolFeedback 实例
*
* <p>通常通过 Builder 创建,而非直接调用构造函数。
*
* @param id 工具调用 ID,来自 AssistantMessage.ToolCall
* @param name 工具名称
* @param arguments 工具参数 (JSON 字符串)
* @param result 审批结果,中断时为 null
* @param description 描述信息
*/
public ToolFeedback(String id, String name, String arguments, FeedbackResult result, String description) {
this.id = id;
this.name = name;
this.arguments = arguments;
this.result = result;
this.description = description;
}
2.4 FeedbackResult
表示用户对工具调用的审批决定,是 HITL 机制的核心决策类型。
三种决策及其处理方式:
| 决策 | 含义 | Agent 处理 |
|---|---|---|
| APPROVED | 批准执行 | 保留原 toolCall,AgentToolNode 正常执行 |
| EDITED | 修改后执行 | 替换 arguments,AgentToolNode 使用新参数执行 |
| REJECTED | 拒绝执行 | 添加 ToolResponseMessage,LLM 收到反馈重新规划 |
java
public enum FeedbackResult {
APPROVED,
REJECTED,
EDITED;
}
3. 执行流程
生命周期:
LLM请求调用工具 →AssistantMessage.ToolCallHumanInTheLoopHook检测需审批工具 → 创建ToolFeedback (result=null)- 返回
InterruptionMetadata,等待人工决策 - 用户交互 → 设置
result(APPROVED/EDITED/REJECTED) - 构建反馈
InterruptionMetadata,恢复Agent执行 HumanInTheLoopHook.afterModel()根据决策处理工具调用
完整执行时序图:
