第18章 Human-in-the-Loop

第18章 Human-in-the-Loop

Human-in-the-Loop(HITL,人在回路)是一种将人类决策集成到AI系统中的关键模式。在Agent执行敏感操作或需要专业判断时,HITL机制允许人类介入、审核和控制Agent的行为。本章将详细介绍AgentScope-Java中的HITL实现。

18.1 HITL概述

18.1.1 为什么需要Human-in-the-Loop

┌─────────────────────────────────────────────────────────────────┐
│                     HITL 应用场景                                │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  ┌─────────────┐   安全关键操作                                  │
│  │  金融交易    │   ← 大额转账需要人工确认                        │
│  │  数据删除    │   ← 不可逆操作需要审批                          │
│  │  权限变更    │   ← 敏感操作需要多重确认                        │
│  └─────────────┘                                                 │
│                                                                  │
│  ┌─────────────┐   专业判断                                      │
│  │  医疗诊断    │   ← AI建议需要医生确认                          │
│  │  法律合规    │   ← 自动合规检查需要法务审核                    │
│  │  内容审核    │   ← AI标记的内容需要人工复核                    │
│  └─────────────┘                                                 │
│                                                                  │
│  ┌─────────────┐   质量保证                                      │
│  │  代码提交    │   ← AI生成代码需要Review                        │
│  │  文档发布    │   ← 自动生成内容需要编辑审核                    │
│  │  决策确认    │   ← 重要决策需要人工复核                        │
│  └─────────────┘                                                 │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

18.1.2 AgentScope-Java的HITL架构

AgentScope-Java提供了多层次的HITL支持:

层次机制用途
Agent层UserAgent桥接用户输入和Agent系统
执行层Hook系统拦截和暂停Agent执行
工具层Tool暂停暂停特定工具执行等待确认
消息层GenerateReason标识Agent暂停原因
┌─────────────────────────────────────────────────────────────────┐
│                        HITL 执行流程                             │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  用户输入 → ReActAgent → LLM推理 → Hook检查 → 工具执行           │
│                              ↓                                   │
│                      ┌───────────────┐                           │
│                      │ 检测到敏感操作 │                           │
│                      └───────┬───────┘                           │
│                              ↓                                   │
│                      ┌───────────────┐                           │
│                      │ stopAgent()   │ ← 暂停执行                │
│                      └───────┬───────┘                           │
│                              ↓                                   │
│                      ┌───────────────┐                           │
│                      │ 返回待确认消息 │ ← GenerateReason标识      │
│                      └───────┬───────┘                           │
│                              ↓                                   │
│                      ┌───────────────┐                           │
│                      │ 等待人工确认   │ ← UserAgent或其他输入    │
│                      └───────┬───────┘                           │
│                              ↓                                   │
│                      ┌───────────────┐                           │
│                      │ 恢复执行      │ ← agent.call()            │
│                      └───────────────┘                           │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

18.2 UserAgent详解

18.2.1 UserAgent核心设计

UserAgent是AgentScope-Java中专门用于处理用户交互的Agent:

/**
 * UserAgent - 用户交互代理
 * 
 * 设计理念:
 * - 不管理Memory,只捕获用户输入
 * - 通过可插拔的UserInputBase获取输入
 * - 支持简单文本和结构化输入
 * - 可参与MsgHub多智能体对话
 */
public class UserAgent extends AgentBase {
    
    // 可插拔的输入方法
    private UserInputBase inputMethod;
    
    // 核心方法:获取用户输入
    public Mono<Msg> getUserInput(
            List<Msg> contextMessages,   // 上下文消息(可选)
            Class<?> structuredModel     // 结构化模型(可选)
    ) {
        return inputMethod
            .handleInput(getAgentId(), getName(), contextMessages, structuredModel)
            .map(this::createMessageFromInput)
            .doOnNext(this::printMessage);
    }
}

18.2.2 基本使用

// 创建默认的UserAgent(使用控制台输入)
UserAgent user = UserAgent.builder()
    .name("User")
    .build();

// 获取用户输入
Msg userInput = user.call().block();
System.out.println("用户说: " + userInput.getText());

// 与AI Agent协作
ReActAgent assistant = ReActAgent.builder()
    .name("Assistant")
    .model(model)
    .build();

// 对话循环
while (true) {
    // 获取用户输入
    Msg input = user.call().block();
    if ("exit".equalsIgnoreCase(input.getText())) {
        break;
    }
    
    // AI响应
    Msg response = assistant.call(input).block();
    System.out.println("[Assistant]: " + response.getText());
}

