AI 代码生成的质量评估——不靠人工看,靠自动化测试
AI 代码生成的质量评估——不靠人工看,靠自动化测试
前段时间有个同事跟我说:「老张,我们现在用 AI 生成了不少代码,但我不知道这些代码质量怎么样,每次都要让人工 review,太费时间了。」
我问他:「你怎么定义代码质量?」
他想了想:「功能对、不崩、跑得快、写得规范。」
「那这四点都能自动化测,你为什么要人工看?」
这就是今天这篇文章的出发点。AI 生成代码的质量评估,不是一个玄学问题,而是一个工程问题。人工 review 不应该是质量保证的主要手段,自动化才是。
一、AI 生成代码的特点和风险
AI 生成的代码和人写的代码有一些典型差异,在做评估方案之前要先了解这些特点。
容易产生的问题:
- 功能性幻觉:代码看起来对,跑起来不对。比如边界条件处理错误、逻辑判断反了。
- 安全性盲区:SQL 注入、路径遍历、不安全的反序列化——AI 经常直接生成有安全隐患的代码,除非你明确要求它注意安全。
- 性能陷阱:在循环里调用数据库、N+1 查询问题、没用索引。AI 不了解你的数据规模,它写的「正确代码」在大数据量下可能直接把你的服务打垮。
- 风格不统一:AI 不了解你的项目约定,可能用了你不允许的库,或者命名风格和项目其他代码完全不同。
- 测试覆盖不足:AI 写的测试用例倾向于覆盖「正常路径」,边界条件和异常路径经常被忽略。
不太容易出问题的地方:
- 语法正确性(AI 很少写出语法错误)
- 基本的 API 使用(标准库用法通常是对的)
- 注释和文档(AI 写注释比大多数人认真)
明确了这些特点,评估方案就有了方向:重点评估功能正确性、安全性和性能,语法检查放在最后做个兜底。
二、评估维度设计
完整的 AI 代码质量评估包含四个维度:
每个维度都要有明确的评分标准和最低通过线,不能模糊处理。
三、功能正确性评估
3.1 单元测试执行
这是最基础的评估。AI 生成代码时,可以同时让它生成单元测试,然后自动运行:
@Service
@Slf4j
public class CodeQualityEvaluator {
private final JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
/**
* 编译并运行生成的代码和对应的测试
* @param generatedCode 生成的业务代码
* @param generatedTest 生成的测试代码
* @return 测试结果
*/
public TestExecutionResult compileAndRunTests(
String generatedCode,
String generatedTest,
String className) throws Exception {
// 创建临时工作目录
Path workDir = Files.createTempDirectory("code-eval-");
try {
// 写入源文件
Path sourceFile = workDir.resolve(className + ".java");
Path testFile = workDir.resolve(className + "Test.java");
Files.writeString(sourceFile, generatedCode);
Files.writeString(testFile, generatedTest);
// 编译
CompilationResult compilResult = compile(workDir, sourceFile, testFile);
if (!compilResult.success()) {
return TestExecutionResult.compilationFailed(compilResult.errors());
}
// 运行测试
return runJUnitTests(workDir, className + "Test");
} finally {
// 清理临时目录
deleteDirectory(workDir);
}
}
private CompilationResult compile(Path workDir, Path... sourceFiles) {
DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
StandardJavaFileManager fileManager = compiler.getStandardFileManager(
diagnostics, null, null
);
Iterable<? extends JavaFileObject> compilationUnits =
fileManager.getJavaFileObjectsFromPaths(Arrays.asList(sourceFiles));
JavaCompiler.CompilationTask task = compiler.getTask(
null, fileManager, diagnostics,
List.of("-d", workDir.toString(), "-cp", getClasspath()),
null, compilationUnits
);
boolean success = task.call();
List<String> errors = diagnostics.getDiagnostics().stream()
.filter(d -> d.getKind() == Diagnostic.Kind.ERROR)
.map(d -> d.getMessage(null))
.toList();
return new CompilationResult(success, errors);
}
private TestExecutionResult runJUnitTests(Path workDir, String testClass) {
// 使用 JUnit Platform Launcher 运行测试
LauncherDiscoveryRequest request = LauncherDiscoveryRequestBuilder.request()
.selectors(selectClass(loadClass(workDir, testClass)))
.build();
SummaryGeneratingListener listener = new SummaryGeneratingListener();
LauncherFactory.create().discover(request);
try (LauncherSession session = LauncherFactory.openSession()) {
session.getLauncher().discover(request);
session.getLauncher().execute(
request,
listener
);
}
TestExecutionSummary summary = listener.getSummary();
return new TestExecutionResult(
summary.getTestsSucceededCount(),
summary.getTestsFailedCount(),
summary.getTestsSkippedCount(),
summary.getFailures().stream()
.map(f -> f.getTestIdentifier().getDisplayName() + ": " +
f.getException().getMessage())
.toList()
);
}
}3.2 LLM Judge:用 AI 评估 AI
这是整个方案里最有趣的一部分。用另一个 LLM(通常是更强的模型,比如 GPT-4o 或 Claude)来评估生成代码的逻辑正确性。
为什么要 LLM Judge?因为有些问题纯靠单元测试发现不了,比如:
- 代码逻辑本身有问题,但恰好测试用例没有覆盖到
- 代码的处理方式不符合业务语义(测试通过,但理解错了需求)
- 代码有潜在的并发安全问题(单线程测试发现不了)
@Service
public class LlmCodeJudge {
private final ChatClient judgeClient;
public LlmCodeJudge(ChatClient.Builder builder) {
// Judge 用更强的模型,温度设 0 保证一致性
this.judgeClient = builder
.defaultOptions(OpenAiChatOptions.builder()
.model("gpt-4o")
.temperature(0.0)
.build())
.build();
}
/**
* 用 LLM 评估代码的逻辑正确性
*/
public CodeJudgeResult judge(
String requirement, // 需求描述
String generatedCode, // 生成的代码
String language // 编程语言
) {
String prompt = String.format("""
你是一个严格的代码审查专家。请对下面的代码进行质量评估。
## 需求描述
%s
## 待审查代码(%s)
```%s
%s
```
## 评估要求
请从以下几个维度打分(每项 0-10 分)并给出具体说明:
1. **逻辑正确性**(0-10):代码是否正确实现了需求?有无逻辑错误?
2. **边界处理**(0-10):是否处理了 null 值、空集合、越界等边界情况?
3. **异常处理**(0-10):是否有适当的异常处理?异常处理是否合理?
4. **并发安全**(0-10):如果涉及共享状态,是否有并发安全问题?
5. **可读性**(0-10):代码是否易于理解和维护?
## 输出格式(严格按 JSON 返回)
{
"scores": {
"logic": <分数>,
"boundary": <分数>,
"exception": <分数>,
"concurrency": <分数>,
"readability": <分数>
},
"overall": <综合评分 0-100>,
"issues": [
{
"severity": "CRITICAL|HIGH|MEDIUM|LOW",
"location": "行号或方法名",
"description": "问题描述",
"suggestion": "修复建议"
}
],
"summary": "整体评价(100字以内)"
}
注意:只返回 JSON,不要有任何其他内容。
""",
requirement, language, language.toLowerCase(), generatedCode
);
String response = judgeClient.prompt()
.user(prompt)
.call()
.content();
return parseJudgeResult(response);
}
private CodeJudgeResult parseJudgeResult(String jsonResponse) {
try {
ObjectMapper mapper = new ObjectMapper();
// 提取 JSON 部分(有时模型会在 JSON 前后加说明文字)
String json = extractJson(jsonResponse);
return mapper.readValue(json, CodeJudgeResult.class);
} catch (Exception e) {
log.error("解析 LLM Judge 结果失败: {}", e.getMessage());
return CodeJudgeResult.parseError(jsonResponse);
}
}
private String extractJson(String text) {
int start = text.indexOf('{');
int end = text.lastIndexOf('}');
if (start >= 0 && end > start) {
return text.substring(start, end + 1);
}
return text;
}
}3.3 边界值自动测试生成
除了让 AI 生成测试,还可以用 LLM 专门针对边界值生成测试:
@Service
public class BoundaryTestGenerator {
private final ChatClient chatClient;
public List<TestCase> generateBoundaryTests(
String methodSignature,
String methodDescription) {
String prompt = String.format("""
分析下面这个方法,生成专门针对边界值和异常情况的测试用例。
不要生成正常路径的测试,只关注:
1. null 值输入
2. 空字符串、空集合、空数组
3. 数值边界(0、负数、最大值、最小值、Integer.MAX_VALUE)
4. 字符串边界(超长字符串、特殊字符、Unicode)
5. 集合边界(单元素、超大集合)
6. 并发场景(如果方法有状态)
方法签名:%s
方法描述:%s
以 JSON 数组返回,每个测试用例格式:
{
"testName": "测试名称",
"input": {"参数名": 参数值},
"expectedBehavior": "期望行为(返回值或抛出异常)",
"rationale": "为什么要测这个边界"
}
""",
methodSignature, methodDescription
);
String response = chatClient.prompt().user(prompt).call().content();
return parseTestCases(response);
}
}四、安全性评估
4.1 集成 SpotBugs 静态分析
@Service
public class SecurityScanner {
/**
* 运行 SpotBugs 扫描
* 需要先把代码编译成 .class 文件
*/
public List<SecurityIssue> scanWithSpotBugs(Path classDir) throws Exception {
// SpotBugs 通过 API 调用
Project project = new Project();
project.addFile(classDir.toString());
project.addAuxClasspathEntry(getClasspath());
FindBugs2 findBugs = new FindBugs2();
findBugs.setProject(project);
findBugs.setDetectorFactoryCollection(DetectorFactoryCollection.instance());
BugCollectionBugReporter reporter = new BugCollectionBugReporter(project);
reporter.setPriorityThreshold(Priorities.LOW_PRIORITY);
findBugs.setBugReporter(reporter);
// 只启用安全相关的检测器
findBugs.addFilter(new SecurityFilter(), true);
findBugs.execute();
return reporter.getBugCollection().stream()
.filter(bug -> isSecurityRelated(bug.getBugPattern().getCategory()))
.map(this::convertToSecurityIssue)
.toList();
}
private boolean isSecurityRelated(String category) {
return Set.of(
"SECURITY",
"SQL",
"MALICIOUS_CODE",
"BAD_PRACTICE"
).contains(category);
}
private SecurityIssue convertToSecurityIssue(BugInstance bug) {
return new SecurityIssue(
bug.getBugPattern().getShortDescription(),
bug.getPriorityAbbreviation(),
bug.getPrimarySourceLineAnnotation().getStartLine(),
bug.getBugPattern().getLongDescription()
);
}
}4.2 LLM 安全审查(OWASP 检查单模式)
public SecurityReviewResult reviewSecurity(String code, String language) {
String prompt = String.format("""
你是一个安全专家,请检查以下 %s 代码是否存在 OWASP Top 10 安全漏洞。
重点检查:
1. SQL 注入(直接拼接 SQL)
2. XSS(未经转义输出用户输入)
3. 不安全的反序列化
4. 路径遍历(未验证文件路径)
5. 硬编码的密钥或密码
6. 不安全的随机数(使用 java.util.Random 处理安全场景)
7. 敏感信息日志泄露
8. 缺少输入验证
代码:
```%s
%s
```
返回 JSON:
{
"hasSecurityIssues": true/false,
"riskLevel": "CRITICAL|HIGH|MEDIUM|LOW|NONE",
"vulnerabilities": [
{
"type": "漏洞类型",
"owaspCategory": "OWASP分类",
"severity": "CRITICAL|HIGH|MEDIUM|LOW",
"lineHint": "大概在哪部分代码",
"description": "漏洞描述",
"fix": "修复方案"
}
]
}
""",
language, language.toLowerCase(), code
);
String response = judgeClient.prompt().user(prompt).call().content();
return parseSecurityResult(response);
}五、完整的代码质量评估流水线
把所有评估环节串联起来:
@Service
@Slf4j
public class CodeQualityPipeline {
private final CodeQualityEvaluator compilationEvaluator;
private final LlmCodeJudge llmJudge;
private final SecurityScanner securityScanner;
private final StyleChecker styleChecker;
private final BoundaryTestGenerator boundaryTestGenerator;
private final ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
/**
* 执行完整的代码质量评估流水线
*/
public QualityReport evaluate(CodeEvaluationRequest request) {
log.info("开始评估代码质量,需求:{}", request.requirementSummary());
QualityReport.Builder reportBuilder = QualityReport.builder()
.requirement(request.requirement())
.generatedCode(request.generatedCode())
.evaluationTime(LocalDateTime.now());
// Step 1: 编译检查(必须先通过)
CompilationResult compilationResult;
try {
compilationResult = compilationEvaluator.compile(request.generatedCode());
reportBuilder.compilationResult(compilationResult);
if (!compilationResult.success()) {
return reportBuilder
.overallScore(0)
.verdict(Verdict.FAILED)
.failReason("编译失败:" + String.join("\n", compilationResult.errors()))
.build();
}
} catch (Exception e) {
return reportBuilder.overallScore(0).verdict(Verdict.ERROR).build();
}
// Step 2: 并行执行其他检查
CompletableFuture<TestExecutionResult> testFuture = CompletableFuture.supplyAsync(
() -> runUnitTests(request), executor
);
CompletableFuture<CodeJudgeResult> judgeFuture = CompletableFuture.supplyAsync(
() -> llmJudge.judge(
request.requirement(),
request.generatedCode(),
request.language()
), executor
);
CompletableFuture<SecurityReviewResult> securityFuture = CompletableFuture.supplyAsync(
() -> securityScanner.review(request.generatedCode(), request.language()),
executor
);
CompletableFuture<StyleCheckResult> styleFuture = CompletableFuture.supplyAsync(
() -> styleChecker.check(request.generatedCode()),
executor
);
// 等待所有检查完成
CompletableFuture.allOf(testFuture, judgeFuture, securityFuture, styleFuture)
.join();
// 收集结果
TestExecutionResult testResult = getResultOrDefault(testFuture, TestExecutionResult.empty());
CodeJudgeResult judgeResult = getResultOrDefault(judgeFuture, CodeJudgeResult.empty());
SecurityReviewResult securityResult = getResultOrDefault(securityFuture, SecurityReviewResult.empty());
StyleCheckResult styleResult = getResultOrDefault(styleFuture, StyleCheckResult.empty());
// Step 3: 综合评分
int overallScore = calculateOverallScore(testResult, judgeResult, securityResult, styleResult);
Verdict verdict = determineVerdict(overallScore, securityResult);
return reportBuilder
.testResult(testResult)
.judgeResult(judgeResult)
.securityResult(securityResult)
.styleResult(styleResult)
.overallScore(overallScore)
.verdict(verdict)
.improvements(generateImprovements(judgeResult, securityResult, styleResult))
.build();
}
/**
* 综合评分算法
* 功能正确性权重最高,安全问题直接扣分
*/
private int calculateOverallScore(
TestExecutionResult test,
CodeJudgeResult judge,
SecurityReviewResult security,
StyleCheckResult style) {
// 各维度权重
double testScore = test.passRate() * 100; // 0-100
double judgeScore = judge.overall(); // 0-100
double styleScore = Math.max(0, 100 - style.violationCount() * 2); // 每个违规扣2分
// 加权平均
double weighted = testScore * 0.40
+ judgeScore * 0.40
+ styleScore * 0.20;
// 安全问题直接扣分
int securityPenalty = 0;
for (SecurityIssue issue : security.issues()) {
securityPenalty += switch (issue.severity()) {
case "CRITICAL" -> 30;
case "HIGH" -> 20;
case "MEDIUM" -> 10;
case "LOW" -> 5;
default -> 0;
};
}
return (int) Math.max(0, weighted - securityPenalty);
}
private Verdict determineVerdict(int score, SecurityReviewResult security) {
// 有 CRITICAL 安全问题直接不通过
boolean hasCriticalSecurity = security.issues().stream()
.anyMatch(i -> "CRITICAL".equals(i.severity()));
if (hasCriticalSecurity) return Verdict.FAILED;
if (score >= 80) return Verdict.PASSED;
if (score >= 60) return Verdict.NEEDS_REVIEW;
return Verdict.FAILED;
}
}六、评估报告的输出格式
评估完成后,需要一个清晰的报告,让开发者知道哪里有问题、怎么改。
@Service
public class ReportFormatter {
public String formatMarkdown(QualityReport report) {
StringBuilder sb = new StringBuilder();
// 整体结论
sb.append("# 代码质量评估报告\n\n");
sb.append(String.format("**综合评分:%d/100** | **结论:%s**\n\n",
report.overallScore(),
formatVerdict(report.verdict())
));
// 各维度得分
sb.append("## 各维度评分\n\n");
sb.append("| 评估维度 | 得分 | 说明 |\n");
sb.append("|---------|------|------|\n");
sb.append(String.format("| 单元测试通过率 | %.0f%% | %d通过/%d失败 |\n",
report.testResult().passRate() * 100,
report.testResult().passed(),
report.testResult().failed()
));
sb.append(String.format("| LLM 逻辑审查 | %d/100 | %s |\n",
report.judgeResult().overall(),
report.judgeResult().summary()
));
sb.append(String.format("| 安全风险等级 | %s | %d 个问题 |\n",
report.securityResult().riskLevel(),
report.securityResult().issues().size()
));
sb.append(String.format("| 代码规范 | %d/100 | %d 处违规 |\n",
Math.max(0, 100 - report.styleResult().violationCount() * 2),
report.styleResult().violationCount()
));
// 问题列表
if (!report.securityResult().issues().isEmpty()) {
sb.append("\n## 安全问题(需优先处理)\n\n");
for (SecurityIssue issue : report.securityResult().issues()) {
sb.append(String.format("- **[%s]** %s\n - 位置:%s\n - 修复:%s\n\n",
issue.severity(),
issue.description(),
issue.location(),
issue.fix()
));
}
}
// LLM 发现的问题
if (!report.judgeResult().issues().isEmpty()) {
sb.append("\n## 逻辑问题\n\n");
for (CodeIssue issue : report.judgeResult().issues()) {
if (!"LOW".equals(issue.severity())) {
sb.append(String.format("- **[%s]** %s(位置:%s)\n - 建议:%s\n\n",
issue.severity(),
issue.description(),
issue.location(),
issue.suggestion()
));
}
}
}
// 改进建议
if (!report.improvements().isEmpty()) {
sb.append("\n## 改进建议\n\n");
report.improvements().forEach(imp -> sb.append("- ").append(imp).append("\n"));
}
return sb.toString();
}
}七、接入 CI/CD 流水线
评估框架要有价值,必须接入到日常开发流程里,而不是手动调用:
# .github/workflows/ai-code-quality.yml
name: AI Code Quality Check
on:
pull_request:
paths:
- 'src/**/*.java'
jobs:
ai-code-quality:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Check if PR contains AI-generated code
id: check_ai
run: |
# 检查 PR 描述中是否标记了 AI 生成
if echo "${{ github.event.pull_request.body }}" | grep -q "\[AI Generated\]"; then
echo "has_ai_code=true" >> $GITHUB_OUTPUT
fi
- name: Run AI Code Quality Evaluation
if: steps.check_ai.outputs.has_ai_code == 'true'
run: |
./mvnw spring-boot:run \
-Dspring-boot.run.arguments="--mode=evaluate --pr=${{ github.event.number }}"
- name: Comment PR with results
if: steps.check_ai.outputs.has_ai_code == 'true'
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const report = fs.readFileSync('quality-report.md', 'utf8');
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: report
});八、评估框架的实际效果
我在项目里跑了两个月,收集了一些数据:
发现的问题分布(共 312 个 AI 生成的方法):
- 功能正确性问题:47 个(15%)——其中 32 个是边界条件处理
- 安全问题:23 个(7.4%)——其中 8 个是 HIGH 级别
- 性能问题:18 个(5.8%)——主要是 N+1 查询
- 代码规范问题:89 个(28.5%)——但大多数是 LOW 级别
最有价值的发现:
LLM Judge 发现了 3 个人工 review 遗漏的并发安全问题,其中 1 个在生产环境里复现了(幸好没有数据问题,只是接口偶发 500)。
安全扫描发现了 2 个 SQL 拼接问题(AI 在示例代码里学到了坏习惯),在上线前被拦截了。
评估耗时:
- 编译:平均 2.1 秒
- 测试执行:平均 8.4 秒
- LLM Judge:平均 12.3 秒(最耗时,但可并行)
- 整体:平均 15-20 秒(并行执行后)
15-20 秒换来自动化的代码质量保证,我觉得完全值得。
九、小结
AI 代码生成的质量评估,核心思路是:
- 功能正确性是重点,用单元测试 + LLM Judge 双保险
- 安全问题是高线,有 CRITICAL 安全问题直接不通过
- 评估要自动化,接入 CI/CD,不要依赖人工 review
- 评分要可量化,每个维度有明确的分数,综合评分有通过线
- 报告要可操作,不是告诉你代码有问题,而是告诉你怎么改
「不靠人工看,靠自动化测试」这句话不是说人工 review 没用,而是说:人工 review 应该关注设计层面的问题,具体的代码质量问题应该由自动化保证。这样才能让工程师的时间用在刀刃上。
