AgentScope-Java 入门:让评审助手读懂 Git diff

0 阅读9分钟

AgentScope-Java 入门:让评审助手读懂 Git diff

一句话导语:上一篇我们让评审任务有了 SSE 进度。这一篇开始接入真实输入:读取本地 Git diff、加载变更文件上下文,并生成第一批结构化 findings。

系列说明

本文是「AgentScope-Java 2.0.0-RC4 项目式入门」系列第 3 篇。

本篇对应:

项目内容
起点 tagchapter-02-complete
分支chapter/03-review-tools
完成 tagchapter-03-complete
目标引入工具链,让项目能基于真实 diff 生成 findings

学习目标:

  • 定义代码评审的结构化输出 ReviewFinding
  • 实现 GitDiffTool
  • 实现 FileContextTool
  • 实现 RuleCheckTool
  • 理解 AgentScope 工具设计中的只读边界。

为什么 Agent 不能只看用户输入

如果用户只输入一句:

帮我评审这个项目

模型其实不知道该看什么。

代码评审需要上下文:

  • 哪些文件变了。
  • diff 具体改了什么。
  • 变更文件当前内容是什么。
  • 这次评审关注哪些问题。

所以这一篇要给评审助手接上三类工具:

工具作用
GitDiffTool读取本地 Git diff
FileContextTool读取变更文件上下文
RuleCheckTool先做确定性规则检查

先规则检查,再模型评审。

这是一个务实设计:简单、确定、低成本的问题先由规则发现,模型负责更复杂的上下文判断。

ReviewFinding:统一输出结构

先定义评审输出。

// ReviewFinding.java
public record ReviewFinding(
        ReviewSeverity severity,
        ReviewCategory category,
        String file,
        Integer line,
        String evidence,
        String impact,
        String suggestion,
        ReviewConfidence confidence) {
}

严重级别:

public enum ReviewSeverity {
    BLOCKER,
    HIGH,
    MEDIUM,
    LOW
}

评审类型:

public enum ReviewCategory {
    BUG_RISK("bug-risk"),
    MAINTAINABILITY("maintainability"),
    CONCURRENCY("concurrency"),
    API_CONTRACT("api-contract"),
    TEST_GAP("test-gap"),
    AGENT_BOUNDARY("agent-boundary");
}

这个结构后续贯穿整个项目:

  • 后端保存 findings。
  • 前端展示 findings。
  • 报告渲染 findings。
  • 模型输出也会被解析成 findings。

设计要点:不要让模型随意输出自然语言段落。先定义结构,再让规则和模型都往这个结构里落。

GitDiffTool:读取本地 diff

GitDiffTool 支持三种 diff 范围:

diffMode含义
WORKING_TREE当前工作区改动
STAGED已暂存改动
BASE_REF和指定基准 ref 对比

核心代码:

// GitDiffTool.java
@Tool(name = "load_git_diff", description = "从本地仓库只读加载 Git diff。", readOnly = true)
public String loadGitDiff(
        @ToolParam(name = "repoPath") String repoPath,
        @ToolParam(name = "diffMode") String diffMode,
        @ToolParam(name = "baseRef") String baseRef) {
    return loadDiff(repoPath, DiffMode.from(diffMode), baseRef).diffText();
}

注意 readOnly = true

这是 AgentScope 工具系统里很关键的标记。它告诉权限系统:这个工具只读,不应该产生副作用。

不过本项目这里有一个刻意的工程选择:第 3 篇的 GitDiffToolFileContextToolRuleCheckToolReviewService 确定性调用,不交给 Agent 自主决定调用顺序。

原因是代码评审的输入边界要稳定:

  • 后端先决定 diff 范围。
  • 后端先校验可读取文件。
  • 后端先执行确定性规则。
  • 模型只基于允许的输入做判断。

所以 @Tool 在这里首先表达工具契约和后续可治理能力,而不是说这一篇已经让 ReAct 循环自主调用这些工具。

读取 diff 时,不把用户输入拼成任意 shell 命令,而是固定 Git 参数:

// GitDiffTool.java
private List<String> diffArguments(DiffMode diffMode, String baseRef, boolean nameStatus) {
    List<String> args = new ArrayList<>();
    args.add("diff");
    if (nameStatus) {
        args.add("--name-status");
    }
    if (diffMode == DiffMode.STAGED) {
        args.add("--cached");
    }
    if (diffMode == DiffMode.BASE_REF) {
        String ref = baseRef == null || baseRef.isBlank() ? "HEAD" : baseRef.trim();
        if (ref.startsWith("-")) {
            throw new IllegalArgumentException("baseRef 不能以 '-' 开头:" + ref);
        }
        args.add(ref);
    }
    args.add("--");
    return args;
}

