4个Agent审一段代码,效率反而更高?——LangChain4j多Agent协作代码审查实战
4个Agent审一段代码,踩了3个坑,总结出一条铁律。
这是我今天做LangChain4j多Agent代码审查流水线时的真实体验。听起来反直觉——4个Agent分工协作,比单个Agent一口气全干完,效率居然更高?但跑完数据我信了:问题多的时候走深度审查路径,Suggester和Validator循环迭代最多3次;问题少的时候走快速路径,顺序执行一次过。16142个Token,8个问题,1次循环就通过了。
说白了,多Agent协作不是"人多力量大"那种粗暴堆料,而是"专业的人干专业的事"——每个Agent只管自己那一块,scope自动传递上下文,你不需要手动拼参数。但这个"自动传递"的机制,恰恰是我踩坑的根源。3个坑,每一个都是对Agentic Scope参数规则的误读。
先说最重要的那个坑:@V声明了scope里没有的key,直接报MissingArgumentException。再说另一个坑:以为scope自动填充就不用声明@V,删掉后又报AgentInvocationException。还有个有意思的事:同一个Agent实例不能同时出现在两个workflow里,条件分支嵌套子workflow是唯一的解法。
这三个坑绕过去之后,整条流水线跑得丝滑。接下来我把架构设计、踩坑过程、核心铁律一步步拆开讲。
一、4-Agent流水线:专业的人干专业的事
先看整体架构。4个Agent各有明确分工:
| Agent | 职责 | outputKey |
|---|---|---|
| CodeReader | 分析代码结构 | codeStructure |
| CodeAnalyzer | 发现代码问题 | issues |
| CodeSuggester | 给出改进建议 | suggestions |
| CodeValidator | 终审建议可行性 | finalReport |
流水线的走向是:CodeReader → CodeAnalyzer → 条件分支 → 输出。
条件分支的判断依据是 issueCount——CodeAnalyzer输出的issues里有多少个问题。如果超过3个,走深度审查路径:Suggester和Validator循环迭代,Validator判定不通过就回退Suggester重新生成,最多3次。如果不超过3个,走快速审查路径:Suggester到Validator顺序执行一次过。
这个设计有意思的地方在于:条件分支里嵌套了完整的子workflow。深度路径是一个loopBuilder,快速路径是一个sequenceBuilder,它们各自有独立的Agent实例,不是共用同一个Suggester/Validator。为什么?这就是第三个坑要讲的事,先按下不表。
看4个Agent的接口定义,每个都非常简洁:
public interface CodeReader {
@Agent(outputKey = "codeStructure")
@UserMessage("你是代码结构分析专家,请分析以下代码的结构:{{codeSnippet}}")
String readCode(@V("codeSnippet") String codeSnippet);
}
CodeReader只接收 codeSnippet(用户输入的原始代码),输出 codeStructure。注意 outputKey = "codeStructure"——这个值会自动写入AgenticScope,后面的Agent通过scope就能拿到。
public interface CodeAnalyzer {
@Agent(outputKey = "issues")
@UserMessage("你是代码质量分析专家,请分析以下代码的问题:{{codeSnippet}}\n代码结构:{{codeStructure}}")
String analyzeCode(@V("codeSnippet") String codeSnippet, @V("codeStructure") String codeStructure);
}
CodeAnalyzer需要两个输入:原始代码 codeSnippet 和 CodeReader输出的 codeStructure。这两个值都从scope自动匹配——scope里有这两个key,@V声明了这两个参数,值就自动填进去。
public interface CodeSuggester {
@Agent(outputKey = "suggestions")
@UserMessage("你是代码改进专家,请为以下代码问题给出改进建议:{{codeSnippet}}\n代码结构:{{codeStructure}}\n发现的问题:{{issues}}")
String suggestImprovements(@V("codeSnippet") String codeSnippet, @V("codeStructure") String codeStructure, @V("issues") String issues);
}
public interface CodeValidator {
@Agent(outputKey = "finalReport")
@UserMessage("你是代码审查终审专家,请验证改进建议的可行性:{{codeSnippet}}\n改进建议:{{suggestions}}\n原始问题:{{issues}}")
String validateSuggestions(@V("codeSnippet") String codeSnippet, @V("suggestions") String suggestions, @V("issues") String issues);
}
Suggester需要3个输入,Validator需要3个输入。scope会提供什么?scope里有 codeSnippet(初始输入)、codeStructure(CodeReader输出)、issues(CodeAnalyzer输出)、suggestions(Suggester自己输出)。只要@V声明和scope里的key对应,值就自动填充。
看起来很清晰对吧?但我的初始版本不是这样的——最初的CodeSuggester声明了4个@V参数,其中3个是scope里根本不会有的key。
二、踩坑实录:3个坑,1条铁律
坑1:MissingArgumentException: reviewMode
这是我遇到的第一枪。运行流水线,直接报错:
MissingArgumentException: reviewMode
我回头看CodeSuggester的接口定义:
// 错误版本!
public interface CodeSuggester {
@Agent(outputKey = "suggestions")
@UserMessage("...{{codeSnippet}}...{{codeStructure}}...{{issues}}...{{reviewMode}}...{{previousSuggestions}}...{{validationFeedback}}")
String suggestImprovements(
@V("codeSnippet") String codeSnippet,
@V("codeStructure") String codeStructure,
@V("issues") String issues,
@V("reviewMode") String reviewMode, // scope里没有这个key!
@V("previousSuggestions") String previousSuggestions, // scope里也没有!
@V("validationFeedback") String validationFeedback // scope里更没有!
);
}
问题很明确:reviewMode、previousSuggestions、validationFeedback 这3个key在AgenticScope里不存在。scope只会包含:初始输入(codeSnippet)+ 前面Agent的outputKey(codeStructure、issues)。你声明了scope里没有的@V参数,LangChain4j尝试从scope取值时找不到,直接抛MissingArgumentException。
修复思路:去掉scope里不存在key的@V声明,同时把@UserMessage里对应的{{变量}}也删掉。最终版本就是上面展示的那个3参数版本——只声明scope里会有的key。
这看起来很简单,但当时我的脑子里有个误区:“循环迭代的时候,Suggester是不是需要知道上一次的建议和Validator的反馈?”——是的,确实需要,但这些值不是通过@V声明的,而是通过scope传递的。loopBuilder在循环时会把前一轮的outputKey值更新到scope里,Agent的@UserMessage模板里可以用{{变量}}引用,但只有scope里有这个key的时候才能声明@V。
坑2:AgentInvocationException: analyzeCode
修完坑1,我以为搞定了。再跑,又报错:
AgentInvocationException: analyzeCode
这次是我自己搞出来的。修坑1的时候,我"矫枉过正"了——我把scope会自动填充的@V(“codeStructure”)和@V(“issues”)也删了!
我的逻辑是:“scope自动提供值,那声明是不是多余的?”——不是。scope自动提供值,但@V声明不能省。@V声明告诉LangChain4j:“这个参数需要从scope取值”,框架才会去scope里找对应的key并填充。如果你删了@V声明,框架不知道这个参数需要从scope取值,@UserMessage里的{{codeStructure}}就成了未绑定变量,运行时直接报错。
修复很简单:把scope会自动填充的@V参数加回来。
// 正确版本:scope会自动填充的@V参数,声明不能省
public interface CodeAnalyzer {
@Agent(outputKey = "issues")
@UserMessage("你是代码质量分析专家,请分析以下代码的问题:{{codeSnippet}}\n代码结构:{{codeStructure}}")
String analyzeCode(@V("codeSnippet") String codeSnippet, @V("codeStructure") String codeStructure);
}
坑1和坑2合在一起看,就是一条铁律:
@UserMessage里的每个{{变量}} → 都必须声明对应的@V参数。值从AgenticScope自动匹配:scope有key → 自动填充;scope没有key → 报MissingArgumentException。所以:只声明scope里会有的@V参数!
说白了,@V声明是"我要这个值"的信号,scope是"我有这个值"的仓库。声明了仓库里没有的东西 → 报错。仓库里有但不声明 → 也报错。两者必须完全对应。
坑3:同一个Agent实例不能在两个workflow中重复引用
坑1和坑2是参数声明的问题,坑3是架构设计的问题。
我的条件分支设计是:issueCount>3走深度路径(loopBuilder),≤3走快速路径(sequenceBuilder)。最初的代码里,两条路径共用同一个 suggester 实例和同一个 validator 实例:
// 错误版本!同一个实例出现在两个workflow里
UntypedAgent deepReviewPath = AgenticServices.loopBuilder()
.subAgents(suggester, validator) // suggester同时出现在conditional和loop里
...
.build();
UntypedAgent quickReviewPath = AgenticServices.sequenceBuilder()
.subAgents(suggester, validator) // 同一个suggester又出现在这里
...
.build();
UntypedAgent conditionalReview = AgenticServices.conditionalBuilder()
.subAgents(scope -> extractIssueCount(scope) > 3, deepReviewPath)
.subAgents(scope -> extractIssueCount(scope) <= 3, quickReviewPath)
.build();
运行时冲突:当条件分支选择一条路径执行时,Agent实例的内部状态(比如绑定的scope、注册的outputKey)会发生冲突。同一个实例注册了两套不同的执行逻辑,框架不知道该用哪套。
修复思路:条件分支嵌套完整子workflow,深度路径和快速路径各自用独立的Agent实例。
// 正确版本:两条路径各自用独立实例(通过AgenticServices构建)
CodeSuggester deepSuggester = AgenticServices.agentBuilder(CodeSuggester.class)
.chatModel(chatModel).outputKey("suggestions").build(); // 深度路径专属
CodeValidator deepValidator = AgenticServices.agentBuilder(CodeValidator.class)
.chatModel(chatModel).outputKey("finalReport").build(); // 深度路径专属
CodeSuggester quickSuggester = AgenticServices.agentBuilder(CodeSuggester.class)
.chatModel(chatModel).outputKey("suggestions").build(); // 快速路径专属
CodeValidator quickValidator = AgenticServices.agentBuilder(CodeValidator.class)
.chatModel(chatModel).outputKey("finalReport").build(); // 快速路径专属
这个修复的本质是:每个Agent实例只能属于一个workflow。如果你需要在多个workflow里使用"同一个"Agent,就创建多个独立的实例。接口定义是同一个,实例是不同的。
三、完整编排代码:条件分支+循环迭代
修完3个坑,最终的编排代码长这样:
// 深度审查路径:Suggester↔Validator循环
UntypedAgent deepReviewPath = AgenticServices
.loopBuilder()
.subAgents(deepSuggester, deepValidator)
.outputKey("finalReport")
.exitCondition(agenticScope -> {
String finalReport = (String) agenticScope.readState("finalReport");
boolean accepted = finalReport.toLowerCase().contains("\"accepted\": true");
return accepted;
})
.maxIterations(3)
.build();
// 快速审查路径:Suggester→Validator顺序
UntypedAgent quickReviewPath = AgenticServices
.sequenceBuilder()
.subAgents(quickSuggester, quickValidator)
.outputKey("finalReport")
.build();
// 条件分支:issueCount>3→深度,≤3→快速
UntypedAgent conditionalReview = AgenticServices
.conditionalBuilder()
.subAgents(scope -> extractIssueCount(scope) > 3, deepReviewPath)
.subAgents(scope -> extractIssueCount(scope) <= 3, quickReviewPath)
.build();
// 完整流水线
UntypedAgent fullPipeline = AgenticServices
.sequenceBuilder()
.subAgents(codeReader, codeAnalyzer, conditionalReview)
.outputKey("finalReport")
.build();
这段代码有几个要点值得单独说:
exitCondition:循环什么时候结束?看Validator输出的finalReport里是否包含 "accepted": true。如果Validator判定建议可行,循环终止;如果不可行,Suggester重新生成建议,下一轮Validator再审。最多3次——maxIterations(3) 是保底机制,防止无限循环。
conditionalBuilder:两条路径的判断条件是 extractIssueCount(scope) > 3 和 <= 3。这个函数从scope里读取CodeAnalyzer输出的issues,解析出问题数量。
sequenceBuilder嵌套conditionalBuilder嵌套loopBuilder:三层嵌套,但逻辑清晰。顶层是顺序执行(Reader→Analyzer→条件分支),条件分支里是两条子workflow(深度是loop,快速是sequence)。
四、测试结果:数据说话
跑了两组测试:
测试1——输入一段有8个问题的代码:
- CodeAnalyzer识别出8个问题,issueCount=8>3
- 走深度审查路径
- Suggester生成建议,Validator第1轮就判定accepted
- 总Token消耗:16142
- 循环次数:1次通过
测试2——输入一段干净的代码:
- CodeAnalyzer识别出少量问题,issueCount≤3
- 走快速审查路径
- Suggester到Validator顺序执行一次过
- 总Token消耗:预计约6000-8000(只有2个Agent执行,无循环)
单Agent vs 多Agent:为什么分工更高效?
16142个Token看起来不少,但考虑到8个问题+深度审查+AI生成建议+验证可行性,这个消耗是合理的。关键是:如果用单个Agent一口气全干完,它需要同时理解结构、发现问题、生成建议、验证可行性——每个环节的质量都会打折。
举个具体的例子。假设我写一个"全能审查Agent",让它一次输出结构分析+问题发现+改进建议+最终报告。Prompt大概长这样:
你是代码审查全能专家,请对以下代码完成4个任务:
1. 分析代码结构
2. 发现所有问题(输出issueCount)
3. 给出改进建议
4. 验证建议可行性,生成最终报告
看起来简洁,实际跑起来有几个硬伤:
硬伤1:Prompt过长导致质量下降。 单个Agent要处理的上下文太多,LLM的注意力会被稀释——结构分析和问题发现需要不同的思维角度,硬塞进一个Prompt里,每个角度的深度都不够。
硬伤2:无法动态调整审查深度。 问题多的代码和问题少的代码,审查策略应该不一样——多Agent流水线通过条件分支自动选择路径,单Agent只能一刀切。
硬伤3:没有质量兜底机制。 多Agent流水线有循环迭代——Validator判定不通过,Suggester会重新生成。单Agent一次输出就是最终结果,错了也没机会修正。
4个Agent各管一摊,每个环节的质量更高,最终输出的finalReport也更靠谱。
extractIssueCount:从LLM输出里抠数字
还有个值得一提的细节——条件分支的判断依据 extractIssueCount 怎么从LLM输出里提取。
Analyzer输出的是一段JSON(包含issueCount字段),但LLM输出不是100%可控的——有时候JSON格式不规范,有时候issueCount字段缺失。所以我设计了三级fallback:
private static int extractIssueCount(AgenticScope scope) {
String issues = (String) scope.readState("issues");
// 第一级:直接匹配 "issuecount": N
Pattern pattern = Pattern.compile("\\"issuecount\\"\\s*:\\s*(\\d+)");
Matcher matcher = pattern.matcher(issues.toLowerCase());
if (matcher.find()) return Integer.parseInt(matcher.group(1));
// 第二级:数"id": N 的出现次数(每个问题都有id字段)
Pattern idPattern = Pattern.compile("\\"id\\"\\s*:\\s*\\d+");
Matcher idMatcher = idPattern.matcher(issues);
int estimatedCount = 0;
while (idMatcher.find()) estimatedCount++;
if (estimatedCount > 0) return estimatedCount;
// 第三级:用overallScore估算(score<5说明问题多,返回10;否则返回1)
Pattern scorePattern = Pattern.compile("\\"overallscore\\"\\s*:\\s*(\\d+\\.?\\d*)");
Matcher scoreMatcher = scorePattern.matcher(issues.toLowerCase());
if (scoreMatcher.find()) {
double score = Double.parseDouble(scoreMatcher.group(1));
return score < 5 ? 10 : 1;
}
// 全部失败:默认走快速路径
return 0;
}
这个三级fallback的设计思路是:永远不要假设LLM的输出格式是完美的。第一级是理想情况,第二级是退而求其次,第三级是保底方案。实际跑的时候,第一级就能匹配到——但如果没有fallback,哪天LLM输出格式变了,流水线直接挂掉。
五、总结:一条铁律和一个设计原则
铁律:@UserMessage里的每个{{变量}} → 都必须声明对应的@V参数。只声明scope里会有的key,scope没有的key不要声明,scope有的key声明不能省。
设计原则:条件分支里嵌套完整子workflow,每条路径用独立的Agent实例。同一个接口可以创建多个实例,但一个实例只能属于一个workflow。
这两个规则合在一起,就是LangChain4j多Agent协作的"通行证"。搞懂了scope参数规则和实例隔离原则,后面的多Agent编排就是拼积木——loopBuilder、sequenceBuilder、conditionalBuilder自由组合,怎么拼都不会踩坑。
4个Agent审一段代码,效率反而更高。不是因为我写了更多代码,而是因为每个Agent只做一件事,scope自动串联上下文,条件分支动态选择路径,循环迭代保证质量。这比单个Agent全包的模式更可控、更可靠。
就这样。
下次聊聊Week7的微调+结构化输出——怎么让Agent不只是输出字符串,而是输出你想要的JSON格式。
更多推荐


所有评论(0)