Ohhnews

分类导航

$ cd ..
InfoQ Java原文

自构建代理:LangChain4j实验

#langchain4j#多代理系统#代码修复#工作流模式#主管模式

主要收获

  • 我们让一个由 LLM 驱动的代码助手使用 LangChain4j 的文档和 API 自行设计并实现了一个多智能体编码系统。
  • 这个自建的编码智能体随后修复了真实存在的 bug,通过了测试,并通过 LangChain4j 暴露了自身的执行流程。
  • 我们比较了两种关键的人工智能智能体模式,发现更刚性的工作流模式比更自主的监督者模式执行速度快三倍,原因是消除了 LLM 引发的协调开销。
  • 使用新引入的 MonitoredAgent 接口,可以清晰获取智能体调用报告以及智能体系统的拓扑结构。
  • 实验中,同一个智能体设计在较旧、较便宜的模型上会陷入工具调用循环导致失败,但在较新的模型上则成功完成了 bug 修复任务。

我们决定进行一次元实验:把 LangChain4j 的文档交给一个代码助手,让它构建一个自己的副本。具体来说,我们希望它设计一个多智能体系统,能够像人类工程师(或代码助手本身)一样编写、测试和调试代码。一个 LLM 能够根据文档构建自己的副本,这件事本身就说明了 LangChain4j 的两点特性:第一,API 足够清晰易懂,模型可以直接使用;第二,框架提供了足够强大的编排能力,使得生成的系统能够端到端地执行真实的调试任务。

这个项目也让我们有机会对 LangChain4j 的新监控工具进行压力测试。当你让 AI 构建另一个 AI 时,你确实需要看到内部发生了什么。本文解释了实验的经过,并介绍了最终的项目成果,该项目可在这个仓库中获取。

“氛围编码”一个智能体系统

为了让代码助手构建它的第一个智能体编码器,我们编写了以下提示:

学习 LangChain4j 智能体框架的 API 和能力,基于其文档和源代码,设计一个基于该框架的智能体编码器,使其成为你自己的克隆版。

经过几分钟的思考和运作,助手认为 LangChain4j 的监督者模式最适合这个任务。它提出了一个初始架构:

$ java
public interface SupervisorCoderSystem {
    @SupervisorAgent(description = """
            一个多智能体编码助手,能够探索代码库、规划实现、编写/编辑代码并运行构建/测试。
            它编排专门的子智能体来完成编码请求。
            """, subAgents = {
            ExplorerAgent.class,
            PlannerAgent.class,
            ImplementerAgent.class,
            ExecutorAgent.class,
    })
    @Override
    String code(@K(UserRequest.class) String request,
                @K(WorkingDirectory.class) String workingDirectory);

    @SupervisorRequest
    static String request(@K(UserRequest.class) String userRequest,
                          @K(WorkingDirectory.class) String workingDirectory) {
        return "以 '" + workingDirectory + "' 作为工作目录,完成以下用户请求:" + userRequest;
    }
}

代码助手还设计并实现了监督者使用的四个子智能体及其系统消息和用户消息。这些智能体负责探索现有代码、制定行动计划、按照计划实现操作、并通过编译和执行来检验生成的代码。代码助手随后为每个智能体正确实现了所需的工具:探索智能体的文件系统浏览器、实现智能体的代码编辑器、以及执行智能体的代码运行能力。

第一次迭代的结果令人印象深刻。如果你曾使用过代码助手,你会知道这大致就是那些“专业”助手在现实中使用的模式:它们通常执行与实验中四个智能体建模的相同的四个动作。然后,助手以监督者式的方式编排这些动作来完成当前任务——这与我们的实现如出一辙。代码助手能够设计和实现这样一个系统,表明它至少在高层次上了解自己的内部工作原理,并能将这种理解转化为具体的智能体系统设计。

让智能体编码器工作起来

为了测试代码助手设计的智能体编码器,我们让它先写一些有 bug 的代码,然后利用新创建的系统来修复它们。代码助手生成了下面这个包含四个稍有缺陷的方法的 Calculator 类:

$ java
/**
 * 一个简单计算器,对数字列表执行基本算术运算。
 */
public class Calculator {
    /**
     * 返回列表中所有数字之和。
     */
    public int sum(List<Integer> numbers) {
        int total = 0;
        for (int i = 0; i <= numbers.size(); i++) {
            total += numbers.get(i);
        }
        return total;
    }

    /**
     * 返回列表中所有数字的平均值。
     */
    public double average(List<Integer> numbers) {
        if (numbers.isEmpty()) {
            return 0;
        }
        return sum(numbers) / numbers.size();
    }

