为纪念 35 周年:《Terminator 2: Judgment Day》将在全球影院重映
2026-07-13
2026-07-14 0
本文是「AgentScope-Java 2.0.0-RC4 项目式入门」系列第 2 篇。
本篇对应:
| 项目 | 内容 |
|---|---|
| 起点 tag | chapter-01-complete |
| 分支 | chapter/02-streaming-review |
| 完成 tag | chapter-02-complete |
| 目标 | 后端提供 SSE 事件流,前端展示评审进度 |
学习目标:
ReviewEvent 和事件类型。text/event-stream。EventSource 连接后端。代码评审不是一个瞬间完成的动作。
它至少包含这些阶段:
复制代码创建任务
读取 Git diff
读取文件上下文
规则检查
模型评审
生成 findings
生成报告
任务完成
如果前端只发一个 HTTP 请求,然后等待最终结果,用户会遇到两个问题:
所以我们需要事件流。
对这个项目来说,SSE 比 WebSocket 更合适:
| 方案 | 适合场景 | 本项目判断 |
|---|---|---|
| HTTP 轮询 | 状态变化很少 | 能用,但浪费请求 |
| WebSocket | 双向实时交互 | 现在不需要前端实时发消息 |
| SSE | 服务端持续推送事件 | 正好适合评审进度 |

