cover


关键要点

本文将引导您通过使用 LangGraph4j 库,将一个经典的 ReAct 代理演进为支持人类审批流程的版本。我们将看到 LangGraph4j 架构如何使实现此类控制流程变得简单。特别是我们关注:

  • 如何将经典的 Agent Executor 实现(参见 图 1)演进为支持人类审批流程。
    • 从标准的代理循环迁移到支持动作调度器和每个动作对应节点的扩展代理循环(参见 图 2)。
    • 将一个动作包装在审批节点中(参见 图 3)。
  • 使用 LangGraph4j 集成 LangChain4j 的详细实现示例。

引言

在 AI 代理的世界中,尤其是那些执行具有现实世界影响的动作的代理,确保控制和监督至关重要。 “人类在循环中”(HITL)模式是一个关键的设计选择,它在代理执行可能产生重大影响的动作之前引入一个供人类审批的步骤。这不仅提高了安全性,也增强了系统信任,这是我们希望客户在生产环境中使用 AI 驱动的工作流时所感受到的。

从标准代理到需要审批的代理

标准的 ReAct 代理

标准的 ReAct(推理与行动)代理在一个简单的循环中运行。代理(或模型)对问题进行推理,决定采取什么行动(使用什么工具),执行该行动,观察结果,并重复这个循环,直到任务完成。

这个流程可以可视化为如下形式:

diagram1

图 1: 一个简单的循环代理,它在推理(代理)和执行(动作)之间交替进行。

虽然这种简单的循环在许多任务中是有效的,但它缺乏一个明确的入口点,以便外部干预。我们如何在代理调用特定工具之前暂停它并请求许可?

进阶代理:动作调度器

为了引入更多的控制,我们首先需要使代理执行动作的过程更加模块化。而不是单一的“动作”步骤,我们可以引入一个“动作调度器”。该节点负责将代理的意图路由到正确的工具专用节点。

这种架构的转变使我们获得了一个更细粒度且更灵活的图。

diagram 2

图 2:调度器模型。该模型决定采取哪个动作,调度器将执行路由到相应的动作节点。

现在,每个工具(action1action2,等等)都是我们图中的一个独立节点。这种分离是实现精细控制的关键。

引入人工审批工作流

有了我们的模块化调度器,我们现在可以在任何需要监督的操作之前插入一个审批节点。例如,我们希望在执行 action2 之前需要人工审批。

我们可以引入一个新的节点 approval_action2,该节点会拦截调度器的请求。这个节点的作用是暂停执行并等待外部输入。

流程如下:

  1. 模型决定执行 action2
  2. action_dispatcher 将请求路由到 approval_action2 节点。
  3. approval_action2 节点等待人类对操作进行批准拒绝
  4. 如果 APPROVED,图将转移到实际的 execTest (action2)节点以执行工具。
  5. 如果被拒绝 ,图会返回到模型,告知其该操作被拒绝。模型随后可以利用此信息重新考虑其计划。

这创建了一个稳健且安全的执行流程。

diagram 3

图 3:完整的 HITL 工作流程。approval_action2 节点作为 action2 的门控节点。

一个实际的例子

LangGraph4j 实现说明

LangGraph4j 提供了一个标准的 AgentExecutor(也称为 ReACT Agent)实现以及一个扩展版本 AgentExecutorEx,它支持人类审批的工作流程。这些实现都可以通过 LangChain4jSpring AI 的集成来使用。

在本文中,我们将重点介绍与 LangChain4j 的集成(请参见 langchain4j-agent 模块中的实现),但也可以使用 Spring AI(请参见 spring-ai-agent 模块中的实现)。

将所有内容整合在一起

要查看代码中的实现方式,您可以查看 DemoConsoleController.java 类,该类位于 langchain4j-agent 测试源代码中。此示例展示了如何构建并运行一个包含条件人工审批步骤的代理,体现了 LangGraph4j 的强大和灵活性。

关键在于使用 AgentExecutorEx 及其 approvalOn 方法。此方法允许您指定一个节点,使图的执行在此节点处中断。当执行到达该节点时,图会暂停并返回一个 InterruptionMetadata 对象,该对象表示需要人工输入。

以下是从示例中摘取的一段代码,展示了如何设置和处理审批机制:

// 1. Configure the agent to require approval on the "action2" tool
var agent = AgentExecutorEx.builder()
        .chatModel(chatModel)
        .toolsFromObject( new Tools() ) // add actions 'action1', 'action2'
        .approvalOn( "action2", ( nodeId, state ) ->
                InterruptionMetadata.builder( nodeId, state )
                        .addMetadata( "label", "confirm execution of action2?")
                        .build()) // request approval before action2
        .build()
        .compile(compileConfig);

// ... inside the execution loop ...

while( true ) {
    // 2. The stream() method returns a generator that yields state updates.
    //    When an interruption occurs, the generator will yield all states up to
    //    the interruption point and then return the InterruptionMetadata.
    var generator = agent.stream(input, runnableConfig );

    // We can process the yielded nodes (e.g., for logging)
    var lastNode = generator.stream().reduce((a, b) -> b).orElseThrow();

    // 3. Check if the graph has finished normally.
    if (lastNode.isEND()) {
        console.format( "result: %s\n", lastNode.state().finalResponse().orElseThrow());
        break;
    }

    // 4. If the graph has not finished, it means it was interrupted.
    //    We get the interruption metadata, which is the return value of the generator.
    var interruption = (InterruptionMetadata<?>) AsyncGenerator.resultValue(generator).orElseThrow();

    // 5. Prompt the user for approval using information from the metadata.
    var answer = console.readLine(format("%s : (N\y) \t\n", interruption.metadata("label").orElse("Approve action ?")));

    // 6. Resume the execution by updating the state with the user's decision.
    if (Objects.equals(answer, "Y") || Objects.equals(answer, "y")) {
        runnableConfig = agent.updateState(runnableConfig, 
                Map.of(AgentEx.APPROVAL_RESULT_PROPERTY, AgentEx.ApprovalState.APPROVED.name()));
    } else {
        runnableConfig = agent.updateState(runnableConfig, 
                Map.of(AgentEx.APPROVAL_RESULT_PROPERTY, AgentEx.ApprovalState.REJECTED.name()));
    }
    input = null; // Clear input for the next iteration, as we are resuming.
}

这段代码展示了完整的流程:

  1. 配置:代理被配置为在执行 execTest 节点之前进行中断。InterruptionMetadata 使用自定义提示标签构建。
  2. 执行:agent.stream() 方法返回一个生成器。我们消费生成器产生的值来处理所有中间状态。
  3. 检查是否结束:我们检查最后一个产生的节点,以确定图是否已完成执行。
  4. 处理中断:如果图尚未完成,我们从生成器的返回值中获取 InterruptionMetadata
  5. 用户交互:我们使用元数据提示用户进行决策。
  6. 恢复:使用 agent.updateState 方法将用户的选择(APPROVEDREJECTED)注入到图的状态中,从而允许执行从上次中断的地方继续。

这个例子展示了 LangGraph4j 如何提供一种干净且强大的机制来实现人机交互的工作流。

结论

Human-in-the-Loop 模式对于创建安全可靠的 AI 代理至关重要。正如我们所见,LangGraph4j 适合实现这种复杂的控制流。通过从简单的代理循环演进到带有调度器和审批节点的模块化图结构,您可以自信地构建复杂且由人类监督的代理。

希望这能帮助您的 AI Java 开发之旅,同时祝您 AI 编码愉快!👋