---
title: "react - 人工介入（HITL）"
---


在自动化程度极高的 AI Agent 应用中，人工介入（Human-In-The-Loop, HITL） 是确保业务安全与合规的最后一道防线。（工具调用时）对于涉及资金退款、敏感数据删除或重要邮件发送等操作，我们需要在 Agent 执行前获得人类的明确许可，甚至允许人类修正 AI 的参数。

Solon AI 通过标准化的 `HITLInterceptor`，实现了 “**任务探知 - 决策回填 - 断点续传**” 的工业级管控流程。

### 1、核心原理：中断与延续
HITL 的本质是利用 ReAct 协议的生命周期钩子进行“切面管控”：

- **任务挂起**： 当 Agent 尝试执行敏感工具调用（Tool Call）时，拦截器在 `onActionStart`（批级预检）捕捉到这一动作，将 `callUuid`、工具名、参数封装成 `HITLTask` 快照存入 Session，随即通过 `session.pending(true, summary)` 挂起会话并写入 Final Answer（源码中并不存在 `trace.interrupt()` 方法）。流式输出下还会推送 `HITLPendingEvent` 供前端渲染审批卡片。
- **断点续传**： 当人类完成审批并回填 `HITLDecision` 后，再次调用 `agent.prompt().session(session).call()`，拦截器在 `onAgentStart` 检测到“全部待批任务均已决策”，强制路由回 ACTION 并应用决策，驱动流程从中断点继续运行（`HITLDecidedEvent` 用于关闭/更新审批卡片）。

### 2、核心组件说明
最新架构引入了四个核心类，实现了业务与 Agent 逻辑的彻底解耦：

- `HITL`： 交互助手。提供定位任务（`getPendingTasks` / `getPendingTask` / `getPendingTaskByCallUuid` / `getPendingTaskByToolName` 等）与提交决策（`submit` / `approve` / `reject` / `skip`，均面向 `HITLTask`）的静态 API。4.0.4 起决策主键统一为 `callUuid`，按 toolName 提交的旧接口已 `@Deprecated`。
- `HITLTask`： 任务快照。以 `callUuid`（= ToolCall.uuid）为主键，记录了“哪个调用实例、想调用哪个工具、具体参数是什么”，供 UI 界面展示给审核员。
- `HITLDecision`： 决策实体。承载人类的最终裁决（批准、拒绝、跳过）及参数修正（`modifiedArgs`）、“始终允许”（`alwaysAllow`）信息。
- `HITLInterceptor`： 管控引擎。在 `onActionStart` 做批级预检：存在未决策项则整批挂起（零执行）；全部已决策则统一应用改参 / skip / reject。

### 3、快速接入
通过 `HITLInterceptor` 声明式地注册需要人工审核的工具。

- 关键准备示例

```java
// 1. 定义并配置 HITL 拦截器
HITLInterceptor hitl = new HITLInterceptor()
 // 快速注册敏感工具（触发时自动挂起）
 .onSensitiveTool("refund_money", "delete_database")
 // 自定义策略：例如只有退款金额超过 100 时才需要人工介入
 .onTool("refund_money", (trace, args) -> {
     double amount = Double.parseDouble(args.get("amount").toString());
     return amount > 100 ? "大额退款需人工审核" : null;
 });

// 2. 注入到 Agent
ReActAgent agent = ReActAgent.of(chatModel)
 .defaultToolAdd(...)
 .defaultInterceptorAdd(hitl)
 .build();

```

- HITL Web 控制器完整示例