18.2.3 UserInputBase接口

/**
 * 用户输入策略接口
 * 支持不同输入源(控制台、Web UI、程序化输入)
 */
public interface UserInputBase {
    
    /**
     * 处理用户输入
     * 
     * @param agentId Agent标识
     * @param agentName Agent名称
     * @param contextMessages 可选的上下文消息
     * @param structuredModel 可选的结构化输入模型
     * @return 包含用户输入数据的Mono
     */
    Mono<UserInputData> handleInput(
        String agentId,
        String agentName,
        List<Msg> contextMessages,
        Class<?> structuredModel
    );
}

/**
 * 用户输入数据
 */
public class UserInputData {
    private List<ContentBlock> blocksInput;     // 内容块输入
    private Map<String, Object> structuredInput; // 结构化输入
}

18.2.4 自定义输入方法

import io.agentscope.core.agent.user.*;
import io.agentscope.core.message.*;
import reactor.core.publisher.Mono;

/**
 * Web UI输入实现
 * 从Web前端获取用户输入
 */
public class WebUIUserInput implements UserInputBase {
    
    private final WebSocketSession session;
    private final ObjectMapper objectMapper;
    
    public WebUIUserInput(WebSocketSession session) {
        this.session = session;
        this.objectMapper = new ObjectMapper();
    }
    
    @Override
    public Mono<UserInputData> handleInput(
            String agentId,
            String agentName,
            List<Msg> contextMessages,
            Class<?> structuredModel) {
        
        // 发送上下文消息到前端
        if (contextMessages != null && !contextMessages.isEmpty()) {
            sendToFrontend(contextMessages);
        }
        
        // 等待用户输入
        return session.receive()
            .next()  // 获取下一条消息
            .map(payload -> parseUserInput(payload, structuredModel));
    }
    
    private void sendToFrontend(List<Msg> messages) {
        try {
            String json = objectMapper.writeValueAsString(
                Map.of("type", "context", "messages", messages)
            );
            session.send(json).subscribe();
        } catch (Exception e) {
            throw new RuntimeException("Failed to send context", e);
        }
    }
    
    private UserInputData parseUserInput(String payload, Class<?> structuredModel) {
        try {
            JsonNode node = objectMapper.readTree(payload);
            
            List<ContentBlock> blocks = new ArrayList<>();
            blocks.add(new TextBlock(node.get("text").asText()));
            
            // 处理可能的附件
            if (node.has("attachments")) {
                for (JsonNode attachment : node.get("attachments")) {
                    if ("image".equals(attachment.get("type").asText())) {
                        blocks.add(ImageBlock.builder()
                            .source(new URLSource(attachment.get("url").asText()))
                            .build());
                    }
                }
            }
            
            // 处理结构化输入
            Map<String, Object> structured = null;
            if (structuredModel != null && node.has("structured")) {
                structured = objectMapper.convertValue(
                    node.get("structured"),
                    new TypeReference<Map<String, Object>>() {}
                );
            }
            
            return new UserInputData(blocks, structured);
        } catch (Exception e) {
            throw new RuntimeException("Failed to parse user input", e);
        }
    }
}

// 使用自定义输入方法
WebUIUserInput webInput = new WebUIUserInput(webSocketSession);

UserAgent user = UserAgent.builder()
    .name("WebUser")
    .inputMethod(webInput)
    .build();

18.2.5 程序化输入方法

/**
 * 程序化输入实现
 * 用于测试或自动化场景
 */
public class ProgrammaticUserInput implements UserInputBase {
    
    private final Queue<String> inputQueue;
    private final Sinks.Many<String> inputSink;
    
    public ProgrammaticUserInput() {
        this.inputQueue = new ConcurrentLinkedQueue<>();
        this.inputSink = Sinks.many().multicast().onBackpressureBuffer();
    }
    
    /**
     * 提交输入(从外部调用)
     */
    public void submitInput(String input) {
        inputSink.tryEmitNext(input);
    }
    
    /**
     * 预设输入队列(用于测试)
     */
    public void queueInputs(String... inputs) {
        inputQueue.addAll(Arrays.asList(inputs));
    }
    
