Appearance
实战案例(二):代码审查 Agent
1. 本节产出
一个能接入 CI 的代码审查 Agent:读 diff → 结合仓库规范(RAG)→ 输出结构化问题清单(含严重级、位置、建议),并且有明确的假阳性控制——这是它能否被团队接受的关键。
2. 前置依赖
3. 为什么假阳性决定代码审查 Agent 的生死
代码审查 Agent 最大的问题不是「漏掉问题」,而是**「报了一堆不存在的问题」**。
一个真实反馈:团队接入后第一周,Agent 在每个 PR 上平均报 12 个问题,其中 9 个是误报。第二周开始没人看了,第三周被关掉。
原因:
| 误报来源 | 例子 |
|---|---|
| 看不到上下文 | 报「这个方法没有判空」,但其实调用方保证了非空 |
| 规则过时 | 按通用规则报,但项目有自己的约定 |
| 过度建议 | 把「风格偏好」当成「问题」报出来 |
| 编造行号 | 引用的行号和实际不符 |
核心判断:审查 Agent 的可信度 = 精确率,不是召回率。 漏掉一个真问题,代价是那个 bug;误报十个,代价是整个工具被弃用。
4. 核心原理
4.1 精确率优先的设计
| 措施 | 说明 |
|---|---|
| 只报高置信度问题 | 置信度 < 0.7 的不输出 |
| 必须给出确切位置 | 文件 + 行号(从 diff 解析,不让模型编) |
| 必须有规则依据 | 引用仓库规范的具体条目(RAG) |
| 分级输出 | BLOCKER / MAJOR / MINOR / NIT,只有前两级默认显示 |
| 允许标记误报 | 用户可以点「误报」,用于迭代 |
「行号从 diff 解析而不是让模型编」是关键:模型编的行号经常对不上,一条位置错误的评论足以让人怀疑整个工具。
4.2 结合 RAG:用仓库规范约束审查
仓库规范库(RAG 内容):
- 团队编码规范(命名、异常处理、日志)
- 常见事故复盘(历史上哪些写法出过事)
- 架构约束(分层调用规则、禁止的写法)
流程:
1. 读 diff
2. 检索与这段代码相关的规范条目
3. 让模型对照规范审查(而不是按通用常识审查)这是「RAG + Agent」的第二种组合:RAG 提供判断标准,Agent 执行判断。
收益:审查标准变成可配置的(改规范文档即可),而不是写死在 prompt 里。团队可以对审查规则达成共识并版本化。
4.3 成本控制:只审查变更
| 策略 | 说明 |
|---|---|
| 只送 diff,不送全文件 | 上下文减少 80%+ |
| 大 PR 分片 | 超过 500 行拆成多个子任务 |
| 跳过生成文件 | 锁文件、构建产物不审 |
| 缓存 | 同一 commit 不重复审 |
大 PR 分片很重要:一个 2000 行的 PR 全塞进去,既超上下文又容易让模型遗漏。分片后每片 300 行,质量更稳定。
4.4 结构化输出
json
{
"issues": [
{
"severity": "MAJOR",
"file": "OrderService.java",
"line": 142,
"rule": "规范 3.2:数据库操作必须在事务内",
"description": "该方法执行了两次更新但未加 @Transactional",
"suggestion": "在方法上添加 @Transactional",
"confidence": 0.88
}
],
"summary": "共 3 个问题:1 MAJOR, 2 NIT"
}每条问题都要有 rule(规则依据)和 confidence(置信度)。前者让开发者知道依据是什么(可申诉),后者让 UI 能过滤。
5. 代码走查
5.1 结构化输出定义
java
// ch03-agent/src/main/java/com/aitech/agent/casereview/ReviewResult.java
public record ReviewResult(List<Issue> issues, String summary) {
public record Issue(
@JsonPropertyDescription("严重级,只能取 BLOCKER/MAJOR/MINOR/NIT")
Severity severity,
@JsonPropertyDescription("文件名,必须来自 diff 中的文件名")
String file,
@JsonPropertyDescription("行号,必须来自 diff 的行号,不要编造")
int line,
@JsonPropertyDescription("违反的规则依据,引用仓库规范原文;若没有明确依据则留空")
String rule,
@JsonPropertyDescription("问题描述,一句话说明问题是什么")
String description,
@JsonPropertyDescription("修改建议,给出具体代码或做法")
String suggestion,
@JsonPropertyDescription("置信度 0-1,低于 0.7 的问题不要输出")
double confidence
) {}
public enum Severity { BLOCKER, MAJOR, MINOR, NIT }
}5.2 从 diff 提取结构化信息(不让模型编行号)
java
// ch03-agent/src/main/java/com/aitech/agent/casereview/DiffParser.java
public final class DiffParser {
public record Hunk(String file, int newStartLine, List<String> addedLines) {}
/** 解析 unified diff,返回带真实行号的片段 */
public static List<Hunk> parse(String diff) {
List<Hunk> hunks = new ArrayList<>();
String currentFile = null;
int lineNo = 0;
for (String line : diff.split("\n")) {
if (line.startsWith("+++ b/")) {
currentFile = line.substring(6);
} else if (line.startsWith("@@")) {
// @@ -10,7 +12,8 @@ → 新文件从第 12 行开始
Matcher m = Pattern.compile("\\+(\\d+)(?:,\\d+)?").matcher(line);
if (m.find()) lineNo = Integer.parseInt(m.group(1));
} else if (line.startsWith("+") && !line.startsWith("+++")) {
// 记录真实行号
addLine(hunks, currentFile, lineNo, line.substring(1));
lineNo++;
} else if (line.startsWith("-")) {
// 删除行不推进新文件行号
} else {
lineNo++;
}
}
return hunks;
}
}行号由代码计算,模型只负责判断「这一行有没有问题」。这个分工消除了编造行号的问题。
5.3 检索仓库规范
java
// 用 02 篇的 RAG 检索相关规范
List<RetrievalResult> rules = ragPipeline.retrieve(
"数据库事务 异常处理的规范要求", tenantId, acl);5.4 审查流程
java
// ch03-agent/src/main/java/com/aitech/agent/casereview/CodeReviewAgent.java
@Service
public class CodeReviewAgent {
public ReviewResult review(String diff, String repoId) {
List<Hunk> hunks = DiffParser.parse(diff);
if (hunks.size() > MAX_HUNKS) {
return reviewInBatches(hunks, repoId); // 分片
}
// 检索该仓库的规范
List<RetrievalResult> rules = ragPipeline.retrieve(
summarizeChanges(hunks), repoId);
var conv = new BeanOutputConverter<>(ReviewResult.class);
String raw = chatClient.prompt()
.system(REVIEW_SYSTEM)
.user(u -> u.text("""
仓库规范:
{rules}
待审查的变更(行号已由系统提供,不要修改):
{hunks}
输出格式:
{format}
""")
.param("rules", formatRules(rules))
.param("hunks", formatHunks(hunks))
.param("format", conv.getFormat()))
.options(OpenAiChatOptions.builder().temperature(0.0).build())
.call().content();
ReviewResult result = conv.convert(raw);
// 过滤低置信度与无位置的问题——假阳性控制的关键
return filter(result);
}
private ReviewResult filter(ReviewResult r) {
List<Issue> kept = r.issues().stream()
.filter(i -> i.confidence() >= 0.7)
.filter(i -> i.line() > 0 && i.file() != null && !i.file().isBlank())
.toList();
return new ReviewResult(kept, summary(kept));
}
}5.5 系统提示(精确率的关键)
java
static final String REVIEW_SYSTEM = """
你是代码审查助手,只报告有明确依据的问题。
严格要求:
1. 只报告你能从 diff 中直接看到的问题,不要推测 diff 之外的内容
2. 调用方已保证的约束(如参数非空)不算问题,除非 diff 里能看到违反
3. 风格偏好(如命名喜好)归为 NIT,不要归为 MAJOR
4. 没有仓库规范依据且不属通用错误的,不要报告
5. 行号与文件名必须原样使用输入中提供的值,不得修改
6. 置信度低于 0.7 的问题不要输出
输出前自检:如果这条评论发给人,他能立刻认同吗?不能就别报。
""";第 6 条「输出前自检」这句 prompt 技巧效果很好:它让模型在生成时做一次自我过滤。
6. 跑起来
bash
git checkout ch03-15-code-review
mvn -q test -Dtest=CodeReviewAgentTestbash
# 用测试 diff 审查
curl -X POST http://localhost:8080/api/review \
-H "Content-Type: application/json" \
-d @src/test/resources/sample.diff期望输出:
json
{
"issues": [
{
"severity": "MAJOR",
"file": "OrderService.java",
"line": 142,
"rule": "规范 3.2:涉及多次写操作必须使用事务",
"description": "连续两次 update 未加事务,中途失败会产生不一致",
"suggestion": "方法上添加 @Transactional(rollbackFor = Exception.class)",
"confidence": 0.91
}
],
"summary": "共 1 个问题:1 MAJOR"
}| 检查项 | 通过标准 |
|---|---|
| 行号准确 | 与 diff 中的真实行号一致(人工核对 3 条) |
| 有规则依据 | 引用了仓库规范原文 |
| 假阳性控制 | 用「明显没问题的 diff」测试,报告数 ≤ 1 |
| 分级合理 | 风格问题归为 NIT |
| 大 PR 分片 | 2000 行 PR 被分片且都能完成 |
「明显没问题的 diff」测试最关键:这一步验证精确率,直接决定工具会不会被弃用。
7. 生产避坑
- 精确率优先于召回率。这是代码审查 Agent 最重要的设计原则:误报的代价是工具被弃用,漏报的代价只是一个 bug。做法:设置信度阈值、只报有依据的问题、用「明显没问题的 diff」做回归测试。
- 行号必须由代码解析,不能让模型生成。模型编的行号经常偏移几行,一条位置错误的评论会让人怀疑所有其他评论。用
DiffParser计算真实行号,模型只做判断。 - 审查标准必须来自可配置的规范库(RAG),不能写死在 prompt。写死的话,团队对规则有争议时无法调整,也无法体现项目特性。用 RAG 检索规范,改文档即刻生效。
8. 延伸与锚点
- 思考题:运维是诊断型、代码审查是分析型,如果是「流程型」(要走多个系统、有状态流转)呢?(答案在下一课时:工单处理 Agent)
- 代码锚点:
git checkout ch03-15-code-review - 下一课时:03-16 实战案例(三):工单处理 Agent
- 对应课件:L03-15 代码审查 Agent