AgentScope-Java 入门:用 SSE 展示评审任务进度
一句话导语 :上一篇我们创建了最小
ReviewJob。这一篇给评审任务加上事件流,让用户能实时看到任务走到了哪一步。
系列说明
本文是「AgentScope-Java 2.0.0-RC4 项目式入门」系列第 2 篇。
本篇对应:
| 项目 | 内容 |
|---|---|
| 起点 tag | chapter-01-complete |
| 分支 | chapter/02-streaming-review |
| 完成 tag | chapter-02-complete |
| 目标 | 后端提供 SSE 事件流,前端展示评审进度 |
学习目标:
- 理解为什么 Agent 应用需要事件流。
- 定义
ReviewEvent和事件类型。 - 用 WebFlux 输出
text/event-stream。 - 用 Vue
EventSource连接后端。 - 让创建任务、执行任务、任务完成形成可观察链路。
为什么需要 SSE
代码评审不是一个瞬间完成的动作。
它至少包含这些阶段:
text
创建任务
读取 Git diff
读取文件上下文
规则检查
模型评审
生成 findings
生成报告
任务完成
如果前端只发一个 HTTP 请求,然后等待最终结果,用户会遇到两个问题:
- 不知道后端是否还在运行。
- 不知道卡在哪个阶段。
所以我们需要事件流。
对这个项目来说,SSE 比 WebSocket 更合适:
| 方案 | 适合场景 | 本项目判断 |
|---|---|---|
| HTTP 轮询 | 状态变化很少 | 能用,但浪费请求 |
| WebSocket | 双向实时交互 | 现在不需要前端实时发消息 |
| SSE | 服务端持续推送事件 | 正好适合评审进度 |

定义 ReviewEvent
先定义事件对象:
java
// ReviewEvent.java
public record ReviewEvent(
String jobId,
ReviewEventType type,
String message,
Map<String, Object> payload,
Instant timestamp) {
}
事件类型:
java
// 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 生成摘要后发布。提前在枚举里声明,避免后续跨篇加字段时编译不过。
设计要点 :业务事件要服务前端展示,不要把底层框架事件原样暴露出去。AgentScope 的
AgentEvent很细,适合内部观测;ReviewEvent更适合产品 UI。
用 Reactor Sinks 管理事件流
后端用 ReviewEventPublisher 维护每个任务的事件流。
核心思路:
java
// ReviewEventPublisher.java
private final Map<String, Sinks.Many<ReviewEvent>> sinks = new ConcurrentHashMap<>();
public void publish(ReviewEvent event) {
sink(event.jobId()).tryEmitNext(event);
}
public Flux<ReviewEvent> stream(String jobId) {
return sink(jobId).asFlux();
}
每个 jobId 对应一个事件管道。
当评审执行到某一步,就调用:
java
eventPublisher.publish(ReviewEvent.of(
job.id(),
ReviewEventType.DIFF_LOADED,
"Git diff 已读取。",
Map.of("changedFiles", diff.changedFiles())));
前端只订阅当前任务的事件,不需要关心其他任务。
提供 SSE API
Controller 增加接口:
java
// ReviewController.java
@GetMapping(value = "/api/reviews/{id}/events", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ReviewEvent> streamEvents(@PathVariable String id) {
return reviewService.streamEvents(id);
}
返回类型是 Flux<ReviewEvent>。
WebFlux 会把它编码成 SSE 数据。
浏览器端接收到的是一条条事件:
json
{
"jobId": "review-20260706-001",
"type": "DIFF_LOADED",
"message": "Git diff 已读取。",
"payload": {
"changedFiles": ["src/main/java/demo/DemoService.java"]
},
"timestamp": "2026-07-06T12:00:00Z"
}
让 ReviewService 发布阶段事件
任务创建后,不应该阻塞 HTTP 请求等待完整评审结束。
所以 createReview 做两件事:
- 保存
ReviewJob。 - 异步启动评审流程。
核心代码:
java
// ReviewService.java
public Mono<ReviewJob> 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 的事件循环线程。
前端封装 EventSource
前端 API 层:
ts
// 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 里维护事件列表:
ts
// 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 后续会继续扩展:
- 保存当前任务。
- 保存 findings。
- 保存 Markdown 报告。
- 复制报告。
进度页面
ReviewProgressView 做三件事:
- 加载当前
ReviewJob。 - 建立 SSE 连接。
- 展示事件时间线。
页面结构可以很简单:
vue
<template>
<main>
<h1>评审进度</h1>
<ReviewStatusBadge :status="store.currentJob?.status" />
<ReviewEventTimeline :events="store.events" />
</main>
</template>
事件时间线不要设计得太复杂。
第一版只需要:
| 字段 | 展示方式 |
|---|---|
type |
中文阶段名 |
message |
阶段说明 |
timestamp |
时间 |
验证
启动后端和前端:
bash
cd backend
mvn spring-boot:run
bash
cd frontend
npm run dev
创建评审任务:
bash
curl -X POST http://localhost:8080/api/reviews ^
-H "Content-Type: application/json" ^
-d "{\"repoPath\":\"D:\\workspace\\demo-project\",\"diffMode\":\"WORKING_TREE\",\"sessionId\":\"demo\"}"
订阅事件流:
bash
curl -N http://localhost:8080/api/reviews/{id}/events
预期能看到阶段事件不断输出。
前端访问:
text
http://localhost:5173/reviews/{id}/progress
第 2 篇的 runReview 还是模拟流程,目的是先验证事件流,不代表真实评审已经完成。预期页面显示类似:
- 评审任务已创建。
- 评审流水线已启动。
- 模拟阶段事件持续出现。
- 模拟任务完成。
从第 3 篇开始,这些模拟事件会逐步替换成真实的 Git diff、文件上下文和规则检查事件。第 4 篇才会生成真实 Markdown 报告。
设计思考:业务事件和 Agent 事件要分层
AgentScope-Java 本身有更细粒度的事件系统。
比如:
ModelCallStartEventModelCallEndEventTextBlockDeltaEventToolCallStartEventToolResultEndEvent
这些事件适合调试 Agent 内部执行。
但前端业务页面不一定需要直接展示它们。
对用户来说,"源码上下文已读取"比"某个 ToolResultBlock 结束"更容易理解。
所以本项目采用两层事件:
| 层级 | 事件 | 用途 |
|---|---|---|
| AgentScope 底层 | AgentEvent |
调试、审计、Middleware |
| 业务层 | ReviewEvent |
前端进度、用户反馈 |
这也是做 Agent 产品时很重要的一点:不要把框架内部复杂度直接推给用户。
常见问题
SSE 连接中断怎么办
前端应该提示用户刷新状态。
本项目的状态会通过 GET /api/reviews/{id} 查询,所以 SSE 中断不应该导致任务丢失。
为什么不是 WebSocket
当前场景是服务端单向推送进度,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,并规划下一步 | 待发布 |
作者:亦暖筑序
系列仓库:获取方式见作者主页说明。
AgentScope-Java 版本:2.0.0-RC4