ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于AI的Java代码自动评审:规则引擎与AST分层实战

基于AI的Java代码自动评审:规则引擎与AST分层实战 简介面向Java开发者及需要高效代码审查的团队这份基于人工智能技术的Java代码自动评审设计源码旨在帮助快速定位代码漏洞、规范代码写法提升代码质量与审查效率。资源包共34个文件约79KB其中包含24个Java源文件作为核心逻辑4个XML配置如pom.xml用于构建依赖管理2个YAML文件配置AI算法参数或运行环境另有Git忽略文件、说明文档及Shell脚本等辅助文件整体结构清晰便于二次开发或集成。已有341人学习/下载。通过该源码读者可了解AI代码评审工具的设计思路包括与AI模型交互、评审任务执行及报告输出等模块的实现方式适合希望将自动化测试与智能评审引入研发流程的开发者作为参考。1. 基于人工智能技术的 Java 代码自动评审这不是一个插件而是一套工程拿到“基于人工智能技术的 Java 代码自动评审”这个需求时我的第一反应不是去找模型而是先问评审到底要审什么格式、缺陷、坏味道还是架构合理性每一层的工具完全不同。把这个问题想清楚之后才会发现所谓 AI 评审并不是让大模型通读一遍代码而是让规则引擎负责确定性检查、让 AST 提供结构化事实、让大模型只处理规则覆盖不到的经验性判断。这套源码设计的核心是分层与组合。适合这么做的人有两类需要批量审 Pull Request、想把评审经验沉淀成工具和规范的工程师以及正在做 Java 课程设计或毕业设计需要一个能跑通全链路且能答辩讲清原理的源码骨架的同学。2. Java 代码评审的四个维度AST 解析与规则引擎的分工代码自动评审的难点不在模型选型而在评审对象的分层。我习惯把评审任务拆成四个维度格式与规范、静态缺陷、重复代码与坏味道、语义层面的设计问题。这四类任务的确定性依次下降对应的技术手段也完全不同。如果一上来就让大模型读全部源码结果往往是输出看起来很专业、但根本定位不到具体行号、也无法被 CI 门禁直接消费的泛泛而谈。2.1 四层评审体系为什么格式与语义不能混在一个模型里先给一张我用来对齐团队认知的评审维度表。它决定了后面源码里的模块划分和依赖引入评审维度典型检查项确定性工具是否适合用模型格式与规范缩进、命名、行长度、Javadoc 缺失Checkstyle否静态缺陷空指针、资源未关闭、数组越界PMD / SpotBugs / 自定义规则部分适合重复与坏味道重复代码、长方法、圈复杂度超标PMD CPD / JavaParser 自研规则是作为语义判断入口语义设计异常被吞、事务边界、并发可见性无现成确定工具是模型的主战场这张表的意思是规则覆盖率最高的前两层恰恰最不需要“人工智能”。一个比较浮点数、一个resources没关这些是纯语法层面的确定性模式规则引擎几毫秒就能定位让大模型去判反而会漏。真正需要模型介入的是第四层某段 catch 块里把异常e.printStackTrace()之后继续跑业务流程这在语法上完全合法但大概率是缺陷只有结合上下文才能判断。把这两类混在一个模型里会导致确定性问题误判、语义问题又覆盖不全最后两头都翻车。2.2 用 JavaParser 提取结构事实类、方法与圈复杂度所有评审工具最终都要落到同一个坐标上文件路径 行号。格式和缺陷规则可以直接在源码文本上做但语义分析必须拿到语法结构。这里我选择 JavaParser 作为整套源码的 AST 基础设施原因有三它解析完能保留精确的行号范围支持访问者模式遍历任意节点类型还带符号解析能力能帮我们区分方法调用是谁声明的。下面是这套设计里最基础的 AST 事实提取器用来算一个文件里所有 public 方法的圈复杂度:import com.github.javaparser.StaticJavaParser; import com.github.javaparser.ast.CompilationUnit; import com.github.javaparser.ast.body.MethodDeclaration; import com.github.javaparser.ast.expr.BinaryExpr; import com.github.javaparser.ast.stmt.CatchClause; import com.github.javaparser.ast.stmt.DoStmt; import com.github.javaparser.ast.stmt.ForStmt; import com.github.javaparser.ast.stmt.IfStmt; import com.github.javaparser.ast.stmt.SwitchStmt; import com.github.javaparser.ast.stmt.WhileStmt; import java.nio.file.Path; public class AstFactExtractor { public static int cyclomaticComplexityOfPublicMethods(Path javaFile) throws Exception { CompilationUnit cu StaticJavaParser.parse(javaFile); // 所有 public 方法各自计复杂度最后累加方便后续按方法做评审 int[] total {0}; cu.findAll(MethodDeclaration.class).stream() .filter(MethodDeclaration::isPublic) .forEach(method - { int count 1; // 每个方法的基础复杂度是 1 count method.findAll(IfStmt.class).size(); count method.findAll(ForStmt.class).size(); count method.findAll(WhileStmt.class).size(); count method.findAll(DoStmt.class).size(); count method.findAll(CatchClause.class).size(); count method.findAll(SwitchStmt.class).size(); // 和 || 属于短路径分支业界主流的复杂度统计都会计入 count method.findAll(BinaryExpr.class).stream() .filter(e - e.getOperator() BinaryExpr.Operator.AND || e.getOperator() BinaryExpr.Operator.OR) .count(); total[0] count; }); return total[0]; } }这段代码里最需要注意的点是findAll是深度优先递归遍历嵌套的if会被内外各算一次这是圈复杂度的正确算法不是 bug。count 1作为基线值也符合 McCabe 定义。如果你要按方法输出而不只是累加把total[0] count改成保存一份MapString, Integerkey 用方法签名即可。JavaParser 的StaticJavaParser.parse对 JDK 17 之后的语法基本全覆盖但遇到 Lombok 会出问题这个坑在第 5 章单独展开。2.3 规则引擎与 AI 的分工边界先过滤可判定项再送语义判断拿到 AST 事实之后下一步不是直接调模型而是先跑一道确定性的过滤器。我在这套源码里把评审分成了两个阶段阶段一用本地规则引擎把格式、静态缺陷、复杂度超限全部处理掉产出一份带精确行号和规则 ID 的问题列表阶段二只把阶段一没有覆盖到的文件和方法、连同 AST 摘要一起交给大模型。这样分工的核心原因是成本与准确率。一段 200 行的 Java 方法如果全文塞给模型按 token 计价大约是 2500~3000 token评审一个中等规模模块就是几万 token而先做规则过滤后只有真正复杂的方法会被送去模型费用能省五到八倍。更重要的是准确率规则引擎的结果是确定的、可测试的、能在 CI 里做门禁的模型的输出是概率性的必须设计人工抽检兜底。一个可行的决策流是规则引擎先跑凡是规则命中的行模型评审时直接跳过避免重复规则漏掉的高复杂度方法再按 2.2 里算出的复杂度降序送入模型。提示AST 摘要不是把整个方法体贴进 prompt而是提取方法签名、参数类型、局部变量声明、异常捕获结构、循环层级这些骨架信息。这样既能给模型足够的上下文又能把 token 控制在几百以内。3. 最小可运行版本JavaParser 规则引擎 LLM 客户端的完整链路很多同学把“基于人工智能技术的 Java 代码自动评审设计源码”理解成一个大而全的平台一上来就规划微服务、消息队列和前端界面。实际上第一版只需要一个能跑的 CLI读入一个 Java 文件先做规则检查再调模型做语义判断最后输出一份 Markdown 报告。链路通了再谈扩展。3.1 工程骨架与依赖清单pom.xml 里只需要两样东西整套最小实现我只会引入两个第三方依赖JavaParser 负责 AST 解析Jackson 处理模型返回的 JSON。HTTP 客户端直接用 JDK 自带的java.net.http.HttpClient完全不依赖 Spring Boot这样源码可以被任何能装 JDK 17 的环境直接编译运行。?xml version1.0 encodingUTF-8? project xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd modelVersion4.0.0/modelVersion groupIdcom.example/groupId artifactIdai-java-reviewer/artifactId version1.0.0/version properties maven.compiler.source17/maven.compiler.source maven.compiler.target17/maven.compiler.target /properties dependencies !-- AST 解析和符号求解 -- dependency groupIdcom.github.javaparser/groupId artifactIdjavaparser-symbol-solver-core/artifactId version3.25.10/version /dependency !-- JSON 序列化与容错解析 -- dependency groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId version2.16.1/version /dependency /dependencies /project选javaparser-symbol-solver-core而不是javaparser-core是因为语义评审阶段需要符号解析能力比如判断某个RuntimeException是项目自定义异常还是 JDK 内置异常这个信息在结果里能显著提升评审的专业度。Jackson 是必要的模型返回的文本必须经过严格的 JSON 解析才能落到结构化的问题对象里。至于规则引擎我这里不引入完整的 PMD 依赖而是用一组自研的简单规则放在内存里目的是让源码“设计感”更强便于读者二次开发。3.2 组装评审客户端AST 事实、规则结果与模型输出合并成报告接下来是这套源码的核心类ReviewEngine。它把规则检查、AST 事实提取、模型调用三步串成一个管道每个步骤的输出都是下一个步骤的输入。这个设计保证了后续替换规则库或者切换模型时不需要改动其他模块。import com.github.javaparser.StaticJavaParser; import com.github.javaparser.ast.CompilationUnit; import com.github.javaparser.ast.body.MethodDeclaration; import com.github.javaparser.ast.body.Parameter; import com.github.javaparser.ast.stmt.CatchClause; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import java.util.Optional; public class ReviewEngine { private final LlmClient llmClient; private final LocalRuleEngine ruleEngine new LocalRuleEngine(); public ReviewEngine(LlmClient llmClient) { this.llmClient llmClient; } public ListReviewIssue review(Path javaFile) throws Exception { CompilationUnit cu StaticJavaParser.parse(javaFile); ListReviewIssue issues new ArrayList(); // 阶段一本地规则确定性结果精确到行号 issues.addAll(ruleEngine.run(cu)); // 阶段二只对规则没有覆盖过的 public 方法做语义评审 ListMethodDeclaration methods cu.findAll(MethodDeclaration.class); for (MethodDeclaration method : methods) { if (!method.isPublic()) { continue; // 私有方法先用规则覆盖不浪费模型调用 } String astSummary buildAstSummary(method); ListReviewIssue aiIssues llmClient.review(astSummary); filterOutAlreadyReportedLines(issues, aiIssues); issues.addAll(aiIssues); } return issues; } private String buildAstSummary(MethodDeclaration method) { StringBuilder sb new StringBuilder(); sb.append(方法: ).append(method.getSignature()).append(\n); sb.append(复杂度: ).append(AstFactExtractor.cyclomaticComplexity(method)).append(\n); sb.append(参数: ); for (Parameter p : method.getParameters()) { sb.append(p.getType()).append( ).append(p.getNameAsString()).append( ); } sb.append(\n异常处理块数量: ).append(method.findAll(CatchClause.class).size()); // 这里只提取结构摘要不把整个方法体塞进去 OptionalString bodyPreview method.getBody().map(b - b.toString().substring(0, Math.min(200, b.toString().length()))); bodyPreview.ifPresent(s - sb.append(\n方法体预览: ).append(s)); return sb.toString(); } private void filterOutAlreadyReportedLines(ListReviewIssue issues, ListReviewIssue aiIssues) { aiIssues.removeIf(ai - issues.stream() .anyMatch(r - r.line() ai.line() r.severity() ! Severity.SUGGESTION)); } }这段代码有三个需要说明的设计决策。第一buildAstSummary只提取方法签名、复杂度、参数类型和异常块数量这是 token 控制的关键也是与“把整个方法粘贴给模型”这种常见误用做区分的地方。第二filterOutAlreadyReportedLines做的是去重规则已经报过空指针的行模型结果再报一遍会被丢弃避免评审报告噪声过大。第三每个 public 方法独立调一次模型而不是整个文件调一次是为了并行化和局部失败重试一个方法超时不会让整份源码评审中断。3.3 大模型调用提示词结构与 JSON 容错解析LLM 客户端是整套源码里最玄学也最需要防御性编程的部分。模型不是数据库它不会保证每次输出都是合法 JSON。我的做法是提示词里给出严格的输出格式要求代码里做三道容错解析。下面是LlmClient的最小实现import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.time.Duration; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; public class LlmClient { private final HttpClient httpClient HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(30)) .build(); private final ObjectMapper mapper new ObjectMapper(); private final String apiBase; private final String apiKey; private final String model; public LlmClient(String apiBase, String apiKey, String model) { this.apiBase apiBase; this.apiKey apiKey; this.model model; } public ListReviewIssue review(String astSummary) throws Exception { String prompt buildPrompt(astSummary); MapString, Object message Map.of(role, user, content, prompt); MapString, Object payload Map.of( model, model, temperature, 0.1, max_tokens, 1024, messages, List.of(message) ); HttpRequest request HttpRequest.newBuilder() .uri(URI.create(apiBase /v1/chat/completions)) .timeout(Duration.ofSeconds(60)) .header(Authorization, Bearer apiKey) .header(Content-Type, application/json) .POST(HttpRequest.BodyPublishers.ofString(mapper.writeValueAsString(payload))) .build(); HttpResponseString response httpClient.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() ! 200) { throw new RuntimeException(LLM 调用失败: HTTP response.statusCode() response.body()); } JsonNode root mapper.readTree(response.body()); String content root.at(/choices/0/message/content).asText(); return parseIssuesFromContent(content); } private ListReviewIssue parseIssuesFromContent(String content) throws Exception { String json extractJson(content); ListReviewIssue issues new ArrayList(); ArrayNode array (ArrayNode) mapper.readTree(json); for (JsonNode node : array) { issues.add(new ReviewIssue( node.get(line).asInt(), node.get(ruleId).asText(), node.get(category).asText(), Severity.valueOf(node.get(severity).asText().toUpperCase()), node.get(message).asText(), node.get(suggestion).asText() )); } return issues; } private String extractJson(String content) { // 第一道保险直接尝试整段解析 try { mapper.readTree(content); return content; } catch (Exception ignored) { // 第二道保险模型经常把 JSON 包在 json 围栏里 } Pattern fencePattern Pattern.compile((?:json)?\\s*(\\{.*?|\\[.*?])\\s*, Pattern.DOTALL); Matcher matcher fencePattern.matcher(content); if (matcher.find()) { return matcher.group(1); } // 第三道保险截取第一个 [ 到最后一个 ] int startIndex content.indexOf([); int endIndex content.lastIndexOf(]); if (startIndex 0 endIndex startIndex) { return content.substring(startIndex, endIndex 1); } throw new IllegalArgumentException(模型输出中没有找到合法 JSON: content); } private String buildPrompt(String astSummary) { return 你是一个 Java 代码评审专家。请基于下面的方法结构摘要进行评审。 只关注规则引擎无法判断的语义问题异常被吞、并发安全隐患、资源未关闭、状态一致性。 不要做风格类建议。 只输出 JSON 数组不要输出任何解释文字数组元素格式如下 [{line: 12, ruleId: AI-E100, category: concurrency, severity: error, message: 问题描述, suggestion: 修改建议}] 方法摘要 astSummary; } }参数选择上temperature是这套源码里最重要的模型参数之一。评审类任务要求稳定、可复现温度设到 0.1~0.2 足够除非你想让评审结果每次都带点“创造性”否则不要超过 0.3。max_tokens设 1024因为单方法的评审结果一般不会超过 500 token给 1024 是给 JSON 围栏和多余的输出兜底。timeout设 60 秒从网络层到模型返回全链路都在这个值内。如果超时调用方需要做指数退避重试常见做法是 1 秒、2 秒、4 秒最多三次。4. 评审打分与参数调优权重、阈值和误报抑制链路跑通之后接下来要回答的问题是怎么把一堆问题项变成一个团队能消费的结果直接展示“发现 47 个问题”没有意义团队需要的是优先级。我在这套源码里设计了一个简单但可解释的多因子加权评分模型同时用一套参数来抑制误报。4.1 多因子加权评分为什么不能把所有缺陷简单相加最简单的错误做法是把问题数量全部加起来算一个分。这会导致一个真实问题一个 5000 行的老项目修了 100 个风格问题后评分变好了但里面 2 个致命并发缺陷完全没暴露。权重必须按严重级别拉开差距而且最终评分要同时保留“扣完的百分制分”和“按严重级别分组的问题计数”。import java.util.List; import java.util.Map; public class ScoreModel { // 权重表集中管理方便后续调整 private static final MapSeverity, Integer WEIGHT Map.of( Severity.FATAL, 100, Severity.ERROR, 50, Severity.WARNING, 20, Severity.SUGGESTION, 5 ); public double score(ListReviewIssue issues) { // 所有缺陷按权重累加从 100 分里扣 long deduct issues.stream() .mapToInt(i - WEIGHT.getOrDefault(i.severity(), 0)) .sum(); return Math.max(0.0, 100.0 - deduct); } public MapSeverity, Long countBySeverity(ListReviewIssue issues) { return Map.of( Severity.FATAL, issues.stream().filter(i - i.severity() Severity.FATAL).count(), Severity.ERROR, issues.stream().filter(i - i.severity() Severity.ERROR).count(), Severity.WARNING, issues.stream().filter(i - i.severity() Severity.WARNING).count(), Severity.SUGGESTION, issues.stream().filter(i - i.severity() Severity.SUGGESTION).count() ); } }这套打分的分档参考如下。FATAL指会导致线上事故的问题比如资源彻底没关闭、死锁、无限循环入口ERROR是明确的功能性缺陷比如错误的比较运算符、空的 catch 块WARNING是大概率会出问题但需要上下文确认的比如复杂度过高、并发集合在遍历时被修改SUGGESTION是风格和可读性类。严重级别权重分典型示例是否阻塞合并FATAL100资源未关闭、线程不安全单例是ERROR50空指针无判空、异常被吞建议是WARNING20圈复杂度超 20、魔法值否SUGGESTION5命名不规范、注释缺失否权重表集中管理的价值在于你可以针对不同的评审场景下发不同的权重配置。比如安全评审时把并发类问题的权重从 50 提到 80普通代码复查时风格类权重降到 2。这样评分模型本身不用改代码只改配置。4.2 三个必调参数温度、超时与并发跑真实项目的过程中有四个参数直接影响评审的稳定性和吞吐量。第一个是temperature前面已经提过0.1 到 0.2 之间再强调一次不要在评审任务里用默认的 1.0否则同一段代码每次评审结果都不一样你的误报分析会变成一场灾难。第二个是请求超时与重试策略。我看过一个失败的方案对每个方法调用模型的 HTTP 超时设了 60 秒整个模块有 80 个方法串行跑最坏情况下评审一个文件要 80 分钟。血泪经验之后我改成单次请求 45 秒超时退避重试最多 2 次同时把方法按复杂度降序后分批并发。第三个是并发度。HttpClient本身支持并发但模型 API 端通常有 QPM 限制。我一般用固定大小线程池初始并发 4监控到 HTTP 429 响应后动态降到 2。这是一个用翻车换来的参数并发开太高触发限流后所有请求一起失败重试风暴会直接打爆模型服务端。还有一个max_tokens要按输出长度调整如果提示词要求输出 JSON 数组并且你有信心单方法问题数不超过 10 条1024已经足够开太大反而会延长推理时间。4.3 误报抑制白名单、去重与报告拆分误报是自动评审里最损伤信任度的东西。团队第一次跑报告发现有 3 个误报他们嘴上不说但下一次就会开始无视全部报告。为此我的源码里加了三个抑制机制。第一个是目录白名单。src/main/java才做完整评审src/test/java只跑规则引擎不跑模型target/generated-sources直接跳过。生成的代码、Lombok 产生的样板代码、历史遗留大粪坑目录都应该通过配置文件一次性排除。第二个是行级去重在第 3 章代码里已经实现了这里补充一点模型报出的问题如果落在规则已经命中的行上直接丢弃规则报WARNING、模型报ERROR保留模型的ERROR但把规则结果降级为提示。第三个是报告拆分把“确定性问题”和“模型判断问题”分成两个区块展示ReviewIssue 对象里的source字段就派上用场了。区块来源信任度处理方式规则引擎发现Checkstyle / PMD / 自研规则高可测试直接进 CI 门禁模型语义发现LLM 客户端中需要抽检进人工复核队列这等于把模型当成一个“建议提供者”而不是“评审决策者”。有了这层定位自动评审系统在团队里的接受度会明显高很多。5. 自动评审落地避坑五个真实翻车现场与修复方法这部分写我在搭建和运行这套源码时遇到的五个高频问题。每条都按“现象 → 原因 → 解决”展开这些问题在你的落地过程中大概率也会碰到。5.1 Lombok 注解让 AST 解析直接崩溃现象StaticJavaParser.parse(javaFile)对使用了Getter、Data、Builder的类抛ParseProblemException或者解析出的 AST 里节点不完整某些字段消失。原因JavaParser 只认标准 Java 语法Lombok 的注解处理器是在编译期生成代码的源码层面看是合法的注解但 Lombok 生成的 getter、setter、构造器根本不在 AST 里。更麻烦的是Builder内部自动生成的外部构建器类会诱导findAll(MethodDeclaration.class)扫出一堆你源码里看不到的方法。解决三个方案按项目情况选。最省事的是在解析前过滤掉 Lombok 注解的类交给模型评审时在提示词里注明“以下源码已移除 Lombok 注解缺失的 getter/setter 视为存在”其次是引入lombok-javaparser这类辅助库做注解逻辑模拟最常见也最稳的做法是让 CI 跑在编译后的目录上把target/generated-sources一起纳入 AST 事实提取然后按第 4 章的白名单机制去重。5.2 模型返回的不是合法 JSON现象parseIssuesFromContent抛JsonProcessingException日志里显示模型输出了一段带 Markdown 列表和“以下是评审结果”的文本。原因模型在指令遵循上的表现不是 100% 稳定。即使提示词写了“只输出 JSON 数组”它仍可能出于“帮忙解释”的本能把内容包在 json 围栏里或者在数组前后加上注脚。解决第 3 章里写的三道容错解析正是为了应对这个场景。第一道直接解析第二道剥围栏第三道截取[到]。还有一个进阶方案提示词里给一个更完整的示例包含一条warn级别的问题模型看到示例后会显著提高格式遵循率。如果模型支持 JSON mode 或结构化输出参数打开它容错率能再上一个台阶。5.3 大仓库评审超时串行调用与 token 爆炸现象对一个 2000 文件的中型服务跑评审跑了 40 分钟还没结束中间不断有HttpTimeoutException弹出。原因两层长尾。第一层是串行循环每个 public 方法都调一次模型一个服务几万个方法排着队等第二层是个别巨大方法比如一个 500 行的handleRequest方法AST 摘要构建时把方法体前 200 字符放进 prompt 后会截断但方法体内嵌了大量字符串常量token 数依然爆炸。解决按 2.2 的复杂度结果做降序排列只对复杂度大于 10 的方法调用模型简单方法完全交由规则引擎。buildAstSummary里的方法体预览从 200 字符砍到 80 字符并且只保留前 80 个字符用于识别方法结构。并发度提到 4 之后这个问题基本消失。另外给整个评审任务加一个总时长预算超时后落一个“部分评审请重试失败批次”的报告而不是无限等下去。5.4 同一行缺陷被报两次规则与模型的叠加噪声现象评审报告里同一行出现两条几乎一样的“空指针风险”记录一条来自 PMD 规则一条来自模型。原因filterOutAlreadyReportedLines写得太宽松只在severity ! SUGGESTION且行号完全相等时去重。实际情况中 PMD 报的是表达式那行模型报的是同一语句内另一列行号相同但列号不同两个规则 ID 不相同程序就认为是两条独立的问题实际上是一个根因。解决去重键从“行号 严重级别”改成“文件路径 行号 根因关键词”。具体实现里我把规则命中的行提取出关键表达式比如user.getName()再用这个表达式去匹配模型输出中是否包含同一个表达式。匹配上就丢弃模型的重复项但保留模型给出的更详细修复建议合并到规则结果的suggestion字段里。这类去重逻辑建议单独抽成IssueMerger类不要塞进引擎主流程。5.5 把评分当成 KPI数值游戏的教训现象上线几周后某个团队为了冲刺评分从 82 分升到 90 分开始把SUGGESTION级别的提示全部改成注释甚至直接在白名单里加src/main/java下他们自己的包。原因单看百分制分数太容易钻空子。扣分制天然鼓励“减少问题数量”但不是所有减分都等于质量提升有些团队会通过在报告里忽略风格类问题来刷分。解决这套源码最终在输出里把百分制分数和严重级别计数拆开。CI 门禁只看两条FATAL数量必须为 0ERROR数量不得超过基线的 20%。百分制分数只在周报里做趋势对比并且对比时固定同一份权重配置不允许临时调。经过这个改动之后团队关注点从“分数好看”回归到“别新增致命缺陷”。6. 让评审结果经得起复核回归基线、抽检与提示词迭代自动评审落地最后一步也是最容易被跳过的构建一个评估集让每次改动模型参数、提示词、权重之后都能回答“变好了还是变坏了”。做法是挑选 20 到 30 个具有代表性的 Java 文件人工标注出真正的缺陷和误报边界形成一个 golden 集合。这套源码里放了一个golden/目录每个文件对应一个同名 JSON 标注文件字段就是line、ruleId、severity、isTruePositive。每次调整提示词后跑一遍对比检出率和误报率。我给自己的一个硬性习惯是每次修改buildPrompt或temperature后必须在 golden 集合上跑出两组数据——TP正确检出的真实缺陷数和FP误报数。例如新的提示词把空指针检出率从 60% 提到 78%但误报数量从 12 条涨到 37 条那这个改动说明阈值调过了需要往回退。用数据卡住每次迭代自动评审才不会慢慢变成一个垃圾回收站。人工抽检周期建议定在每周一次从本周新合并的 PR 里随机抽 5 个比对自动评审报告的准确率。如果发现某个错误类型模型反复漏掉或者规则反复误报把样本补充进 golden 集合然后做针对性的提示词修正或规则白名单调整。最后是我个人总结的一条习惯发现模型误报时不要急着在提示词里加一句“不要误报”那样经常会按下葫芦浮起瓢。正确做法是把这个误报样本单独拿来跑一次找出它的边界特征再针对性地在提示词里增加排除条件。比如模型频繁把 Redis 连接未关闭判成 FATAL而实际上项目引入了连接池提示词里就要明确“使用连接池管理的资源不算未关闭”。这样做过三四轮之后评审结果会稳定很多希望这些方法对你能有帮助。本文还有配套的精品资源点击获取
返回列表