Solon v3.8.0

react - 示例3 - 人工介入(HITL)

</> markdown
2026年1月4日 下午10:41:40

人工介入(HITL)场景测试

  • 场景:Agent 申请退款,拦截器发现是 node_tools 节点且涉及敏感操作,执行中断。
  • 人工审核通过后,恢复执行。

示例代码

使用 ReActInterceptor,在必要情况下中断流程,然后人工介入,审批通过后,再恢复执行。

import demo.ai.agent.LlmUtil;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.noear.solon.ai.agent.Agent;
import org.noear.solon.ai.agent.react.ReActAgent;
import org.noear.solon.ai.agent.react.ReActInterceptor;
import org.noear.solon.ai.agent.react.ReActTrace;
import org.noear.solon.ai.annotation.ToolMapping;
import org.noear.solon.ai.chat.ChatModel;
import org.noear.solon.ai.chat.tool.MethodToolProvider;
import org.noear.solon.annotation.Param;
import org.noear.solon.flow.FlowContext;
import org.noear.solon.flow.FlowException;
import org.noear.solon.flow.Node;
import org.noear.solon.flow.intercept.FlowInvocation;

import java.util.Map;

public class ReActAgentHitlTest {

    @Test
    public void testHumanInTheLoop() throws Throwable {
        ChatModel chatModel = LlmUtil.getChatModel();

        // 1. 定义人工介入拦截器
        ReActInterceptor hitlInterceptor = new ReActInterceptor() {
            @Override
            public void onNodeStart(FlowContext ctx, Node node) {
                // 如果进入工具执行节点,且尚未获得人工批准
                if (Agent.ID_ACTION.equals(node.getId())) {
                    Boolean approved = ctx.getAs("is_approved");
                    if (approved == null) {
                        System.out.println("[拦截器] 检测到敏感工具调用,等待人工审批...");
                        ctx.stop(); // 关键:中断流程
                    }
                }
            }
        };

        ReActAgent agent = ReActAgent.of(chatModel)
                .addTool(new MethodToolProvider(new RefundTools()))
                .interceptor(hitlInterceptor) // 注入拦截器
                .chatOptions(o -> o.temperature(0.0F))
                .build();

        FlowContext context = FlowContext.of("hitl_session_123");
        String prompt = "订单 ORD_888 没收到货,请帮我全额退款。";

        // --- 第一步:发起请求,预期会被拦截 ---
        System.out.println("--- 第一次调用 (预期拦截) ---");
        String result1 = agent.call(context, prompt);

        // 验证:结果应为空(或中间态),且 context 处于 stopped 状态
        Assertions.assertTrue(context.isStopped(), "流程应该被拦截并停止");
        Assertions.assertEquals(Agent.ID_ACTION, context.lastNodeId(), "最后停留在工具节点");

        ReActTrace state = context.getAs("__" + agent.name());
        Assertions.assertTrue(state.getStepCount() > 0);
        System.out.println("当前状态:" + state.getRoute());

        // --- 第二步:人工介入,注入批准信号 ---
        System.out.println("\n--- 人工介入:批准退款 ---");
        context.put("is_approved", true);

        // --- 第三步:恢复执行 ---
        System.out.println("--- 第二次调用 (恢复执行) ---");
        // 恢复时传入原 context,prompt 会从 state 中自动获取
        String result2 = agent.call(context);

        // 验证:最终结果应包含退款成功的关键字
        Assertions.assertNotNull(result2);
        Assertions.assertTrue(result2.contains("成功") || result2.contains("退款"), "审批后应执行成功");
        System.out.println("最终答复: " + result2);
    }

    public static class RefundTools {
        @ToolMapping(description = "执行退款操作")
        public String do_refund(@Param(description = "订单号") String orderId) {
            return "订单 " + orderId + " 已退款成功,金额将原路返回。";
        }
    }
}