双 Agent 自我纠错代码生成:Write Agent + Test Agent 驱动 loop() 循环
标签:
JavaloopMcpAgentExecutorMCP filesystem自我纠错代码生成JUnit实时编译
前置阅读:三 Agent 并行调研:concurrent 节点构建并发-汇聚式旅游规划助手
适合人群:已掌握McpAgentExecutor与loop()基础,希望构建真实双 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 生成 + 执行验证、配置文件生成 + 语法检查、接口代码生成 + 集成测试。
📎 相关资源
- 完整代码:Article20TwoAgentSelfCorrect.java
- j-langchain GitHub:https://github.com/flower-trees/j-langchain
- j-langchain Gitee 镜像:https://gitee.com/flower-trees-z/j-langchain
- 运行环境:JDK 17+、Node.js、
ALIYUN_KEY(qwen3.6-plus)
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)