第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机制:
- UserAgent:专门用于用户交互的Agent,支持可插拔的输入方法
- 工具确认Hook:通过
stopAgent()暂停Agent执行,等待人工确认 - GenerateReason:标识Agent暂停的原因,便于外部处理
- 分级确认:根据操作风险级别采用不同确认策略
- 异步审批:支持与外部审批系统集成
- gotoReasoning:支持人工干预后重新推理
HITL机制是构建安全、可控AI应用的关键组件,特别适用于涉及敏感操作、需要专业判断或要求质量保证的场景。