react - 示例3 - 人工介入(HITL)
2026年8月3日 下午5:10:03
在自动化 AI Agent 应用中,人工介入(Human-In-The-Loop, HITL) 是确保业务合规的最后一道防线。(工具调用时)对于涉及大额转账、删除数据等敏感操作,系统会暂停执行并等待人类审批。
Solon AI 通过 HITLInterceptor 实现了标准化的 “拦截 - 决策 - 续传” 流程。
1、核心原理
- 声明式拦截:通过拦截器配置“敏感工具”。当 Agent 尝试调用这些工具(Tool Call)时,若无审批记录,则自动中断(
session.pending(true, ...))并生成 HITLTask 快照,同时通过流式事件HITLPendingEvent推送审批卡片。 - 状态保持:利用 AgentSession(如内存或 Redis)保留 Agent 的思考进度。
- 断点续传:管理员提交 HITLDecision 后,再次调用 agent.prompt().session(session).call(),拦截器会识别决策并继续完成剩余的任务(流式下通过
HITLDecidedEvent关闭审批卡片)。
2、示例代码
import demo.ai.llm.LlmUtil;
import org.noear.solon.annotation.*;
import org.noear.solon.ai.agent.AgentSession;
import org.noear.solon.ai.agent.react.ReActAgent;
import org.noear.solon.ai.agent.react.ReActResponse;
import org.noear.solon.ai.agent.react.intercept.*;
import org.noear.solon.ai.agent.session.InMemoryAgentSession;
import org.noear.solon.core.handle.Result;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Controller
@Mapping("/ai/hitl")
public class HitlWebController {
// 假设这是我们的 Agent 实例(实际开发中可由 Bean 注入)
private final ReActAgent agent = ReActAgent.of(LlmUtil.getChatModel())
.defaultToolAdd(null)
.defaultInterceptorAdd(new HITLInterceptor()
.onTool("transfer", (trace, args) ->{
double amount = Double.parseDouble(args.get("amount").toString());
return amount > 1000 ? "大额转账审批" : null;
}))
.build();
private final Map<String, AgentSession> agentSessionMap = new ConcurrentHashMap<>();
private final AgentSession getSession(String sid) {
return agentSessionMap.computeIfAbsent(sid, k -> InMemoryAgentSession.of(k));
}
/**
* 1. 提问接口:用户输入指令
* 如果触发转账 > 1000,Response 会返回中断状态,前端应引导至审批流
*/
@Post
@Mapping("ask")
public Result ask(String sid, String prompt) throws Throwable {
AgentSession session = getSession(sid);
// 执行 Agent 逻辑
ReActResponse resp = agent.prompt(prompt).session(session).call();
if (resp.getSession().isPending()) {
return Result.failure("REQUIRED_APPROVAL", HITL.getPendingTask(session));
}
return Result.succeed(resp.getContent());
}
/**
* 2. 任务查询:获取当前会话中挂起的任务详情
*/
@Get
@Mapping("task")
public HITLTask getTask(String sid) {
AgentSession session = getSession(sid);
return HITL.getPendingTask(session);
}
/**
* 3. 决策提交:管理员进行操作
*
* @param action: approve / reject
* @param modifiedArgs: 修正后的参数(可选)
*/
@Post
@Mapping("approve")
public Result approve(String sid, String action, @Body Map modifiedArgs) throws Throwable {
AgentSession session = getSession(sid);
HITLTask task = HITL.getPendingTask(session);
if (task == null) return Result.failure("没有挂起的任务");
// 构建决策
HITLDecision decision;
if ("approve".equals(action)) {
decision = HITLDecision.approve().comment("管理员已核实");
if (modifiedArgs != null && !modifiedArgs.isEmpty()) {
decision.modifiedArgs(modifiedArgs);
}
} else {
decision = HITLDecision.reject("风险操作,已被管理员驳回");
}
// 提交决策(4.0.4 主路径:面向 HITLTask;按 toolName 提交的旧接口已废弃)
HITL.submit(session, task, decision);
// 提交后,通常自动触发一次“静默续传”,让 AI 完成后续动作
try {
ReActResponse resp = agent.prompt()
.session(session)
.call();
return Result.succeed(resp.getContent());
} catch (Exception e) {
// 如果是拒绝产生的异常,直接返回拒绝理由
return Result.succeed(e.getMessage());
}
}
}
3、交互流程解析
- 触发阶段:用户输入“给老王转账 2000 元”。Agent 解析出 transfer(amount=2000),拦截器发现金额超限,抛出中断并保存 HITLTask。
- 审批阶段:管理员调用 approve 接口。可以根据实际情况在 modifiedArgs 中修正参数(例如将账号改为实名认证后的账号)。
- 恢复阶段:代码执行 agent.prompt().session(session).call()(不传 prompt 即静默续传)。拦截器在 onAgentStart 识别到“全部待批任务均已决策”后强制回到 ACTION,将修正参数注入,执行真实工具调用。
- 闭环阶段:Agent 拿到工具返回的 Observation,继续思考并给出最终答复:“转账已完成,这是