Skip to content

实战案例(二):代码审查 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
// src/main/java/com/example/harness/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
// src/main/java/com/example/harness/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
// src/main/java/com/example/harness/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=CodeReviewAgentTest
bash
# 用测试 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. 生产避坑 ​

  1. 精确率优先于召回率。这是代码审查 Agent 最重要的设计原则:误报的代价是工具被弃用,漏报的代价只是一个 bug。做法:设置信度阈值、只报有依据的问题、用「明显没问题的 diff」做回归测试。
  2. 行号必须由代码解析,不能让模型生成。模型编的行号经常偏移几行,一条位置错误的评论会让人怀疑所有其他评论。用 DiffParser 计算真实行号,模型只做判断。
  3. 审查标准必须来自可配置的规范库(RAG),不能写死在 prompt。写死的话,团队对规则有争议时无法调整,也无法体现项目特性。用 RAG 检索规范,改文档即刻生效。

8. 延伸与锚点 ​