    @Override
    public Mono<UserInputData> handleInput(
            String agentId,
            String agentName,
            List<Msg> contextMessages,
            Class<?> structuredModel) {
        
        // 优先使用预设队列
        String queued = inputQueue.poll();
        if (queued != null) {
            return Mono.just(createInputData(queued));
        }
        
        // 否则等待实时输入
        return inputSink.asFlux()
            .next()
            .map(this::createInputData);
    }
    
    private UserInputData createInputData(String text) {
        return new UserInputData(
            List.of(new TextBlock(text)),
            null
        );
    }
}

// 使用示例:自动化测试
ProgrammaticUserInput programInput = new ProgrammaticUserInput();
programInput.queueInputs(
    "你好",
    "帮我查询天气",
    "上海",
    "谢谢"
);

UserAgent testUser = UserAgent.builder()
    .name("TestUser")
    .inputMethod(programInput)
    .build();

18.3 工具确认机制

18.3.1 ToolConfirmationHook

通过Hook机制实现工具调用前的人工确认:

import io.agentscope.core.hook.*;
import io.agentscope.core.message.*;
import reactor.core.publisher.Mono;

/**
 * 工具确认Hook
 * 在危险工具执行前暂停Agent等待人工确认
 */
public class ToolConfirmationHook implements Hook {
    
    // 需要确认的危险工具集合
    private final Set<String> dangerousTools;
    
    public ToolConfirmationHook() {
        this.dangerousTools = new HashSet<>();
    }
    
    public ToolConfirmationHook(Set<String> dangerousTools) {
        this.dangerousTools = new HashSet<>(dangerousTools);
    }
    
    /**
     * 添加危险工具
     */
    public void addDangerousTool(String toolName) {
        dangerousTools.add(toolName);
    }
    
    /**
     * 检查工具是否危险
     */
    public boolean isDangerous(String toolName) {
        return dangerousTools.contains(toolName);
    }
    
    @Override
    public <T extends HookEvent> Mono<T> onEvent(T event) {
        if (event instanceof PostReasoningEvent postReasoning) {
            Msg reasoningMsg = postReasoning.getReasoningMessage();
            if (reasoningMsg == null) {
                return Mono.just(event);
            }
            
            // 检查是否有危险工具调用
            List<ToolUseBlock> toolCalls = 
                reasoningMsg.getContentBlocks(ToolUseBlock.class);
            
            boolean hasDangerousTool = toolCalls.stream()
                .anyMatch(tool -> dangerousTools.contains(tool.getName()));
            
            if (hasDangerousTool) {
                // 暂停Agent执行,等待人工确认
                postReasoning.stopAgent();
            }
        }
        
        return Mono.just(event);
    }
}

18.3.2 GenerateReason枚举

Agent返回的消息包含GenerateReason标识暂停原因:

/**
 * Agent消息生成原因
 */
public enum GenerateReason {
    
    /** 模型正常停止,任务完成 */
    MODEL_STOP,
    
    /** 模型返回工具调用(内部工具,框架会继续执行) */
    TOOL_CALLS,
    
    /** 结构化输出完成 */
    STRUCTURED_OUTPUT,
    
    /** 工具执行被暂停,等待用户提供结果 */
    TOOL_SUSPENDED,
    
    /** 推理阶段被Hook暂停(PostReasoningEvent.stopAgent()) */
    REASONING_STOP_REQUESTED,
    
    /** 执行阶段被Hook暂停(PostActingEvent.stopAgent()) */
    ACTING_STOP_REQUESTED,
    
    /** Agent被中断 */
    INTERRUPTED,
    
    /** 达到最大迭代次数 */
    MAX_ITERATIONS
}

18.3.3 完整的工具确认流程

// 1. 创建工具确认Hook
ToolConfirmationHook confirmHook = new ToolConfirmationHook(
    Set.of("delete_file", "send_email", "execute_sql", "transfer_money")
);

// 2. 创建带Hook的Agent
ReActAgent agent = ReActAgent.builder()
    .name("SecureAssistant")
    .model(model)
    .tools(fileTools, emailTools, sqlTools)
    .hooks(confirmHook)
    .build();

// 3. 执行并处理确认
Msg userRequest = Msg.user("删除所有临时文件");
Msg response = agent.call(userRequest).block();

