Agents

实战四:手搓 Human in the Loop

在前面的章节中,我们已经了解了 Spring AI Alibaba 官方提供的 Human in the Loop(HITL)能力,以及它实现 HITL 的整体流程。本章将通过完整手写一个 支持 HITL 的 ReactAgent,…

TL;DR

在前面的章节中,我们已经了解了 Spring AI Alibaba 官方提供的 Human in the Loop(HITL)能力,以及它实现 HITL 的整体流程。本章将通过完整手写一个 支持 HITL 的 ReactAgent,…

在前面的章节中,我们已经了解了 Spring AI Alibaba 官方提供的 Human-in-the-Loop(HITL)能力,以及它实现 HITL 的整体流程。本章将通过完整手写一个 支持 HITL 的 ReactAgent,带你从工程实现的角度,真正理解 HITL 需要解决哪些问题、关键机制如何设计,以及它是如何落地的。 在动手之前,我们先从整体视角回顾一下 Spring AI Alibaba 中 HITL 的完整执行流程: 1. Agent 启动并进入推理流程 2. 大模型在推理过程中生成 Tool Call 3. 框架识别到 Tool Call,并判断该工具是否被配置为需要人工审批 4. 若需要审批,Agent 执行被中断,返回 InterruptionMetadata 5. 人类用户基于 InterruptionMetadata 做出审批决策(通过 / 修改 / 拒绝) 6. Agent 在同一个 threadId 下携带人工反馈恢复执行 7. 框架根据审批结果决定是否、以及如何真正执行工具调用 8. Agent 继续后续的推理与执行循环,直至任务完成 流程清晰了之后,我们可以参考他的实现流程,来改造我们之前的 SimpleReactAgent,看能否复刻 HITL。

配置中断

在 Spring AI Alibaba 中,配置中断是通过 HumanInTheLoopHook 实现的。Hook本质上就是一种拦截器,它拦截的并不是用户输入,而是模型推理完成之后产生的 Tool Call,并在工具真正执行之前,判断这些调用是否需要经过人类审批。同样的思想也可以迁移到我们的 SimpleReactAgent 中。虽然没有内置 Hook 机制,但我们可以利用 Spring AI 原生的 Advisor 体系 实现等价能力。具体来说,我们直接实现 CallAdvisor 接口,并重写 adviseCall 方法。那第一个问题就是,这个拦截点在什么位置? 拦截点必须发生在:模型生成 Tool Call 之后、工具真正执行之前。所以在 adviseCall 中,我们首先让模型正常执行一次 nextCall,拿到完整的 response。如果响应中不包含 tool_calls,说明当前轮次不涉及工具调用,直接返回结果即可;只有在响应中检测到 tool_calls 时,才进入 HITL 处理逻辑。当检测到 Tool Call 后,我们会构造一个 List,用于描述所有待人工确认的工具调用。每个 PendingToolCall 至少包含以下信息:工具的 id、name、调用参数、工具描述,以及后续由用户给出的审批结果。后续的中断、恢复与执行,都会围绕这一结构展开。 最后将 HITL_REQUIRED(是否需要 HITL)、HITL_PENDING_TOOLS(待人工确认工具列表)放置在context上下文之中,方便后续流程使用。 此外,我们还需要一个 HITLState 类来记录会话中的 HITL 状态。它包含两组信息: - consumedToolCallIds:记录已处理的工具调用 ID,防止同一个 Tool Call 被重复处理; - approvedToolNames:记录已被人工审批通过的工具名称,实现同一会话中,同一工具只需审批一次,后续同名工具调用自动放行。

public


        consumedToolCallIds


        approvedToolNames

}
public


        APPROVED
        REJECTED
        EDIT


}
public


                nonInterceptTools


                nonInterceptTools


            pending


        response
        response


            response


}

主调用

为了清晰区分首次调用和 HITL 恢复调用,我将它们设计为两个接口: - 主调用:用户第一次调用智能体时触发,用于生成初始响应并可能触发 HITL 中断; - 恢复调用:在 HITL 中断后,获取用户反馈并再次调用智能体,用于继续执行剩余流程。 我们对 ✅手搓 ReactAgent(非流式)中介绍的 SimpleReactAgent的call 方法进行改造,首先将核心执行逻辑抽取到一个独立的 run 方法,以便 恢复调用流程 可以直接复用。这块的主要流程和原来的是一致的,都是 React 架构模式。 在主调用中,需要完成以下初始化工作: - 初始化用户问题; - 创建 context 状态,用于存储会话级信息和 HITL 状态。 与之前的 SimpleReactAgent 不同的还有几个关键改造点: - 返回值类型 - 之前 SimpleReactAgent 的 call 方法直接返回 String 类型,表示最终结果; - 改造后,为了同时支持任务完成和 HITL 中断,返回值改为 AgentResult。 - AgentResult 的实现 - AgentFinished:表示任务已经完成,包含最终结果; - AgentInterrupted:表示 HITL 中断,包含以下信息: - 待确认的工具列表(List); - 快照上下文 messages; - context 上下文状态。 - 中断判断 - 在run方法的迭代循环中增加HITL_REQUIRED判断,满足则直接返回AgentInterrupted中断元数据。 - 这个地方其实就是我们在Advisor中返回的状态和工具列表,我们直接从response.context中获取即可。

