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


在自动化 AI Agent 应用中，人工介入（Human-In-The-Loop, HITL） 是确保业务合规的最后一道防线。（工具调用时）对于涉及大额转账、删除数据等敏感操作，系统会暂停执行并等待人类审批。

 Solon AI 通过 HITLInterceptor 实现了标准化的 “拦截 - 决策 - 续传” 流程。

### 1、核心原理

- 声明式拦截：通过拦截器配置“敏感工具”。首次命中时，Session 进入 pending 状态并生成 `HITLTask`；流式调用会收到 `HITLPendingEvent`，随后以 `RunEndEvent(isAbnormal=true)` 完成本次请求，但不会产生 Action 或 ToolCall 的开始/结束事件。这不是 Reactor 错误。
- 状态保持：利用 AgentSession（如内存或 Redis）保留 Agent 的思考进度。
- 断点续传：管理员提交 `HITLDecision` 后，使用同一个 Session 和空 Prompt 恢复。同步 `call()` 只返回最终响应；流式 `stream()` 会在恢复请求中产生 `HITLDecidedEvent`。

### 2、示例代码

```java
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 / skip
     * @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 if ("skip".equals(action)) {
            decision = HITLDecision.skip("管理员选择跳过");
        } 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)`，HITL 拦截器在 Action 开始前保存 `HITLTask` 并挂起 Session；这不是抛出异常。
- 审批阶段：管理员调用 approve 接口。可以根据实际情况在 modifiedArgs 中修正参数（例如将账号改为实名认证后的账号）。
- 恢复阶段：代码执行 agent.prompt().session(session).call()（不传 prompt 即静默续传）。拦截器在 onAgentStart 识别到“全部待批任务均已决策”后强制回到 ACTION，将修正参数注入，执行真实工具调用。
- 闭环阶段：Agent 拿到工具返回的 Observation，继续思考并给出最终答复：“转账已完成，这是