    /**
     * 返回列表中的最大值。
     * 如果列表为空,抛出 IllegalArgumentException。
     */
    public int max(List<Integer> numbers) {
        if (numbers.isEmpty()) {
            throw new IllegalArgumentException("列表不能为空");
        }
        int max = 0;
        for (int n : numbers) {
            if (n > max) {
                max = n;
            }
        }
        return max;
    }

    /**
     * 返回 n 的阶乘。
     * 如果 n 为负数,抛出 IllegalArgumentException。
     */
    public long factorial(int n) {
        if (n < 0) {
            throw new IllegalArgumentException("n 必须非负");
        }
        long result = 1;
        for (int i = 1; i < n; i++) {
            result *= i;
        }
        return result;
    }
}

接着,它生成了一个测试,将包含 Calculator 类的文件夹克隆到临时目录,然后对该目录执行 LangChain4j 的智能体编码器:

$ java
@Test
void workflow_should_fix_buggy_calculator() throws Exception {
    var coder = CoderAgenticSystem.supervisorCoder(coderModel());
    Path source = Path.of("src/test/resources/buggy-project");
    Path workDir = Path.of("/tmp/buggy-calculator");
    String result = coder.code(
            "测试目前失败,因为 Calculator.java 存在 bug。" +
            workDir.toAbsolutePath().toString());
    assertThat(result).isNotBlank();
    System.out.println("Result: " + result);
}

现在是时候运行代码,看看使用常见 LLM 时该实现是否真的有效了。我们为监督者和编码智能体都使用了 OpenAI 的 gpt-4o。选择这个模型是因为 OpenAI API 是默认的基础 URL,gpt-4o 是 LangChain4j 中的默认模型。还有一个好处是,该模型也支持工具调用。

可惜事情并不顺利。几分钟后,我们收到了一个错误:

dev.langchain4j.agentic.agent.AgentInvocationException: 无法调用智能体方法:
public abstract java.lang.String dev.langchain4j.agentic.coder.agent.ImplementerAgent.implement(...)
(...)
Caused by: java.lang.RuntimeException: 出现问题,连续工具调用超过 100 次

显然,LLM 陷入了工具调用循环,而 LangChain4j 在超过默认的最大允许次数 100 后中断了该循环。这个限制可以通过 AiServicesmaxToolCallingRoundTrips() 方法进行配置,但通常这是一个非常合理的默认值。同一个智能体实际上需要超过 100 次工具调用是非常不合理的。

幸运的是,我们之前就在较旧或较不先进的模型上见过这个问题,因此我们改用更现代的 gpt-5-mini 模型重新运行了相同的测试。又过了几分钟,系统输出了以下内容:

Result: CalculatorTest.java 中的所有测试通过 (11/11)。
如果您愿意,我可以展示更新后的 Calculator.java 内容、提供 git 补丁,
或运行额外的测试/边界检查。
工作目录: /tmp/buggy-calculator

Result: 我检查了 /tmp/buggy-calculator 项目,找到了 src/main/java/Calculator.java
和测试文件 src/test/java/CalculatorTest.java,对 Calculator.java 应用了修复,
并运行了测试套件。

执行的动作:
- 将 /tmp/buggy-calculator/src/main/java/Calculator.java 中的实现替换为修正后的代码。
  - sum(List<Integer>):改用 for-each 循环避免索引越界,并正确处理空列表。
  - average(List<Integer>):空列表返回 0,使用 (double) sum(...) / size() 进行浮点除法。
  - max(List<Integer>):空列表抛出异常,初始化 max 为第一个元素以正确处理全负数列表。
  - factorial(int):验证 n >= 0,循环 i = 1..n(包含 n)计算 n!。

执行的命令及结果:
  - mvn -f /tmp/buggy-calculator -DskipTests=true package -> BUILD SUCCESS(编译并打包)
  - mvn -f /tmp/buggy-calculator test -> BUILD SUCCESS,测试数:11,失败:0,错误:0,忽略:0

这次,系统成功修复了 Calculator 类中的所有 bug,并使所有测试通过。它还将临时文件夹中的代码正确修改为:

$ java
public class Calculator {
    public int sum(List<Integer> numbers) {
        int total = 0;
        // 使用 for-each 避免索引错误,并正确处理空列表
        for (int n : numbers) {
            total += n;
        }
        return total;
    }

    public double average(List<Integer> numbers) {
        if (numbers.isEmpty()) {
            return 0;
        }
        // 确保浮点除法,返回小数平均值
        return (double) sum(numbers) / numbers.size();
    }

