1. Spring AI与SSE技术背景解析
Spring AI是Spring生态系统中用于构建人工智能应用的新兴框架,它通过标准化协议(如MCP)简化了与各类AI模型的交互。SSE(Server-Sent Events)则是基于HTTP的长连接技术,允许服务端主动向客户端推送数据,特别适合需要实时更新的AI交互场景。
在Spring AI中,SSE主要用于以下场景:
- 大语言模型(LLM)的流式响应输出
- 实时显示AI处理进度
- 长时间运行任务的中间结果推送
- 多步骤AI工作流的阶段更新
注意:SSE是单向通信(服务端→客户端),与WebSocket的双向通信有本质区别。选择SSE而非WebSocket的场景通常是只需要服务器推送的场合。
2. Spring AI-SSE核心实现方案
2.1 基础环境配置
首先需要在项目中添加SSE传输依赖:
xml复制<!-- Maven配置 -->
<dependency>
<groupId>org.springframework.experimental</groupId>
<artifactId>mcp-webmvc-sse-transport</artifactId>
</dependency>
<!-- 如果使用WebFlux -->
<dependency>
<groupId>org.springframework.experimental</groupId>
<artifactId>mcp-webflux-sse-transport</artifactId>
</dependency>
对于Gradle项目:
groovy复制implementation 'org.springframework.experimental:mcp-webmvc-sse-transport'
// 或
implementation 'org.springframework.experimental:mcp-webflux-sse-transport'
2.2 服务端实现
2.2.1 WebMvc实现方案
java复制@RestController
@RequestMapping("/ai/sse")
public class SseAiController {
@GetMapping("/stream")
public SseEmitter streamAiResponse(@RequestParam String query) {
SseEmitter emitter = new SseEmitter(30_000L); // 30秒超时
// 模拟AI处理线程
CompletableFuture.runAsync(() -> {
try {
for (int i = 1; i <= 5; i++) {
// 模拟AI处理分片结果
String chunk = "AI处理进度:" + i*20 + "%";
emitter.send(SseEmitter.event()
.id("chunk-"+i)
.data(chunk)
.comment("部分响应"));
Thread.sleep(500); // 模拟处理延迟
}
emitter.complete();
} catch (Exception ex) {
emitter.completeWithError(ex);
}
});
return emitter;
}
}
2.2.2 WebFlux实现方案
java复制@RestController
@RequestMapping("/ai/sse")
public class ReactiveSseAiController {
@GetMapping(value = "/flux-stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> streamAiResponse(@RequestParam String query) {
return Flux.interval(Duration.ofMillis(500))
.take(5)
.map(i -> "AI处理进度:" + (i+1)*20 + "%");
}
}
2.3 客户端实现
2.3.1 浏览器端JavaScript实现
javascript复制const eventSource = new EventSource('/ai/sse/stream?query=你好');
eventSource.onmessage = (event) => {
console.log('收到AI响应:', event.data);
// 更新UI...
};
eventSource.onerror = (err) => {
console.error('SSE连接错误:', err);
eventSource.close();
};
2.3.2 Java HttpClient实现
java复制public void consumeAiSseStream() {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("http://localhost:8080/ai/sse/stream?query=你好"))
.build();
client.sendAsync(request, HttpResponse.BodyHandlers.ofLines())
.thenAccept(response -> {
response.body().forEach(line -> {
if (line.startsWith("data:")) {
System.out.println("AI响应: " + line.substring(5).trim());
}
});
});
}
3. 高级应用与问题解决
3.1 大语言模型(LLM)流式响应
实现LLM的逐词输出:
java复制@GetMapping("/llm-stream")
public SseEmitter streamLlmResponse(@RequestParam String prompt) {
SseEmitter emitter = new SseEmitter();
aiService.generateStream(prompt, new StreamingResponseHandler() {
@Override
public void onNext(String token) {
try {
emitter.send(SseEmitter.event()
.data(token)
.id(UUID.randomUUID().toString()));
} catch (IOException e) {
// 处理异常
}
}
@Override
public void onComplete() {
emitter.complete();
}
@Override
public void onError(Throwable t) {
emitter.completeWithError(t);
}
});
return emitter;
}
3.2 常见问题解决方案
3.2.1 连接超时问题
java复制// 解决方案:调整超时时间和心跳机制
SseEmitter emitter = new SseEmitter(180_000L); // 3分钟超时
// 添加心跳事件
ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
scheduler.scheduleAtFixedRate(() -> {
try {
emitter.send(SseEmitter.event().comment("heartbeat"));
} catch (IOException e) {
scheduler.shutdown();
}
}, 15, 15, TimeUnit.SECONDS);
3.2.2 消息顺序保证
java复制// 使用sequence编号确保顺序
AtomicLong sequence = new AtomicLong();
emitter.send(SseEmitter.event()
.id(String.valueOf(sequence.incrementAndGet()))
.data(chunk)
.name("ai-chunk"));
3.2.3 错误恢复机制
javascript复制// 前端实现自动重连
function setupEventSource() {
const es = new EventSource('/ai/sse/stream');
es.onerror = () => {
es.close();
setTimeout(setupEventSource, 5000); // 5秒后重连
};
return es;
}
4. 性能优化与最佳实践
4.1 服务端配置优化
yaml复制# application.yml配置
spring:
mvc:
async:
request-timeout: 300000 # 5分钟超时
servlet:
servlet:
async-timeout: 300000
4.2 负载均衡考虑
在Nginx中增加配置:
nginx复制proxy_buffering off;
proxy_cache off;
proxy_read_timeout 86400s; # 长连接超时
4.3 监控与度量
java复制@Bean
public MeterRegistryCustomizer<MeterRegistry> sseMetrics() {
return registry -> {
SseEmitterMetrics.monitor(registry, "ai.sse.emitters");
};
}
5. 实际应用案例
5.1 智能客服对话系统
java复制@GetMapping("/chat")
public SseEmitter chatStream(@RequestParam String question) {
SseEmitter emitter = new SseEmitter();
chatService.streamAnswer(question, new ChatCallback() {
@Override
public void onThinking(String thought) {
sendEvent(emitter, "thinking", thought);
}
@Override
public void onAnswerChunk(String chunk) {
sendEvent(emitter, "answer", chunk);
}
@Override
public void onComplete() {
emitter.complete();
}
});
return emitter;
}
private void sendEvent(SseEmitter emitter, String type, String data) {
try {
emitter.send(SseEmitter.event()
.name(type)
.data(data));
} catch (IOException e) {
// 错误处理
}
}
5.2 实时数据分析仪表盘
java复制@GetMapping("/analytics")
public Flux<AnalyticsEvent> realtimeAnalytics() {
return Flux.interval(Duration.ofSeconds(1))
.flatMap(tick -> analyticsService.getLatestMetrics())
.map(metric -> new AnalyticsEvent(metric));
}
6. 调试与测试技巧
6.1 使用curl测试SSE接口
bash复制curl -N http://localhost:8080/ai/sse/stream?query=test
6.2 单元测试示例
java复制@Test
public void testSseEndpoint() throws Exception {
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
MvcResult mvcResult = mockMvc.perform(get("/ai/sse/stream?query=test"))
.andExpect(request().asyncStarted())
.andReturn();
MockAsyncContext asyncContext = (MockAsyncContext) mvcResult.getRequest().getAsyncContext();
String content = asyncContext.getOut().getContentAsString();
assertThat(content).contains("AI处理进度");
}
6.3 浏览器开发者工具分析
在Chrome开发者工具的Network标签中:
- 过滤
type:eventsource请求 - 查看EventStream面板中的原始事件
- 检查事件ID、类型和数据内容
7. 安全考虑
7.1 认证与授权
java复制@GetMapping("/secure-stream")
public SseEmitter secureStream(@RequestParam String query,
@AuthenticationPrincipal User user) {
if (!user.hasPermission("AI_ACCESS")) {
throw new AccessDeniedException("无访问权限");
}
// ...正常SSE逻辑
}
7.2 CSRF防护
对于需要修改状态的SSE请求:
javascript复制// 前端添加CSRF token
const eventSource = new EventSource(`/ai/sse/stream?query=test&_csrf=${csrfToken}`);
7.3 速率限制
java复制@RateLimiter(value = 10, duration = 1, unit = TimeUnit.MINUTES)
@GetMapping("/limited-stream")
public SseEmitter rateLimitedStream() {
// ...
}
8. 扩展与集成
8.1 与Spring AI Alibaba集成
java复制@Bean
public SseTransport alibabaAiSseTransport() {
return new HttpServletSseServerTransport("/alibaba-ai/sse");
}
@Bean
public ReactAgent reactAgent(SseTransport transport) {
return new ReactAgent.Builder()
.transport(transport)
.model(alibabaAiModel())
.build();
}
8.2 实现RAG混合检索
java复制@GetMapping("/rag-search")
public Flux<String> ragSearchStream(@RequestParam String question) {
return ragService.streamSearch(question)
.delayElements(Duration.ofMillis(200))
.map(SearchResult::getSnippet);
}
8.3 输入压缩实现
java复制@PostMapping("/compressed-input")
public SseEmitter handleCompressedInput(@RequestBody byte[] compressedInput) {
String input = CompressionUtils.decompress(compressedInput);
return processAiRequest(input);
}
在项目实践中,我发现SSE连接在移动端网络环境下可能会不稳定,建议实现以下增强策略:
- 客户端自动重连机制
- 服务端连接状态追踪
- 关键消息的确认重传机制
- 离线消息缓存(当连接恢复后补发)
对于高并发场景,还需要特别注意:
- 使用NIO而非线程-per-connection模型
- 合理配置连接池参数
- 考虑使用专门的SSE代理服务分担负载