// 4. 检查是否需要确认
if (response.getGenerateReason() == GenerateReason.REASONING_STOP_REQUESTED) {
    // 提取待确认的工具调用
    List<ToolUseBlock> pendingTools = response.getContentBlocks(ToolUseBlock.class);
    
    System.out.println("以下操作需要确认:");
    for (ToolUseBlock tool : pendingTools) {
        System.out.println("  - " + tool.getName() + ": " + tool.getArguments());
    }
    
    // 获取用户确认
    System.out.print("是否执行?(yes/no): ");
    Scanner scanner = new Scanner(System.in);
    String confirmation = scanner.nextLine();
    
    if ("yes".equalsIgnoreCase(confirmation)) {
        // 恢复执行
        Msg finalResponse = agent.call().block();
        System.out.println("执行完成: " + finalResponse.getText());
    } else {
        System.out.println("操作已取消");
    }
} else {
    // 正常完成
    System.out.println("响应: " + response.getText());
}

18.4 高级HITL模式

18.4.1 分级确认策略

/**
 * 分级确认Hook
 * 根据操作风险级别采用不同确认策略
 */
public class TieredConfirmationHook implements Hook {
    
    public enum RiskLevel {
        LOW,      // 低风险:自动通过
        MEDIUM,   // 中风险:日志记录,可配置确认
        HIGH,     // 高风险:必须人工确认
        CRITICAL  // 关键:需要多人确认
    }
    
    private final Map<String, RiskLevel> toolRiskLevels;
    private final RiskLevel autoApproveThreshold;
    
    public TieredConfirmationHook(RiskLevel autoApproveThreshold) {
        this.toolRiskLevels = new HashMap<>();
        this.autoApproveThreshold = autoApproveThreshold;
    }
    
    public void setToolRiskLevel(String toolName, RiskLevel level) {
        toolRiskLevels.put(toolName, level);
    }
    
    @Override
    public <T extends HookEvent> Mono<T> onEvent(T event) {
        if (event instanceof PostReasoningEvent e) {
            Msg msg = e.getReasoningMessage();
            if (msg == null) return Mono.just(event);
            
            List<ToolUseBlock> toolCalls = msg.getContentBlocks(ToolUseBlock.class);
            
            // 找出最高风险级别
            RiskLevel maxRisk = toolCalls.stream()
                .map(tool -> toolRiskLevels.getOrDefault(tool.getName(), RiskLevel.LOW))
                .max(Comparator.comparingInt(RiskLevel::ordinal))
                .orElse(RiskLevel.LOW);
            
            // 根据风险级别决定是否暂停
            if (maxRisk.ordinal() > autoApproveThreshold.ordinal()) {
                e.stopAgent();
            }
        }
        
        return Mono.just(event);
    }
}

// 使用分级确认
TieredConfirmationHook tieredHook = new TieredConfirmationHook(RiskLevel.LOW);
tieredHook.setToolRiskLevel("read_file", RiskLevel.LOW);
tieredHook.setToolRiskLevel("write_file", RiskLevel.MEDIUM);
tieredHook.setToolRiskLevel("delete_file", RiskLevel.HIGH);
tieredHook.setToolRiskLevel("format_disk", RiskLevel.CRITICAL);

18.4.2 参数级别审核

/**
 * 参数级别审核Hook
 * 对特定参数值进行审核
 */
public class ParameterAuditHook implements Hook {
    
    // 参数审核规则
    private final Map<String, List<ParameterRule>> toolRules;
    
    @FunctionalInterface
    public interface ParameterRule {
        boolean requiresApproval(String paramName, Object value);
    }
    
    public ParameterAuditHook() {
        this.toolRules = new HashMap<>();
    }
    
    public void addRule(String toolName, ParameterRule rule) {
        toolRules.computeIfAbsent(toolName, k -> new ArrayList<>()).add(rule);
    }
    
    @Override
    public <T extends HookEvent> Mono<T> onEvent(T event) {
        if (event instanceof PostReasoningEvent e) {
            Msg msg = e.getReasoningMessage();
            if (msg == null) return Mono.just(event);
            
            for (ToolUseBlock tool : msg.getContentBlocks(ToolUseBlock.class)) {
                List<ParameterRule> rules = toolRules.get(tool.getName());
                if (rules == null) continue;
                
                Map<String, Object> args = tool.getArguments();
                for (ParameterRule rule : rules) {
                    for (Map.Entry<String, Object> entry : args.entrySet()) {
                        if (rule.requiresApproval(entry.getKey(), entry.getValue())) {
                            e.stopAgent();
                            return Mono.just(event);
                        }
                    }
                }
            }
        }
        
        return Mono.just(event);
    }
}