    public int max(List<Integer> numbers) {
        if (numbers.isEmpty()) {
            throw new IllegalArgumentException("列表不能为空");
        }
        // 初始化为第一个元素,以正确处理全负数列表
        int max = numbers.get(0);
        for (int i = 1; i < numbers.size(); i++) {
            int n = numbers.get(i);
            if (n > max) {
                max = n;
            }
        }
        return max;
    }

    public long factorial(int n) {
        if (n < 0) {
            throw new IllegalArgumentException("n 必须非负");
        }
        long result = 1;
        // 包含 n 以计算 n!
        for (int i = 1; i <= n; i++) {
            result *= i;
        }
        return result;
    }
}

成功之后,我们想更清晰地了解监督者智能体的执行流程。LangChain4j-agentic 的 1.12.2-beta22 版本引入了一项新功能:只需让建模根智能体的接口扩展 MonitoredAgent 接口,即可监控智能体编码器的执行。

$ java
public interface SupervisorCoderSystem extends MonitoredAgent {
    @SupervisorAgent(description = "...")
    ...
}

这个接口允许我们打印智能体调用报告以及系统拓扑,如下图所示。它清晰地展示了智能体编码器的运行方式。

[LOADING...]

点击查看全尺寸图片

图1:由 LangChain4j 可观测性 UI 生成的基于监督者的架构的系统拓扑和执行追踪截图。(图片来源:作者截图)

从监督者到工作流

监督者模式因其自主性而有用,但这种自由也带来了隐藏的开销。为了看看是否能让系统更高效、更可预测,我们要求代码助手使用基于工作流的方法重新设计智能体编码器。

在保留监督者实现的基础上,增加第二个实现,以更确定性的方式实现类似行为,仅使用 LangChain4j 智能体框架提供的工作流模式。特别是,生成一系列行动步骤,尽可能重用现有智能体,并在必要时添加评审循环。

按照要求,这次它提出了一个更确定性的架构:一个严格的五步序列。前四步与之前的智能体对应,第五步引入了一个专门的 SummarizerAgent,它接管了之前由监督者隐式处理的汇总职责。计划和执行步骤都不是单个智能体,而是通过循环结构实现,允许它们进行多次迭代,在继续前进之前调整和改进工作。

$ java
public interface WorkflowCoderSystem extends MonitoredAgent {
    @SequenceAgent(
        description = "一个基于工作流的编码流水线:探索、规划(带评审循环)、" +
                      "实现、执行(带评估和重构循环),然后汇总",
        typedOutputKey = Summary.class,
        subAgents = {
            ExplorerAgent.class,
            PlanReviewLoop.class,
            ImplementerAgent.class,
            ExecutionLoop.class,
            SummarizerAgent.class
        })
    @Override
    String code(@K(UserRequest.class) String request,
                @K(WorkingDirectory.class) String workingDirectory);
}

例如,执行循环由三个子智能体组成:执行器、评估器和重构智能体。这些智能体会被迭代调用,直到评估分数足够好或者达到最大迭代次数。在这个具体例子中,“足够好”的分数被设定为准确率百分之八十,这是一个相当常见且现实的阈值。迭代次数上限设为 5,以防止如果准确率从未达到百分之八十阈值时产生过多的 LLM 调用和令牌消耗。

$ java
public interface ExecutionLoop {
    @LoopAgent(
        description = "迭代执行、评估和重构代码,直到质量足够",
        typedOutputKey = ExecutionResult.class,
        maxIterations = 5,
        subAgents = {ExecutorAgent.class, EvaluatorAgent.class, RefactorAgent.class})
    String executeAndRefine(
        @K(ImplementationResult.class) String implementationResult,
        @K(WorkingDirectory.class) String workingDir);

    @ExitCondition(description = "评估分数大于等于 0.8")
    static boolean exit(@K(EvaluationScore.class) double score) {
        return score >= 0.8;
    }
}

针对这个替代实现运行与之前相同的测试,产生了非常相似的结果,输出如下:

----
Result: ### 编码工作流总结
#### 1. 请求
用户请求分析 `Calculator.java` 源代码,并使用命令 `mvn test` 运行其测试。
测试因 `Calculator.java` 中的 bug 而失败,用户要求修复所有已识别的 bug,
以便 `CalculatorTest.java` 中的所有测试通过。

#### 2. 探索
在代码库探索过程中,确定在当前环境中无法执行 `mvn test` 命令。
但是,提供了如何在本地运行测试的指导。主要发现包括:
- 项目是一个简单的计算器应用程序,包含基本算术运算。
- `Calculator.java` 中发现了多处 bug,包括循环条件、初始值和平均计算类型处理等问题。