// 限定只有2个实现类
public
}

public
}
public


}
public


    messages
    messages


    context


}

private


        round


                messages


                    messages


                    context


        messages


            messages


}

恢复调用

当主调用因 HITL 中断返回 AgentInterrupted 后,恢复流程由 resume 方法负责。它的核心目标:在同一个context上下文中,把人工反馈转化为模型可以继续理解和执行的消息,然后重新进入推理循环。恢复流程的第一步,是从 AgentInterrupted 中取回中断时的快照信息,包括:当时的 messages、context 。其中 context中保存的 HITLState 用于记录哪些 Tool Call 已经被人工处理,避免在多次恢复调用中重复触发 HITL。接下来,resume 会根据用户反馈构造新的工具调用和工具执行结果。对于每一个 PendingToolCall,先判断是否已被消费;未消费的才会被标记为已处理,并转换为 AssistantMessage.ToolCall 补充进消息列表。如果用户审批通过,还会调用 hitlState.markToolNameApproved() 将该工具名称记录下来,这样同一会话中后续再次调用同名工具时,HITLAdvisor 会自动放行,不再需要人工审批。随后,再根据用户的审批结果,决定是拒绝执行工具,还是实际调用对应的 ToolCallback,并将执行结果封装成 ToolResponseMessage 追加到消息流中。完成这些消息补全后,恢复流程并不会单独实现一套执行逻辑,而是直接复用 run 方法,在原有上下文和线程下继续执行主推理循环。这样就保证了主调用与恢复调用在执行路径上的一致性,而不是一套割裂的逻辑。需要注意的是,在 run 方法中调用 chatClient.prompt() 时,需要通过 .advisors(a -> context.forEach(a::param)) 将 context(包含 HITLState)传入 Advisor 请求参数中。这样 HITLAdvisor 就能从 chatClientRequest.context() 中获取到 HITLState,从而判断哪些工具已经被审批过。如果不加这行,HITLAdvisor 拿到的 HITLState 会是 null,已审批工具的自动放行逻辑将无法生效。

public


            messages


        hitlState


            hitlState


        toolCalls


        messages


                result


                result


            messages


}

使用 HITL

下面是一个完整示例,展示了 HITLReactAgent 的使用方式和整体运行思路。 首先初始化底层大模型 ChatModel,同时注册 Agent 可用的工具(还是之前的 weather 和 search ),随后通过 HITLAdvisor 明确指定哪些工具调用需要人工审批,从而为 Agent 注入 HITL 能力。在调用 agent.call() 发起任务后,Agent 会按照 ReAct 模式自动推理并尝试调用工具;当执行过程中触发被拦截的工具时,流程会被中断并返回 AgentInterrupted,其中包含待审批的工具调用列表以及当前的消息快照和内部上下文状态。外部系统或人工用户对这些工具调用给出“同意”或“拒绝”的反馈后,这边使用了控制台输入的方式,来体现这一流程,实际项目中可以通过前后端的交互来实现。接着就通过 agent.resume() 将审批结果重新注入,Agent 会在保留历史消息和内部状态的前提下继续执行推理与工具调用。上述过程可能会重复多次,直到不再需要人工介入,最终 Agent 返回 AgentFinished 并输出完整的结果。

public


    opts
    opts
    opts


                feedbacks

                feedbacks


//            List<PendingToolCall> feedbacks = interrupted.pendingToolCalls().stream()
//                    .map(tc -> new PendingToolCall(tc.id(), tc.name(), tc.arguments(), PendingToolCall.FeedbackResult.REJECTED, "拒绝使用"))
//                    .toList();


        result


}

版本提示

模型、框架与接口会持续变化。涉及版本号、参数与生产配置时,请在实践前对照对应官方文档。

LLMentor系统化学习大模型应用工程

内容来自个人课程知识库备份,并经过结构化整理。技术版本持续演进,生产使用前请结合官方文档验证。