这里有两个安全点:

  • baseRef 不能以 - 开头,避免被当成额外命令参数。
  • 最后加 --,明确后面是路径边界。

最小 Agent 行为:先生成 diff 摘要

为了避免读者等到第 6 篇才第一次看到 Agent 工作,本篇在读取 diff 后加入一个很小的 Agent 调用:让模型对 diff 做 3 到 5 句话摘要。

这不是完整代码评审,只是让 Agent 提前参与流水线:

// DiffSummaryService.java
public DiffSummaryResult summarize(ReviewJob job, GitDiffTool.GitDiffResult diff) {
    RuntimeContext runtimeContext = RuntimeContext.builder()
            .sessionId(job.sessionId())
            .build();

    try (ReActAgent agent = agentFactory.createModelOnlyReviewer(job.sessionId())) {
        Msg response = agent.call(prompt, runtimeContext).block(MODEL_CALL_TIMEOUT);
        String summary = response == null ? "" : response.getTextContent();
        return new DiffSummaryResult(summary);
    }
}

ReActAgent 实现了 AutoCloseable,所以可以用 try-with-resources 确保资源释放。这与 ModelReviewService(第 6 篇审计链路中引用)的用法一致。

ReviewService 在模型已配置时发布一个新的业务事件:

eventPublisher.publish(ReviewEvent.of(
        job.id(),
        ReviewEventType.DIFF_SUMMARIZED,
        "模型已生成 diff 摘要。",
        Map.of("summary", diffSummary)));

这样第 3 篇结束时,项目已经不是“只准备了 AgentFactory”,而是有一次真实的 AgentScope ReActAgent.call()

如果没有配置模型 provider,这一步会跳过,规则检查仍然可以继续运行。

评审流水线流程图.png

FileContextTool:读取变更文件上下文

只看 diff 有时不够。

比如一个方法里新增了一行 return fallback;,你需要看这个方法整体,才能判断是不是异常吞掉。

FileContextTool 负责读取变更文件内容:

// FileContextTool.java
@Tool(name = "load_file_context", description = "读取变更文件上下文。", readOnly = true)
public String loadFileContext(
        @ToolParam(name = "repoRoot") String repoRoot,
        @ToolParam(name = "relativePath") String relativePath) {
    return loadContext(Path.of(repoRoot), relativePath);
}

这里同样是只读工具。

读取文件时需要限制:

  • 文件必须在仓库根目录内。
  • 跳过二进制文件。
  • 限制最大读取行数。
  • 阻止 .env.ssh、密钥文件等敏感路径。

这一点在第 5 篇会展开。

RuleCheckTool:先做确定性规则检查

规则检查不是为了替代模型,而是为了先抓住明确问题。

第一版规则包括:

规则类别
改了生产代码但没改测试test-gap
捕获异常后只打印或忽略bug-risk
使用静态可变集合concurrency
Controller 参数缺少校验api-contract
可写 AgentScope 工具缺少边界agent-boundary

例如测试缺口规则:

// RuleCheckTool.java
if (enabled.contains(ReviewCategory.TEST_GAP)
        && hasProductionChange(diff.changedFiles())
        && !hasTestChange(diff.changedFiles())) {
    findings.add(new ReviewFinding(
            ReviewSeverity.MEDIUM,
            ReviewCategory.TEST_GAP,
            "项目测试",
            null,
            "本次 diff 修改了生产代码,但没有检测到测试文件改动。",
            "关键分支可能缺少回归保护。",
            "为本次变更补充单元测试或集成测试。",
            ReviewConfidence.MEDIUM));
}

这个规则很朴素,但对入门项目有价值:

  • 容易理解。
  • 容易验证。
  • 不依赖模型。
  • 能作为模型评审的参考输入。

把工具链接入 ReviewService

现在评审流程变成:

ReviewRequest
  -> GitDiffTool.loadDiff
  -> FileContextTool.loadContexts
  -> RuleCheckTool.check
  -> ReviewFinding[]

核心编排:

// ReviewService.java
GitDiffTool.GitDiffResult diff =
        gitDiffTool.loadDiff(job.repoPath(), job.diffMode(), job.baseRef());

Map<String, String> contexts =
        fileContextTool.loadContexts(diff.repoRoot(), diff.changedFiles());

List<ReviewFinding> ruleFindings =
        ruleCheckTool.check(diff, contexts, job.focusCategories());

每一步都发布事件:

eventPublisher.publish(ReviewEvent.of(
        job.id(),
        ReviewEventType.RULE_CHECK_DONE,
        "规则检查已完成。",
        Map.of("findingCount", ruleFindings.size())));

这样前端就能看到任务不是黑盒执行,而是一步步推进。

前端展示 findings

第三篇前端不需要做复杂筛选,先展示基础卡片即可。

