AgentScope-Java 入门:让评审助手读懂 Git diff
一句话导语:上一篇我们让评审任务有了 SSE 进度。这一篇开始接入真实输入:读取本地 Git diff、加载变更文件上下文,并生成第一批结构化 findings。
系列说明
本文是「AgentScope-Java 2.0.0-RC4 项目式入门」系列第 3 篇。
本篇对应:
| 项目 | 内容 |
|---|---|
| 起点 tag | chapter-02-complete |
| 分支 | chapter/03-review-tools |
| 完成 tag | chapter-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 篇的 GitDiffTool、FileContextTool、RuleCheckTool 由 ReviewService 确定性调用,不交给 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,这一步会跳过,规则检查仍然可以继续运行。
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 不只是函数暴露。
它还带着工具元信息:
namedescriptionreadOnlyconcurrencySafeexternalToolstateInjected
Spring AI 的工具调用更轻量,适合普通函数调用。
AgentScope 的工具系统更强调生产级 Agent 的工具治理:哪些工具只读,哪些工具可写,哪些工具需要外部执行,哪些工具可以并发。
这也是为什么本项目从第三篇开始就给工具标注只读属性。
如果后续要演示 Agent 自主调用工具,可以基于同一批 @Tool 继续扩展。但本系列第一版选择“服务端确定性调用工具 + Agent 基于受控输入做判断”,优先保证代码评审边界清晰。
常见问题
为什么先用规则检查,不直接调用模型
因为规则检查可控、便宜、稳定。
模型评审应该建立在清晰输入和结构化输出之上,而不是一开始就让模型自由发挥。
为什么读取 diff 用 git 命令,不直接用 JGit
第一版为了简单可验证,使用固定参数 Git 命令。命令参数被白名单控制,不接受任意 shell 输入。
如果要进一步生产化,可以用 JGit 替代部分命令调用。
为什么不自动修改源码
本系列第一阶段只做评审,不做修复。
代码自动修复涉及更高的权限边界和人工确认机制,适合作为后续扩展,而不是入门主线。
本篇小结
这一篇让项目具备了真实评审输入:
ReviewFinding结构。GitDiffTool。DiffSummaryService。FileContextTool。RuleCheckTool。- 规则化 findings。
- 前端基础 finding 展示。
下一篇,我们把任务状态和报告保存下来,让刷新页面后仍然能看到结果,并提供 Markdown 报告下载。
系列导航
| 篇目 | 标题 | 状态 |
|---|---|---|
| 1 | AgentScope-Java 入门:搭建 Review Copilot 项目骨架 | 已发布 |
| 2 | AgentScope-Java 入门:用 SSE 展示评审任务进度 | 已发布 |
| 3 | AgentScope-Java 入门:让评审助手读懂 Git diff | 本文 |
| 4 | AgentScope-Java 入门:保存评审状态并生成 Markdown 报告 | 待发布 |
| 5 | AgentScope-Java 入门:给代码评审助手加上只读安全边界 | 待发布 |
| 6 | AgentScope-Java 入门:用 Middleware 审计 Agent 调用 | 待发布 |
| 7 | AgentScope-Java 入门:完善 Vue 前端、发布 GitHub,并规划下一步 | 待发布 |
作者:亦暖筑序
系列仓库:获取方式见作者主页说明。
AgentScope-Java 版本:2.0.0-RC4