AgentScope-Java 入门:用 SSE 展示评审任务进度
AgentScope-Java 入门用 SSE 展示评审任务进度一句话导语上一篇我们创建了最小ReviewJob。这一篇给评审任务加上事件流让用户能实时看到任务走到了哪一步。系列说明本文是「AgentScope-Java 2.0.0-RC4 项目式入门」系列第 2 篇。本篇对应项目内容起点 tagchapter-01-complete分支chapter/02-streaming-review完成 tagchapter-02-complete目标后端提供 SSE 事件流前端展示评审进度学习目标理解为什么 Agent 应用需要事件流。定义ReviewEvent和事件类型。用 WebFlux 输出text/event-stream。用 VueEventSource连接后端。让创建任务、执行任务、任务完成形成可观察链路。为什么需要 SSE代码评审不是一个瞬间完成的动作。它至少包含这些阶段创建任务 读取 Git diff 读取文件上下文 规则检查 模型评审 生成 findings 生成报告 任务完成如果前端只发一个 HTTP 请求然后等待最终结果用户会遇到两个问题不知道后端是否还在运行。不知道卡在哪个阶段。所以我们需要事件流。对这个项目来说SSE 比 WebSocket 更合适方案适合场景本项目判断HTTP 轮询状态变化很少能用但浪费请求WebSocket双向实时交互现在不需要前端实时发消息SSE服务端持续推送事件正好适合评审进度定义 ReviewEvent先定义事件对象// ReviewEvent.javapublicrecordReviewEvent(StringjobId,ReviewEventTypetype,Stringmessage,MapString,Objectpayload,Instanttimestamp){}事件类型// ReviewEventType.javapublicenumReviewEventType{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维护每个任务的事件流。核心思路// ReviewEventPublisher.javaprivatefinalMapString,Sinks.ManyReviewEventsinksnewConcurrentHashMap();publicvoidpublish(ReviewEventevent){sink(event.jobId()).tryEmitNext(event);}publicFluxReviewEventstream(StringjobId){returnsink(jobId).asFlux();}每个jobId对应一个事件管道。当评审执行到某一步就调用eventPublisher.publish(ReviewEvent.of(job.id(),ReviewEventType.DIFF_LOADED,Git diff 已读取。,Map.of(changedFiles,diff.changedFiles())));前端只订阅当前任务的事件不需要关心其他任务。提供 SSE APIController 增加接口// ReviewController.javaGetMapping(value/api/reviews/{id}/events,producesMediaType.TEXT_EVENT_STREAM_VALUE)publicFluxReviewEventstreamEvents(PathVariableStringid){returnreviewService.streamEvents(id);}返回类型是FluxReviewEvent。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}让 ReviewService 发布阶段事件任务创建后不应该阻塞 HTTP 请求等待完整评审结束。所以createReview做两件事保存ReviewJob。异步启动评审流程。核心代码// ReviewService.javapublicMonoReviewJobcreateReview(ReviewRequestrequest){returnMono.fromCallable(()-{ReviewJobjobReviewJob.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();returnjob;});}为什么这里用boundedElastic因为后续会有文件读取、Git 命令、报告写入等阻塞操作。这类操作不应该占用 WebFlux 的事件循环线程。前端封装 EventSource前端 API 层// reviewApi.tsexportfunctionconnectReviewEvents(id:string,handlers:ReviewEventHandlers){constsourcenewEventSource(/api/reviews/${id}/events)source.onmessage(message){handlers.onEvent(JSON.parse(message.data))}source.onerror(){handlers.onError?.()}returnsource}Pinia store 里维护事件列表// reviewStore.tsfunctionconnectEvents(id:string){eventSource?.close()eventSourceconnectReviewEvents(id,{onEvent(event){events.value.push(event)if(event.typeJOB_COMPLETED||event.typeJOB_FAILED){voidload(id)eventSource?.close()eventSourcenull}},onError(){error.valueSSE 连接已中断。请重新加载任务以刷新状态。},})}这个 store 后续会继续扩展保存当前任务。保存 findings。保存 Markdown 报告。复制报告。进度页面ReviewProgressView做三件事加载当前ReviewJob。建立 SSE 连接。展示事件时间线。页面结构可以很简单template main h1评审进度/h1 ReviewStatusBadge :statusstore.currentJob?.status / ReviewEventTimeline :eventsstore.events / /main /template事件时间线不要设计得太复杂。第一版只需要字段展示方式type中文阶段名message阶段说明timestamp时间验证启动后端和前端cdbackend mvn spring-boot:runcdfrontendnpmrun dev创建评审任务curl-XPOST http://localhost:8080/api/reviews ^-HContent-Type: application/json^-d{\repoPath\:\D:\\workspace\\demo-project\,\diffMode\:\WORKING_TREE\,\sessionId\:\demo\}订阅事件流curl-Nhttp://localhost:8080/api/reviews/{id}/events预期能看到阶段事件不断输出。前端访问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。系列导航篇目标题状态1AgentScope-Java 入门搭建 Review Copilot 项目骨架已发布2AgentScope-Java 入门用 SSE 展示评审任务进度本文3AgentScope-Java 入门让评审助手读懂 Git diff待发布4AgentScope-Java 入门保存评审状态并生成 Markdown 报告待发布5AgentScope-Java 入门给代码评审助手加上只读安全边界待发布6AgentScope-Java 入门用 Middleware 审计 Agent 调用待发布7AgentScope-Java 入门完善 Vue 前端、发布 GitHub并规划下一步待发布作者亦暖筑序系列仓库https://github.com/ynzz-j/agentscope-review-copilotAgentScope-Java 版本2.0.0-RC4