// 使用参数审核
ParameterAuditHook auditHook = new ParameterAuditHook();

// 审核大额转账
auditHook.addRule("transfer_money", (param, value) -> {
    if ("amount".equals(param) && value instanceof Number) {
        return ((Number) value).doubleValue() > 10000;
    }
    return false;
});

// 审核批量删除
auditHook.addRule("delete_records", (param, value) -> {
    if ("count".equals(param) && value instanceof Number) {
        return ((Number) value).intValue() > 100;
    }
    return false;
});

// 审核敏感路径
auditHook.addRule("write_file", (param, value) -> {
    if ("path".equals(param) && value instanceof String) {
        String path = (String) value;
        return path.startsWith("/etc/") || path.startsWith("/system/");
    }
    return false;
});

18.4.3 异步审批流程

/**
 * 异步审批Hook
 * 支持异步审批工作流
 */
public class AsyncApprovalHook implements Hook {
    
    private final ApprovalService approvalService;
    private final Duration timeout;
    
    public AsyncApprovalHook(ApprovalService approvalService, Duration timeout) {
        this.approvalService = approvalService;
        this.timeout = timeout;
    }
    
    @Override
    public <T extends HookEvent> Mono<T> onEvent(T event) {
        if (event instanceof PreActingEvent e) {
            ToolUseBlock toolUse = e.getToolUse();
            
            // 检查是否需要审批
            if (approvalService.requiresApproval(toolUse.getName())) {
                return submitForApproval(e)
                    .flatMap(approved -> {
                        if (!approved) {
                            // 拒绝执行,替换为错误结果
                            e.setSkipTool(true);
                            e.setToolResult(ToolResultBlock.builder()
                                .toolUseId(toolUse.getId())
                                .output(List.of(new TextBlock("Operation rejected by approver")))
                                .build());
                        }
                        return Mono.just(event);
                    });
            }
        }
        
        return Mono.just(event);
    }
    
    private Mono<Boolean> submitForApproval(PreActingEvent event) {
        ToolUseBlock tool = event.getToolUse();
        
        ApprovalRequest request = ApprovalRequest.builder()
            .requestId(UUID.randomUUID().toString())
            .agentName(event.getAgentName())
            .toolName(tool.getName())
            .arguments(tool.getArguments())
            .requestTime(Instant.now())
            .build();
        
        return approvalService.submitRequest(request)
            .timeout(timeout)
            .onErrorReturn(false);  // 超时视为拒绝
    }
}

/**
 * 审批服务接口
 */
public interface ApprovalService {
    
    boolean requiresApproval(String toolName);
    
    Mono<Boolean> submitRequest(ApprovalRequest request);
}

/**
 * Slack审批服务实现
 */
public class SlackApprovalService implements ApprovalService {
    
    private final SlackClient slackClient;
    private final String approvalChannel;
    private final Map<String, CompletableFuture<Boolean>> pendingApprovals;
    
    @Override
    public Mono<Boolean> submitRequest(ApprovalRequest request) {
        CompletableFuture<Boolean> future = new CompletableFuture<>();
        pendingApprovals.put(request.getRequestId(), future);
        
        // 发送Slack消息
        sendApprovalMessage(request);
        
        return Mono.fromFuture(future);
    }
    
    private void sendApprovalMessage(ApprovalRequest request) {
        String message = String.format(
            ":robot_face: *工具执行审批请求*\n" +
            "请求ID: `%s`\n" +
            "Agent: %s\n" +
            "工具: `%s`\n" +
            "参数: ```%s```\n" +
            "请回复 `/approve %s` 或 `/reject %s`",
            request.getRequestId(),
            request.getAgentName(),
            request.getToolName(),
            formatArguments(request.getArguments()),
            request.getRequestId(),
            request.getRequestId()
        );
        
        slackClient.sendMessage(approvalChannel, message);
    }
    
    // 处理Slack命令回调
    public void handleApproval(String requestId, boolean approved) {
        CompletableFuture<Boolean> future = pendingApprovals.remove(requestId);
        if (future != null) {
            future.complete(approved);
        }
    }
}

18.5 MsgHub中的用户参与

18.5.1 用户作为多智能体对话参与者

// 创建参与对话的Agent
UserAgent user = UserAgent.builder()
    .name("User")
    .build();

ReActAgent analyst = ReActAgent.builder()
    .name("Analyst")
    .model(model)
    .formatter(new OpenAIMultiAgentFormatter())
    .systemPrompt("你是数据分析师,负责分析数据并提出建议")
    .build();