每条 finding 展示:

  • severity
  • category
  • file
  • line
  • evidence
  • impact
  • suggestion
  • confidence

示例:

<template>
  <article class="finding">
    <header>
      <strong>{{ finding.severity }}</strong>
      <span>{{ finding.category }}</span>
      <code>{{ finding.file }}</code>
    </header>
    <p>证据:{{ finding.evidence }}</p>
    <p>影响:{{ finding.impact }}</p>
    <p>建议:{{ finding.suggestion }}</p>
  </article>
</template>

后续第 7 篇再加筛选、状态标签、复制反馈等体验。

验证

准备一个 demo 仓库,修改生产代码但不改测试。

例如:

// DemoService.java
public String hello() {
    try {
        return risky();
    } catch (Exception e) {
        e.printStackTrace();
        return "fallback";
    }
}

启动后端:

cd backend
mvn spring-boot:run

创建评审:

curl -X POST http://localhost:8080/api/reviews ^
  -H "Content-Type: application/json" ^
  -d "{\"repoPath\":\"D:\\workspace\\demo-project\",\"diffMode\":\"WORKING_TREE\",\"sessionId\":\"demo\"}"

查询任务:

curl http://localhost:8080/api/reviews/{id}

预期:

  • status 最终为 COMPLETED
  • findings 至少包含测试缺口。
  • 如果 diff 中包含异常吞掉,也会生成 bug-risk
  • 被评审仓库没有被修改。

项目也提供了 smoke 脚本:

.\scripts\smoke-review.ps1

预期输出包含:

{
  "status": "COMPLETED",
  "findingCount": 2,
  "demoRepoStatus": " M src/main/java/demo/DemoService.java"
}

设计思考:规则和模型不是二选一

很多人做 AI 代码评审,会直接把 diff 扔给模型。

这能跑,但不一定稳定。

更稳的方式是:

规则检查负责确定性问题
模型评审负责上下文判断
最终统一输出 ReviewFinding
维度规则检查模型评审
成本取决于模型
稳定性有波动
上下文理解
适合问题明确模式复杂语义

本项目先引入规则检查,并在本篇加入一个最小 Agent 摘要调用;完整模型评审后续再接入。

这样即使用户没有配置模型 provider,项目仍然能跑通完整评审链路。

AgentScope 工具系统的价值

AgentScope 的 @Tool 不只是函数暴露。

它还带着工具元信息:

  • name
  • description
  • readOnly
  • concurrencySafe
  • externalTool
  • stateInjected

Spring AI 的工具调用更轻量,适合普通函数调用。

AgentScope 的工具系统更强调生产级 Agent 的工具治理:哪些工具只读,哪些工具可写,哪些工具需要外部执行,哪些工具可以并发。

这也是为什么本项目从第三篇开始就给工具标注只读属性。

如果后续要演示 Agent 自主调用工具,可以基于同一批 @Tool 继续扩展。但本系列第一版选择“服务端确定性调用工具 + Agent 基于受控输入做判断”,优先保证代码评审边界清晰。

常见问题

为什么先用规则检查,不直接调用模型

因为规则检查可控、便宜、稳定。

模型评审应该建立在清晰输入和结构化输出之上,而不是一开始就让模型自由发挥。

为什么读取 diff 用 git 命令,不直接用 JGit

第一版为了简单可验证,使用固定参数 Git 命令。命令参数被白名单控制,不接受任意 shell 输入。

如果要进一步生产化,可以用 JGit 替代部分命令调用。

为什么不自动修改源码

本系列第一阶段只做评审,不做修复。

代码自动修复涉及更高的权限边界和人工确认机制,适合作为后续扩展,而不是入门主线。

本篇小结

这一篇让项目具备了真实评审输入:

  • ReviewFinding 结构。
  • GitDiffTool
  • DiffSummaryService
  • FileContextTool
  • RuleCheckTool
  • 规则化 findings。
  • 前端基础 finding 展示。

下一篇,我们把任务状态和报告保存下来,让刷新页面后仍然能看到结果,并提供 Markdown 报告下载。

系列导航

篇目标题状态
1AgentScope-Java 入门:搭建 Review Copilot 项目骨架已发布
2AgentScope-Java 入门:用 SSE 展示评审任务进度已发布
3AgentScope-Java 入门:让评审助手读懂 Git diff本文
4AgentScope-Java 入门:保存评审状态并生成 Markdown 报告待发布
5AgentScope-Java 入门:给代码评审助手加上只读安全边界待发布
6AgentScope-Java 入门:用 Middleware 审计 Agent 调用待发布
7AgentScope-Java 入门:完善 Vue 前端、发布 GitHub,并规划下一步待发布

作者:亦暖筑序
系列仓库:获取方式见作者主页说明。
AgentScope-Java 版本:2.0.0-RC4