先定义事件对象:
复制代码// ReviewEvent.java
public record ReviewEvent(
String jobId,
ReviewEventType type,
String message,
Map payload,
Instant timestamp) {
}
事件类型:
复制代码// ReviewEventType.java
public enum ReviewEventType {
JOB_CREATED,
DIFF_LOADED,
DIFF_SUMMARIZED,
FILE_CONTEXT_LOADED,
RULE_CHECK_DONE,
MODEL_REVIEWING,
FINDING_GENERATED,
REPORT_READY,
JOB_COMPLETED,
JOB_FAILED
}
这里的事件不是 AgentScope 的底层 AgentEvent,而是我们业务层的 ReviewEvent。
其中 DIFF_SUMMARIZED 在第 2 篇的模拟流程中不会出现,它是为第 3 篇预留的:当 Agent 对 diff 生成摘要后发布。提前在枚举里声明,避免后续跨篇加字段时编译不过。
后端用 ReviewEventPublisher 维护每个任务的事件流。
核心思路:
复制代码// ReviewEventPublisher.java
private final Map> sinks = new ConcurrentHashMap<>(); public void publish(ReviewEvent event) {
sink(event.jobId()).tryEmitNext(event);
}public Flux stream(String jobId) {
return sink(jobId).asFlux();
}
每个 jobId 对应一个事件管道。
当评审执行到某一步,就调用:
复制代码eventPublisher.publish(ReviewEvent.of(
job.id(),
ReviewEventType.DIFF_LOADED,
"Git diff 已读取。",
Map.of("changedFiles", diff.changedFiles())));
前端只订阅当前任务的事件,不需要关心其他任务。
Controller 增加接口:
复制代码// ReviewController.java
@GetMapping(value = "/api/reviews/{id}/events", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux streamEvents(@PathVariable String id) {
return reviewService.streamEvents(id);
}
返回类型是 Flux。
WebFlux 会把它编码成 SSE 数据。
浏览器端接收到的是一条条事件:
复制代码{
"jobId": "review-20260706-001",
"type": "DIFF_LOADED",
"message": "Git diff 已读取。",
"payload": {
"changedFiles": ["src/main/java/demo/DemoService.java"]
},
"timestamp": "2026-07-06T12:00:00Z"
}
任务创建后,不应该阻塞 HTTP 请求等待完整评审结束。
所以 createReview 做两件事:
ReviewJob。核心代码:
复制代码// ReviewService.java
public Mono createReview(ReviewRequest request) {
return Mono.fromCallable(() -> {
ReviewJob job = ReviewJob.created(request, repoRoot.toString());
jobStore.save(job); eventPublisher.publish(ReviewEvent.of(
job.id(),
ReviewEventType.JOB_CREATED,
"评审任务已创建。",
Map.of("repoPath", job.repoPath()))); Mono.fromRunnable(() -> runReview(job.id()))
.subscribeOn(Schedulers.boundedElastic())
.subscribe(); return job;
});
}
为什么这里用 boundedElastic?
因为后续会有文件读取、Git 命令、报告写入等阻塞操作。这类操作不应该占用 WebFlux 的事件循环线程。
前端 API 层:
复制代码// reviewApi.ts
export function connectReviewEvents(id: string, handlers: ReviewEventHandlers) {
const source = new EventSource(`/api/reviews/${id}/events`) source.onmessage = (message) => {
handlers.onEvent(JSON.parse(message.data))
} source.onerror = () => {
handlers.onError?.()
} return source
}
Pinia store 里维护事件列表:
复制代码// reviewStore.ts
function connectEvents(id: string) {
eventSource?.close()
eventSource = connectReviewEvents(id, {
onEvent(event) {
events.value.push(event)
if (event.type === 'JOB_COMPLETED' || event.type === 'JOB_FAILED') {
void load(id)
eventSource?.close()
eventSource = null
}
},
onError() {
error.value = 'SSE 连接已中断。请重新加载任务以刷新状态。'
},
})
}
这个 store 后续会继续扩展:
ReviewProgressView 做三件事:
ReviewJob。页面结构可以很简单:
复制代码
评审进度
事件时间线不要设计得太复杂。
第一版只需要:
| 字段 | 展示方式 |
|---|---|
type | 中文阶段名 |
message | 阶段说明 |
timestamp | 时间 |
启动后端和前端:
复制代码cd backend
mvn spring-boot:run
复制代码cd frontend
npm run dev
创建评审任务:
复制代码curl -X POST ^
-H "Content-Type: application/json" ^
-d "{"repoPath":"D:\workspace\demo-project","diffMode":"WORKING_TREE","sessionId":"demo"}"
订阅事件流:
复制代码curl -N {id}/events
预期能看到阶段事件不断输出。
前端访问:
复制代码{id}/progress
第 2 篇的 runReview 还是模拟流程,目的是先验证事件流,不代表真实评审已经完成。预期页面显示类似:
从第 3 篇开始,这些模拟事件会逐步替换成真实的 Git diff、文件上下文和规则检查事件。第 4 篇才会生成真实 Markdown 报告。
AgentScope-Java 本身有更细粒度的事件系统。
比如:
ModelCallStartEventModelCallEndEventTextBlockDeltaEventToolCallStartEventToolResultEndEvent这些事件适合调试 Agent 内部执行。
但前端业务页面不一定需要直接展示它们。
对用户来说,“源码上下文已读取”比“某个 ToolResultBlock 结束”更容易理解。
所以本项目采用两层事件:
| 层级 | 事件 | 用途 |
|---|---|---|
| AgentScope 底层 | AgentEvent | 调试、审计、Middleware |
| 业务层 | ReviewEvent | 前端进度、用户反馈 |
这也是做 Agent 产品时很重要的一点:不要把框架内部复杂度直接推给用户。
前端应该提示用户刷新状态。
本项目的状态会通过 GET /api/reviews/{id} 查询,所以 SSE 中断不应该导致任务丢失。
当前场景是服务端单向推送进度,SSE 更轻量。
等后续需要用户实时确认、暂停、继续时,再考虑 WebSocket 或 AgentScope HITL 事件。
因为评审可能耗时较长。
POST /api/reviews 应该只负责创建任务,具体执行放到后台。
这一篇完成了:
ReviewEvent。ReviewEventType。ReviewEventPublisher。GET /api/reviews/{id}/events。EventSource 连接。下一篇,我们把“模拟流程”变成“真实评审”:读取本地 Git diff、读取文件上下文,并生成第一批规则化 findings。
| 篇目 | 标题 | 状态 |
|---|---|---|
| 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,并规划下一步 | 待发布 |