ReActAgent reviewer = ReActAgent.builder()
    .name("Reviewer")
    .model(model)
    .formatter(new OpenAIMultiAgentFormatter())
    .systemPrompt("你是审核员,负责审核分析结果的准确性")
    .build();

// 使用MsgHub进行多方对话
try (MsgHub hub = MsgHub.create(user, analyst, reviewer)) {
    // 用户发起分析请求
    Msg request = Msg.user("请分析上季度销售数据");
    hub.broadcast(request);
    
    // 分析师响应
    Msg analysis = analyst.call(request).block();
    hub.broadcast(analysis);
    
    // 审核员审核
    Msg review = reviewer.call().block();
    hub.broadcast(review);
    
    // 用户确认或提问
    Msg userFeedback = user.call().block();
    hub.broadcast(userFeedback);
    
    // 继续对话...
}

18.5.2 人工介入的工作流

/**
 * 人工介入的多Agent工作流
 */
public class HumanInLoopWorkflow {
    
    private final UserAgent user;
    private final ReActAgent worker;
    private final ReActAgent supervisor;
    
    public Mono<Msg> execute(String task) {
        return Mono.defer(() -> {
            // 1. Worker执行任务
            Msg workerResult = worker.call(Msg.user(task)).block();
            
            // 2. Supervisor审核
            Msg supervisorReview = supervisor.call(workerResult).block();
            
            // 3. 检查是否需要人工介入
            if (needsHumanIntervention(supervisorReview)) {
                // 显示当前状态给用户
                displayStatus(workerResult, supervisorReview);
                
                // 获取用户决定
                Msg userDecision = user.call(
                    List.of(workerResult, supervisorReview),
                    null
                ).block();
                
                // 根据用户决定继续
                return handleUserDecision(userDecision, workerResult);
            }
            
            return Mono.just(workerResult);
        });
    }
    
    private boolean needsHumanIntervention(Msg review) {
        String text = review.getText().toLowerCase();
        return text.contains("需要确认") || 
               text.contains("不确定") ||
               text.contains("建议人工检查");
    }
    
    private void displayStatus(Msg work, Msg review) {
        System.out.println("=== 任务执行结果 ===");
        System.out.println(work.getText());
        System.out.println("\n=== 审核意见 ===");
        System.out.println(review.getText());
        System.out.println("\n请选择: [1]批准 [2]修改 [3]拒绝");
    }
    
    private Mono<Msg> handleUserDecision(Msg decision, Msg originalWork) {
        String text = decision.getText().trim();
        
        if ("1".equals(text) || text.contains("批准")) {
            return Mono.just(originalWork);
        } else if ("2".equals(text) || text.contains("修改")) {
            // 获取修改意见
            System.out.print("请输入修改意见: ");
            Msg modification = user.call().block();
            
            // 重新执行
            return execute("根据以下修改意见重新执行: " + modification.getText());
        } else {
            return Mono.just(Msg.user("任务已取消"));
        }
    }
}

18.6 gotoReasoning重新推理

18.6.1 PostReasoningEvent的gotoReasoning

当Hook需要修改Agent的推理结果并让Agent重新推理时:

/**
 * 重新推理Hook
 * 在特定条件下让Agent重新思考
 */
public class ReasoningCorrectionHook implements Hook {
    
    @Override
    public <T extends HookEvent> Mono<T> onEvent(T event) {
        if (event instanceof PostReasoningEvent e) {
            Msg reasoning = e.getReasoningMessage();
            
            // 检查推理结果是否需要纠正
            if (needsCorrection(reasoning)) {
                // 添加提示消息让Agent重新推理
                Msg hint = Msg.builder()
                    .role(MsgRole.USER)
                    .content(new TextBlock(
                        "请注意:你的回答可能不够准确,请重新思考并给出更详细的分析。"))
                    .build();
                
                // 触发重新推理
                e.gotoReasoning(hint);
            }
        }
        
        return Mono.just(event);
    }
    
    private boolean needsCorrection(Msg reasoning) {
        if (reasoning == null) return false;
        String text = reasoning.getText();
        
        // 检查是否有不确定的表达
        return text.contains("我不确定") || 
               text.contains("可能") ||
               text.length() < 50;  // 回答太短
    }
}

18.6.2 工具结果替换