#### 3. 计划
方法包括分析 `Calculator.java` 以识别和修复 bug,然后运行 `CalculatorTest.java`
中的测试以验证所有测试通过。计划包括:
- 审查常见算术运算的实现。
- 应用必要的代码更改以解决已识别的问题。
- 运行测试以确保所有功能正常工作。

#### 4. 实现
进行了以下修改:
- **修改**: `src/main/java/com/example/calculator/Calculator.java`
  - 修复了 `sum`、`average`、`max` 和 `factorial` 等方法中的 bug。
- **修改**: `src/test/java/com/example/calculator/CalculatorTest.java`
  - 更新测试以确保它们正确验证 `Calculator.java` 的功能,包括处理边界情况。

#### 5. 执行与评估
- **执行命令**: 在本地环境中运行 `mvn test`。
- **构建状态**: 成功
- **测试结果**:
  - 运行测试数:11
  - 失败:0
  - 错误:0
  - 忽略:0
  - 耗时:0.019 秒
所有测试均成功通过,确认对 `Calculator.java` 和 `CalculatorTest.java` 所做的更改是有效的。
构建完成无编译错误,尽管存在一些与弃用方法和编码相关的警告。

### 后续步骤
为确保持续功能,用户应在本地环境中运行测试。
如果需要进一步帮助,请随时联系。

### 最终评估分数
实现和测试过程的整体质量被评为 0.8,表明在构建过程中出现少量警告的情况下,
初始请求已成功解决。
----

和之前一样,智能体编码器修复了所有 bug 并使所有测试通过,但这次执行追踪显示了一个更复杂的拓扑,包含更多智能体和边。

[LOADING...]

点击查看全尺寸图片

图2:由 LangChain4j 可观测性 UI 生成的基于工作流的架构的系统拓扑和执行追踪截图。(图片来源:作者截图)

然而,这次尽管涉及的智能体数量更多,完整的调试会话版本(与监督者实现相比)却快了整整三倍:两分钟对六分多钟。节省时间的原因在于监督者智能体本身引入的开销——它必须自主生成所有其他智能体的调用及相应参数来协调它们。

结论

在本文中,我们展示了如何使用 LangChain4j 智能体框架,通过“氛围编码”的方式构建一个智能体编码器,让 LLM 自行设计和实现该系统。在这种背景下,LangChain4j 智能体框架的 API 表现出了易用性,使 LLM 能够自主设计和实现一个复杂的智能体编码器,同时其能力足以让代码助手在该系统中克隆自己的内部工作机制,创建了一种元智能体编码器。

我们还展示了如何将该系统投入真实的调试会话,监控其执行并可视化其拓扑。结果令人印象深刻:系统使用一个精心设计的智能体拓扑修复了所有 bug 并通过了所有测试。

最后,这个实验展示了基于工作流与基于更自主监督者的智能体实现之间速度与自主性的权衡。当效率、速度和可预测性至关重要时,选择工作流模式。工作流模式是一种更刚性的方法,在我们的案例中,其执行速度大约是监督者模式的三倍。这种效率来自于消除了 LLM 引发的协调开销,转而依赖确定性的架构和严格顺序的步骤。该模式非常适合那些可以分解为预定义的顺序阶段的任务,并且通常包含内置的循环结构用于迭代改进。

当自主性和动态灵活性比执行速度更重要时,选择监督者模式。监督者模式更加自主,允许主智能体自主生成其他所有智能体的调用及对应参数,动态地进行协调。但这种自由带来了隐藏的开销,使其显著变慢。

关于作者

[LOADING...]

Kevin Dubois

Kevin Dubois 是一位软件架构师和平台工程师,拥有超过二十年的职业生涯。他经常作为主题演讲嘉宾出席世界各地的会议,分享他在云原生和 AI 软件开发、开发者体验、开源及 Java 方面的经验与知识。Kevin 还是一位作家和 Java 冠军。他目前在 IBM 担任高级首席开发者倡导者,并且是 CNCF 开发者体验技术咨询小组的技术负责人以及 Agentic AI 基金会大使。

[LOADING...]

Mario Fusco

Mario Fusco 是 IBM 的高级首席软件工程师,担任 Drools 项目负责人。他的兴趣包括高性能系统和生成式 AI。Mario 是 Quarkus 和 LangChain4j 等广泛采用的项目活跃贡献者。他还是一位 Java 冠军,JUG Milano 协调员,经常发表演讲,并合著了由 Manning 出版的《Modern Java in Action》。