```java

@Controller
@Mapping("/ai/hitl")
public class HitlWebController {
    private final Map<String, AgentSession> agentSessionMap = new ConcurrentHashMap<>();

    private AgentSession getSession(String sid) {
        return agentSessionMap.computeIfAbsent(sid, k -> InMemoryAgentSession.of(k));
    }

    // 1. 初始化带 HITL 拦截器的 Agent
    private final ReActAgent agent = ReActAgent.of(LlmUtil.getChatModel())
            .defaultInterceptorAdd(new HITLInterceptor()
                    .onSensitiveTool("transfer_money") // 只要调此工具就拦截
                    .onTool("send_msg", (trace, args) -> args.size() > 2 ? "复杂指令需审核" : null))
            .build();

    /**
     * 执行/续传接口
     * 无论初次提问还是审批后恢复，均调用此接口。不传 prompt 则视为“断点续传”
     */
    @Post
    @Mapping("call")
    public Result call(String sid, String prompt) throws Throwable {
        AgentSession session = getSession(sid);

        // 核心：调用 agent。如果是审批后恢复，prompt 传 null 即可
        ReActResponse resp = agent.prompt(prompt).session(session).call();

        // 检查是否被 HITL 拦截（挂起状态在 Session 上，Trace 不提供 isPending）
        if (resp.getSession().isPending()) {
            return Result.failure(403, "审批拦截", HITL.getPendingTask(session));
        }

        return Result.succeed(resp.getContent());
    }

    /**
     * 决策提交接口
     * 由管理员或业务系统调用，提交批准、拒绝或修正参数
     */
    @Post
    @Mapping("submit")
    public Result submit(String sid, int action, @Body Map args) {
        AgentSession session = getSession(sid);
        HITLTask task = HITL.getPendingTask(session);
        if (task == null) return Result.failure("任务不存在");

        // 构建决策对象（4.0.4 推荐工厂方法；action 常量仍可用）
        HITLDecision decision = HITLDecision.approve().modifiedArgs(args);
        if (action == HITLDecision.ACTION_REJECT) decision.comment("安全合规性拒绝");

        // 回填决策（主路径面向 HITLTask；按 toolName 提交的旧接口已废弃）
        HITL.submit(session, task, decision);

        return Result.succeed("决策已提交，请重新请求 call 接口触发续传");
    }
}
```
### 4、 业务闭环流程
人工介入在实际开发中分为三个标准阶段：

#### 第一阶段：触发拦截
当用户发送“帮我退款 200 元”，Agent 推理出需要调用 refund_money。拦截器检测到触发条件，执行中断。

在 Controller 层，你可以探知到这个挂起的任务：

```java
// 获取当前会话中被拦截的任务
HITLTask task = HITL.getPendingTask(session);
if (task != null) {
 System.out.println("等待审批：" + task.getToolName());
 System.out.println("AI 拟调用的参数：" + task.getArgs());
}

```
#### 第二阶段：人工决策
审核员在管理后台看到任务快照后，通过 HITL 工具类提交决策。

- 批准并执行：`HITL.approve(session, task);`
- 拒绝并终止：`HITL.reject(session, task, "理由：账户异常");`
- 参数修正（人类发现 AI 填错了账号）：

```java
HITLTask task = HITL.getPendingTaskByToolName(session, "refund_money"); // 批内同名须唯一
Map fixedArgs = Collections.singletonMap("account", "correct_888");
HITL.submit(session, task, HITLDecision.approve().modifiedArgs(fixedArgs));

```
#### 第三阶段：恢复执行
业务系统再次调用 agent.prompt().session(session).call()（无需再次传入 Prompt）。此时拦截器会读取 HITLDecision 并应用：

- 如果是 **Approve**：拦截器将 `modifiedArgs` 合并进工具参数（未修正则按原参执行），执行工具并继续后续推理；带 `alwaysAllow` 时触发 `onApproved` 回调注入会话级规则，后续同类操作不再弹确认。
- 如果是 **Reject**：仅命中单个敏感工具时，直接路由 END，以拒绝理由作为最终答复（不再继续思考）；同一批内存在多个敏感工具时，仅把拒绝理由写入该工具的 Observation，流程继续处理其余工具。
- 如果是 **Skip**：跳过真实工具执行，返回一条“人工已处理”的观测结果给 Agent。