/**
 * 工具结果替换Hook
 * 在工具执行后替换结果并重新推理
 */
public class ToolResultOverrideHook implements Hook {
    
    @Override
    public <T extends HookEvent> Mono<T> onEvent(T event) {
        if (event instanceof PostReasoningEvent e) {
            Msg reasoning = e.getReasoningMessage();
            if (reasoning == null) return Mono.just(event);
            
            List<ToolUseBlock> toolCalls = reasoning.getContentBlocks(ToolUseBlock.class);
            
            for (ToolUseBlock tool : toolCalls) {
                if ("get_weather".equals(tool.getName())) {
                    // 替换天气工具的结果
                    ToolResultBlock overrideResult = ToolResultBlock.builder()
                        .toolUseId(tool.getId())
                        .output(List.of(new TextBlock(
                            "天气数据来自内部系统:晴天,温度25°C")))
                        .build();
                    
                    Msg toolResultMsg = Msg.builder()
                        .role(MsgRole.TOOL)
                        .content(overrideResult)
                        .build();
                    
                    // 跳过工具执行,使用替换的结果重新推理
                    e.gotoReasoning(toolResultMsg);
                    return Mono.just(event);
                }
            }
        }
        
        return Mono.just(event);
    }
}

18.7 最佳实践

18.7.1 HITL设计原则

原则说明实践
最小干预只在必要时请求人工介入使用风险分级确定干预阈值
清晰上下文提供足够信息供决策展示完整的操作意图和参数
超时处理设置合理的等待超时超时后安全降级或拒绝
审计追踪记录所有人工决策保存确认记录和决策人
异步友好支持异步审批流程使用消息队列或通知系统

18.7.2 常见错误避免

// 错误:在Hook中阻塞等待
@Override
public <T extends HookEvent> Mono<T> onEvent(T event) {
    // ❌ 不要在Hook中直接阻塞
    Scanner scanner = new Scanner(System.in);
    String input = scanner.nextLine();  // 阻塞!
    
    // ✅ 正确做法:使用stopAgent()暂停
    if (event instanceof PostReasoningEvent e) {
        e.stopAgent();  // 暂停Agent,让外部处理确认
    }
    return Mono.just(event);
}

// 错误:未处理暂停状态
Msg response = agent.call(userMsg).block();
System.out.println(response.getText());  // ❌ 可能是暂停消息

// ✅ 正确做法:检查GenerateReason
Msg response = agent.call(userMsg).block();
if (response.getGenerateReason() == GenerateReason.REASONING_STOP_REQUESTED) {
    handlePendingApproval(response);
} else {
    System.out.println(response.getText());
}

18.7.3 安全考虑

/**
 * 安全增强的确认Hook
 */
public class SecureConfirmationHook implements Hook {
    
    private final Set<String> criticalTools;
    private final AuditLogger auditLogger;
    private final AuthorizationService authService;
    
    @Override
    public <T extends HookEvent> Mono<T> onEvent(T event) {
        if (event instanceof PostReasoningEvent e) {
            List<ToolUseBlock> tools = e.getReasoningMessage()
                .getContentBlocks(ToolUseBlock.class);
            
            for (ToolUseBlock tool : tools) {
                if (criticalTools.contains(tool.getName())) {
                    // 记录审计日志
                    auditLogger.logPendingApproval(
                        e.getAgentName(),
                        tool.getName(),
                        tool.getArguments()
                    );
                    
                    // 暂停等待确认
                    e.stopAgent();
                    break;
                }
            }
        }
        
        return Mono.just(event);
    }
    
    /**
     * 验证确认权限
     */
    public boolean validateApproval(String userId, String toolName) {
        // 检查用户是否有权限确认此操作
        return authService.hasApprovalPermission(userId, toolName);
    }
}

18.8 本章小结

本章详细介绍了AgentScope-Java的Human-in-the-Loop机制:

  1. UserAgent:专门用于用户交互的Agent,支持可插拔的输入方法
  2. 工具确认Hook:通过stopAgent()暂停Agent执行,等待人工确认
  3. GenerateReason:标识Agent暂停的原因,便于外部处理
  4. 分级确认:根据操作风险级别采用不同确认策略
  5. 异步审批:支持与外部审批系统集成
  6. gotoReasoning:支持人工干预后重新推理

HITL机制是构建安全、可控AI应用的关键组件,特别适用于涉及敏感操作、需要专业判断或要求质量保证的场景。

← 返回目录