标签Java loop McpAgentExecutor MCP filesystem 自我纠错 代码生成 JUnit 实时编译
前置阅读三 Agent 并行调研:concurrent 节点构建并发-汇聚式旅游规划助手
适合人群:已掌握 McpAgentExecutorloop() 基础,希望构建真实双 Agent 纠错流程的 Java 开发者


一、想解决什么问题?

让 LLM 写代码、跑测试、发现错误、自动修复——这个流程很自然。但如果只用一个 AgentExecutor 把所有事情塞在一起,有几个明显问题:

  • 写代码和修代码混在一起,看不清当前在哪一步
  • 无法用不同模型分别负责"写"和"测"
  • 想把"跑测试"这一步换成真实 CI 环境,改不了

本篇的方案是:把写和测分开,各用一个 McpAgentExecutor,用 loop() 节点在外层协调它们。测试失败 → Write Agent 修代码 → 重跑测试,直到通过。


二、整体流程

用户输入需求
     ↓
保存需求到 ContextBus
     ↓
┌─────────────────────────────────────────────────────┐
│  loop 循环(最多 3 轮)        						  │
│                                                     │
│  Write Agent                                        │
│    第 1 轮:LLM 生成代码 → 写入 Palindrome.java     	  │
│    第 2 轮起:读失败原因 → 修复 → 覆写文件           	  │
│                                                     │
│  Test Agent                                         │
│    第 1 轮:读实现 → 生成测试 → 编译 → 运行        	  │
│    第 2 轮起:直接重跑已有测试(跳过 LLM)           	  │
└─────────────────────────────────────────────────────┘
     ↓
输出最终结果 + 文件路径

两个 Agent 通过 ContextBus.transmitMap 共享状态,互相不需要感知对方的存在。


三、compile_and_run 工具

Test Agent 除了 MCP filesystem 工具,还额外挂了一个 compile_and_run 工具,负责真实编译和执行:

static class JavaTestRunner {

    @AgentTool("编译Java实现类和测试类,用JUnit 4运行所有测试,返回PASS/FAIL及详情")
    public String compileAndRun(
        @Param("实现类Java文件绝对路径") String implFile,
        @Param("测试类Java文件绝对路径") String testFile
    ) {
        String result = doCompileAndRun(implFile, testFile);
        // 把结果写入共享状态,loop 条件直接读这个字段,不用解析 LLM 的自然语言输出
        ContextBus.get().putTransmit("test_result", result);
        return result;
    }

    private String doCompileAndRun(String implFile, String testFile) {
        // 1. 用 javax.tools.JavaCompiler 在进程内编译(需要 JDK,不能是 JRE)
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        String classpath = System.getProperty("java.class.path"); // 含 JUnit JAR
        // ... 编译到 GEN_DIR ...

        // 2. 每次新建 URLClassLoader,避免 JVM 缓存旧版 .class
        try (URLClassLoader loader = new URLClassLoader(
                new URL[]{new File(GEN_DIR).toURI().toURL()},
                Thread.currentThread().getContextClassLoader())) {
            Class<?> testClass = loader.loadClass("PalindromeTest");
            Result result = new JUnitCore().run(testClass);
            return result.wasSuccessful()
                ? "PASS: All " + result.getRunCount() + " tests passed ✅"
                : buildFailReport(result);
        }
    }
}

有两个细节值得关注:

为什么每次新建 URLClassLoader?
第 2 轮修复后,磁盘上的 .class 文件已经更新。如果复用旧的 ClassLoader,JVM 会继续用缓存中的旧字节码,导致即使代码修好了测试也还是失败。新建 ClassLoader 强制重新加载。

为什么结果写入 ContextBus 而不是直接返回给 loop?
loop() 的 condition 函数需要一个明确的布尔值。从 ContextBus 读一个字段比解析 LLM 输出字符串要可靠得多。


四、两个 Agent 的配置

Write Agent

McpAgentExecutor writeAgent = McpAgentExecutor.builder(chainActor)
    .llm(ChatAliyun.builder().model("qwen3.6-plus").temperature(0f).build())
    .tools(mcpClient, "filesystem")   // 只给文件读写工具
    .systemPrompt(
        "You are a Java implementation expert.\n" +
        "Follow these steps in order:\n" +
        "  1. Do NOT include any package declaration.\n" +
        "  2. The class must be public with the required method signature.\n" +
        "  3. Call write_file ONCE to save the file.\n" +
        "  4. Call list_directory to confirm the file is listed.\n" +
        "  5. Output one confirmation line and stop."
    )
    .maxIterations(5)
    .build();

只给 MCP filesystem 工具,职责单一:把代码写到文件。最后让它调 list_directory 确认文件存在,相当于给 LLM 一个明确的"完成动作",避免它反复重写。

Test Agent

