SpringAI流式对话(带前端)

SpringAI流式对话(带前端)

引言在现代Web应用中,流式对话(Streaming Chat)已经成为提升用户体验的重要手段。尤其是在AI助手、实时客服等场景中,用户希望看到AI“边思考边输出”,而不是等待完整响应后才显示。Spring AI框架提供了对流式响应的原生支持,结合前端技术,可以轻松实现类似ChatGPT的打字机效果。本文将深入剖析SpringAI流式对话的原理,并提供一个完整的可运行示例(后端Spring Boot + 前端Vue.js)。## 流式对话的核心原理### 为什么需要流式响应?传统的REST API请求-响应模式中,客户端发送请求后,服务器需要处理完整个任务才返回结果。对于大语言模型,生成一段长文本可能需要数秒甚至数十秒,用户在此期间只能看到加载动画,体验较差。流式响应(Server-Sent Events,SSE)允许服务器逐步推送数据,客户端可以实时渲染,实现“逐字输出”的效果。### Spring AI 如何实现流式?Spring AI 的ChatClient接口提供了stream()方法,它返回一个Flux<ChatResponse>(基于Project Reactor的响应式流)。每个ChatResponse包含一个token(词元),例如一个单词或一个字符。后端通过HTTP的text/event-stream格式,将每个token作为事件推送。前端使用EventSourceFetch API读取流,并逐段更新DOM。### 技术栈选择-后端:Spring Boot 3.2 + Spring AI 1.0.0 + OpenAI API(或其他兼容模型)-前端:Vue 3 + Vite,使用原生EventSource接收SSE-通信格式:Server-Sent Events (SSE)## 后端实现:Spring AI 流式对话服务### 项目依赖xml<!-- pom.xml --><dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-openai-spring-boot-starter</artifactId> <version>1.0.0</version></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId></dependency>### 配置在application.properties中配置OpenAI API密钥:propertiesspring.ai.openai.api-key=sk-your-api-keyspring.ai.openai.chat.model=gpt-3.5-turbo### 核心控制器javaimport org.springframework.ai.chat.ChatClient;import org.springframework.ai.chat.ChatResponse;import org.springframework.ai.chat.prompt.Prompt;import org.springframework.ai.chat.prompt.SystemPromptTemplate;import org.springframework.ai.chat.prompt.UserMessage;import org.springframework.http.MediaType;import org.springframework.web.bind.annotation.*;import reactor.core.publisher.Flux;@RestController@RequestMapping("/api/chat")public class ChatController { private final ChatClient chatClient; public ChatController(ChatClient chatClient) { this.chatClient = chatClient; } /** * 流式对话端点,返回text/event-stream格式 * @param message 用户消息 (来自请求体) * @return Flux<ChatResponse> 流式响应 */ @PostMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux<ChatResponse> streamChat(@RequestBody String message) { // 1. 构建系统提示(可选) SystemPromptTemplate systemPrompt = new SystemPromptTemplate( "你是一个友好的AI助手,请用中文回答用户问题。" ); // 2. 构建用户消息 UserMessage userMessage = new UserMessage(message); // 3. 创建Prompt对象 Prompt prompt = new Prompt(List.of(systemPrompt.createMessage(), userMessage)); // 4. 调用stream方法获取流式响应 return chatClient.stream(prompt) .doOnNext(response -> { // 可选:日志记录每个token System.out.println("Token: " + response.getResult().getOutput().getContent()); }); }}关键点解释:-produces = MediaType.TEXT_EVENT_STREAM_VALUE告诉Spring返回SSE格式-chatClient.stream(prompt)返回Flux<ChatResponse>,每个ChatResponse包含一个token-doOnNext()可以对每个token进行额外处理(如日志)## 前端实现:Vue 3 流式渲染### 搭建基础页面vue<template> <div class="chat-container"> <div class="messages" ref="messagesRef"> <div v-for="(msg, index) in messages" :key="index" :class="['message', msg.role]"> <div class="content" v-html="renderMessage(msg.content)"></div> </div> </div> <div class="input-area"> <input v-model="inputText" @keyup.enter="sendMessage" placeholder="输入消息..." :disabled="isLoading"/> <button @click="sendMessage" :disabled="isLoading || !inputText.trim()"> {{ isLoading ? '生成中...' : '发送' }} </button> </div> </div></template><script setup>import { ref, nextTick } from 'vue';const inputText = ref('');const messages = ref([]);const isLoading = ref(false);const messagesRef = ref(null);// 模拟Markdown渲染(简单实现)function renderMessage(content) { return content .replace(/\n/g, '<br>') .replace(/(.*?)/gs, '<pre><code>$1</code></pre>') .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');}async function sendMessage() { if (!inputText.value.trim() || isLoading.value) return; const userMessage = inputText.value; inputText.value = ''; // 添加用户消息 messages.value.push({ role: 'user', content: userMessage }); // 添加一个空的AI消息占位 const aiMessageIndex = messages.value.length; messages.value.push({ role: 'ai', content: '' }); isLoading.value = true; try { // 使用Fetch API读取SSE流 const response = await fetch('http://localhost:8080/api/chat/stream', { method: 'POST', headers: { 'Content-Type': 'text/plain' }, body: userMessage }); const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); // 解析SSE格式(data: 字段) const lines = buffer.split('\n'); buffer = lines.pop(); // 保留未完成行 for (const line of lines) { if (line.startsWith('data: ')) { const data = line.substring(6); try { // Spring AI 返回的ChatResponse JSON const parsed = JSON.parse(data); const content = parsed.result?.output?.content || ''; if (content) { // 追加到AI消息末尾 messages.value[aiMessageIndex].content += content; // 滚动到底部 await nextTick(); messagesRef.value?.scrollTo(0, messagesRef.value.scrollHeight); } } catch (e) { // 忽略解析错误(如心跳包) } } } } } catch (error) { console.error('SSE连接失败:', error); messages.value[aiMessageIndex].content = '(连接失败,请重试)'; } finally { isLoading.value = false; }}</script><style>.chat-container { max-width: 800px; margin: 0 auto; padding: 20px; }.messages { height: 400px; overflow-y: auto; border: 1px solid #ddd; padding: 10px; }.message { margin-bottom: 10px; padding: 8px; border-radius: 5px; }.message.user { background: #e3f2fd; text-align: right; }.message.ai { background: #f5f5f5; }.input-area { display: flex; margin-top: 10px; }.input-area input { flex: 1; padding: 8px; }</style>前端关键原理:1. 使用fetchReadableStream读取SSE数据,而不是EventSource(因为需要POST请求)2. 逐行解析data:前缀的JSON数据3. 每次收到token,立即更新Vue响应式数据messages,触发DOM更新4. 通过nextTickscrollTo实现自动滚动## 深入剖析:SSE与WebSocket的对比| 特性 | SSE (Server-Sent Events) | WebSocket ||------|--------------------------|-----------|| 通信方向 | 仅服务器→客户端 | 双向 || 协议 | HTTP长连接 | 独立协议 (ws://) || 自动重连 | 原生支持 | 需手动实现 || 浏览器支持 | 所有现代浏览器 | 所有现代浏览器 || 适用场景 | 实时通知、流式数据推送 | 实时游戏、聊天应用 |对于流式对话场景,SSE更轻量,且Spring AI原生支持SSE格式,无需额外配置。## 常见问题与优化### 1. 网络断开处理前端应实现重连逻辑,可以使用retry机制或显示“重新连接”按钮。### 2. 大模型速率限制OpenAI 等API可能有速率限制,后端可以添加令牌桶算法进行流控。### 3. 前端性能优化对于大量token,使用requestAnimationFrame批量更新DOM,避免频繁重绘。## 总结本文从原理到实践,完整演示了如何使用Spring AI构建流式对话服务,并配合Vue前端实现打字机效果。核心要点包括:-后端:利用Spring AI的Flux<ChatResponse>和SSE格式,将每个token逐步推送到客户端-前端:使用ReadableStream读取流式数据,逐段渲染到UI-架构:SSE模式天然适合AI对话场景,无需复杂的WebSocket配置通过流式响应,用户无需等待完整回答,体验大幅提升。你可以将此模式扩展到其他AI服务(如Claude、Llama等),只需更换Spring AI的模型适配器即可。希望本文能帮助你快速构建生产级的AI对话应用!