15-13 AgentScope Java进阶:Human-in-the-Loop
✅AgentScope Java进阶:Human-in-the-Loop
Human-in-the-Loop我们之前也讲过,这是一种允许在 Agent 执行过程中暂停、人工审查、交互确认后继续执行的机制。这一机制使得 AI Agent 可以在涉及敏感操作、信息缺失或需要人类决策的场景下安全、可控地运行。
前面在介绍ReAct Agent的实现原理的时候提到过,ReActAgent每一轮迭代只干2件事,推理和行动。
整个过程对应用层是自动运转的,agent.call(userMsg).block() 一次调用要么成功返回最终答案,要么抛异常。HITL 要解决的核心问题就一句话:怎么在这个全自动循环里安全地插入一个”暂停—等用户—继续”的缝隙。
注意”安全”二字 —— 暂停后 memory 里可能留着没填 result 的 ToolUseBlock,下一次调用必须能恢复,不能让模型看到悬空状态。
敏感动作前确认
HIL的一个最常见的场景:模型想调 delete_file,我希望先弹出 “确认/取消”。
在ReActAgent.reasoning() 里,看 510-543 行:
1 | .flat |
这里面有一个暂停标记检查的动作,也就是说,如果这时候event.isStopRequested() 如果为true的话,那么就会直接进入HIL了,
那么这个event是啥呢?通过查看代码我们能知道他是PostReasoningEvent,而如果我们有一个Hook可以监听这个事件,并且把他的isStopRequested设置为true,就能触发HIL了。
于是我们可以这么做:
1 | Hook confirmationHook = new Hook() { |
通过PostReasoningEvent的stopAgent方法,来把isStopRequested设置为true:
1 | public void stopAgent() { |
Agent在执行时,如果触发了HIL用户拿到的 Msg 里一定带着 ToolUseBlock(还没执行),所以判断条件就是它。如果用户同意,则继续执行,否则提示失败。
1 | Msg resp = agent.call(userMsg).block(); |
如果想要继续执行,会执行到acting方法中,而这个方法的第一句就是获取pending的ToolUseBlock,然后把他执行完。
1 | private Mono<Msg> acting(int iter) { |
关键动作后确认
前面是在执行前人工审查,但有的需求是工具已经跑完,结果也要给人审一遍(比如生成了一封邮件草稿,让人看完再决定要不要发下一步动作)。
ReActAgent.acting() 605-619 行:
1 | return Flux.fromIterable(successPairs) .concat |
同样判断event.isStopRequested() ,如果为true,触发HIL。只不过这里的Event是PostActingEvent。
LLM主动找用户确认
前面两种都是”ASJ框架替用户拦截”,但还有一类需求:LLM 自己意识到信息不够,想反过来问用户。例如健身教练 agent 缺少”用户身高体重”,想弹出一个表单让用户填。
如果还走 Hook 路径,相当于要在外面写状态机猜什么时候问什么——很别扭。设计者的解法是给 LLM 一把钥匙:让它调一个工具,工具的实现就是抛ToolSuspendException异常。
1 | // 取自 examples/advanced/hitl/UserInteractionTool.java@Tool(name = "ask_user", description = "Ask the user for clarification ...")public String askUser(@ToolParam("question") String question, @ToolParam("ui_type") String uiType, @ToolParam("options") List<String> options, @ToolParam("fields") List<Map<String,Object>> fields, ...) { throw new ToolSuspendException(question);} |
1 | // tool/ToolSuspendException.javapublic class ToolSuspendException extends RuntimeException { private final String reason; public ToolSuspendException(String reason) { ... }} |
LLM 在系统提示里被告知 “需要更多信息时调 ask_user,参数自己填”,于是它生成的 ToolUseBlock.input 里就带着 question/ui_type/options——这些参数本身就是 UI 描述。
在ASJ中会单独处理这种ToolSuspendException:
acting() 588-630 行:
1 | List<...> successPairs = results.stream().filter(e -> !e.getValue().isSuspended()).to |
buildSuspendedMsg 把 ToolUseBlock + 占位的 pending ToolResultBlock 一起塞进消息:
1 | private Msg buildSuspendedMsg(...) { |
前端读 ToolUseBlock.input 渲染 UI,用户填完,按 toolId 构造 ToolResult 注回:
1 | ToolResultBlock answer = ToolResultBlock.of( toolId, "ask_user", TextBlock.builder().text("User responded: " + userInput).build());Msg responseMsg = Msg.builder().role(MsgRole.TOOL).content(answer).build();agent.call(responseMsg).block(); |
兜底策略
看完HIL的实现之后,不知道你会不会和我一样有个疑问:如果应用层忘了恢复怎么办?
比如暂停后用户直接刷新页面,新一轮发了一条普通问题进来。这时 memory 里还有”悬空的 ToolUseBlock”,下一轮 reasoning 一进 LLM 就会报错。
为此ASJ默认搞了一个 PendingToolRecoveryHook(priority=10,最先跑):
1 | private Mono<PreCallEvent> handlePreCall(PreCallEvent event) { |
它只在”用户没按规则恢复”时兜底。如果你做了完整的状态持久化(比如把 session 序列化到 DB),可以 Builder.enablePendingToolRecovery(false) 关掉,自己掌控。
Demo
1 | import io.agentscope.core.ReActAgent; |