McpAgentExecutor testAgent = McpAgentExecutor.builder(chainActor)
    .llm(ChatAliyun.builder().model("qwen3.6-plus").temperature(0f).build())
    .tools(mcpClient, "filesystem")   // 文件读写
    .tools(runner)                    // compile_and_run
    .systemPrompt(
        "You are a Java testing expert. Execute these three tool calls in order, then stop:\n" +
        "  1. read_file — read the implementation file\n" +
        "  2. write_file — write a JUnit 4 test class\n" +
        "  3. compile_and_run — pass impl path and test path\n" +
        "Output compile_and_run's result verbatim."
    )
    .maxIterations(12)
    .build();

.tools() 支持链式调用,MCP 工具和自定义工具可以混用。Test Agent 的职责:读实现、写测试、跑测试、汇报结果


五、loop 驱动逻辑

.loop(
    // condition:第一轮无条件进入;后续轮次测试失败才继续;最多 3 轮
    i -> {
        String testResult = ContextBus.get().getTransmit("test_result");
        boolean failed = testResult != null && testResult.startsWith("FAIL");
        return (i == 0 || failed) && i < 3;
    },

    // 节点A:Write Agent 写代码或修复代码
    (Object input) -> {
        String req        = ContextBus.get().getTransmit("requirement");
        String testResult = ContextBus.get().getTransmit("test_result");
        String prompt = testResult == null
            ? "Write Palindrome.java... save to " + IMPL_FILE
            : "Fix " + IMPL_FILE + " to pass failures: " + testResult;
        return writeAgent.invoke(prompt);
    },

    // 节点B:第 1 轮用 Test Agent 写测试+执行;第 2 轮起直接跑,跳过 LLM
    (Object writeResult) -> {
        String testsWritten = ContextBus.get().getTransmit("tests_written");
        if (testsWritten == null) {
            ContextBus.get().putTransmit("tests_written", "true");
            return testAgent.invoke("read_file, write tests to " + TEST_FILE + ", compile_and_run");
        } else {
            return runner.compileAndRun(IMPL_FILE, TEST_FILE); // 直接执行,不走 LLM
        }
    }
)

三个共享状态字段说明:

字段 由谁写 由谁读 作用
requirement 前置 TranslateHandler Write Agent / Test Agent 存原始需求,每轮都能拿到
test_result compile_and_run 工具 loop condition startsWith("FAIL") 判断是否继续
tests_written testNode testNode(下一轮) 标记测试已写,后续轮次跳过 LLM 直接重跑

"测试只写一次"是关键设计:如果每轮都让 LLM 重写测试,测试用例本身可能发生变化,就无法判断到底是代码改好了还是测试变松了。固定测试文件,只修改实现,结论才可靠。


六、执行日志示例

========== 双Agent自我纠错代码生成 ==========
需求:判断字符串是否为回文,忽略大小写和非字母数字字符

--- 步骤1:Write Agent 编写初始实现 ---
[WriteAgent] tool call: write_file {"path":"/private/tmp/gen/Palindrome.java", ...}
[WriteAgent 完成] I have written the Palindrome class to /private/tmp/gen/Palindrome.java...

--- 步骤2:Test Agent 编写测试并执行 ---
[TestAgent] tool call: read_file {"path":"/private/tmp/gen/Palindrome.java"}
[TestAgent] tool call: write_file {"path":"/private/tmp/gen/PalindromeTest.java", ...}
[TestAgent] tool call: compile_and_run {...}
[TestAgent] observation: PASS: All 4 tests passed ✅

--- Loop 条件检查:第2轮,failed=false,继续=false ---

========== 最终结果 ==========
PASS: All 4 tests passed ✅
生成文件:/private/tmp/gen/Palindrome.java  /private/tmp/gen/PalindromeTest.java
================================

如果第 1 轮失败,Write Agent 在第 2 轮拿到失败详情进行修复,Test Agent 直接重跑同一套测试,直到通过或达到最大轮次上限。


七、运行前置条件

  • JDK(非 JRE)compile_and_run 依赖 javax.tools.JavaCompiler,JRE 中没有
  • Node.js:MCP filesystem 服务器通过 npx 启动
  • mcp.server.config.json:filesystem 的 args 设为 /private/tmp(macOS)或 /tmp(Linux)
  • ALIYUN_KEY 环境变量:示例使用 qwen3.6-plus

macOS 上 /tmp 是符号链接,getCanonicalPath() 会解析为 /private/tmp,需要与 MCP 服务器配置的路径保持一致,否则 MCP 会拒绝访问。


八、总结

本篇的核心是一个可复用的模式:两个专职 Agent + loop 协调

  • Write Agent 只写代码,Test Agent 只验证——工具集与职责严格对应
  • compile_and_run 直接写 ContextBus,loop condition 读状态,两个 Agent 之间没有直接依赖
  • 测试文件写一次后固定,后续轮次只改实现,验证结果更可靠
  • 全链路无 mock:LLM 生成 → MCP 写文件 → javac 编译 → JUnit 执行

这个模式可以直接迁移到其他场景:SQL 生成 + 执行验证、配置文件生成 + 语法检查、接口代码生成 + 集成测试。


📎 相关资源

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