Spring AI Alibaba 1.x 系列【34】Human-in-the-Loop(人在回路)执行流程

文章目录

  • [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 概述

HumanInTheLoopHookSpring AI Alibaba Agent Framework 提供的 HITLHuman-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() ,在模型调用返回结果后执行,主要逻辑:

  1. 检查是否存在人类反馈元数据
  2. 获取最后一个 AssistantMessage(包含工具调用)
  3. 根据人类反馈处理每个工具调用:
    • APPROVED: 保留原始工具调用,正常执行
    • EDITED: 使用修改后的参数替换
    • REJECTED: 添加拒绝响应消息
  4. 构建状态更新,移除旧消息并添加新消息
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() 方法,在节点 actionapply() 方法之前调用,用于判断是否需要中断执行等待人类审批。

检查逻辑:

  1. 获取最后一个 AssistantMessage
  2. 检查是否有工具调用
  3. 检查是否已有反馈元数据(恢复执行场景)
  4. 验证反馈的有效性
  5. 如果没有反馈或验证失败,构建中断元数据
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. 执行流程

生命周期

  1. LLM 请求调用工具 → AssistantMessage.ToolCall
  2. HumanInTheLoopHook 检测需审批工具 → 创建 ToolFeedback (result=null)
  3. 返回 InterruptionMetadata,等待人工决策
  4. 用户交互 → 设置 result (APPROVED/EDITED/REJECTED)
  5. 构建反馈 InterruptionMetadata,恢复 Agent 执行
  6. HumanInTheLoopHook.afterModel() 根据决策处理工具调用

完整执行时序图:

相关推荐
nuoxin1142 小时前
CYUSB4024-FCAXI 是一款USB 20Gbps 控制器-富利威
网络·人工智能·嵌入式硬件·fpga开发·dsp开发
InfiniSynapse2 小时前
打工人ai效率工具:一键修改excel
大数据·人工智能·数据分析·excel·ai编程
m0_564914922 小时前
AI科技应用课
大数据·人工智能·机器学习
wechatbot8882 小时前
企业微信 iPad 协议接口全功能开发实战
汇编·人工智能·ios·微信·企业微信·ipad
Mr -老鬼2 小时前
Salvo Web框架专属AI智能体 - 让Rust开发效率翻倍
人工智能·后端·rust·智能体·salvo
最新快讯2 小时前
科技简报 | 2026年4月22日
人工智能·科技·机器人
好家伙VCC2 小时前
**发散创新:基于Solidity的通证经济模型设计与智能合约实现**在区块链技术日益成熟的今天,**通证经济(Token Econo
java·python·区块链·智能合约
jjjava2.02 小时前
计算机体系与进程管理全解析
java·开发语言
薛定e的猫咪2 小时前
2026 年 AI 编码多代理协作全景:Claude Code + Codex CLI 7 个开源工具深度评测
人工智能·开源·ai编程