本篇目标搭建 Spring Boot Spring AI 项目骨架实现单轮对话、多轮对话和 SSE 流式逐字输出。一、本篇关键概念1.1 Spring AI含义Spring 官方推出的 AI 应用开发框架提供统一的 API 抽象层来对接各种大模型OpenAI、通义千问、Ollama 等。作用屏蔽不同模型厂商的 API 差异让你用同一套代码切换不同模型就像 Spring Data 屏蔽了不同数据库的差异一样。1.2 ChatModel含义Spring AI 的核心接口代表一个可对话的模型。调用chatModel.call(prompt)即可获取模型回复。作用统一的对话入口。无论底层是通义千问、GPT 还是本地模型都通过这个接口调用。Spring Boot 自动根据配置注入对应实现。1.3 Prompt提示词含义发送给模型的完整输入包含一个或多个Message消息。可以理解为给 AI 的完整问题包。作用Prompt 是与模型交互的基本单元。它可以包含系统指令SystemMessage、用户问题UserMessage、历史对话AssistantMessage等模型根据这些信息生成回复。1.4 SSEServer-Sent Events含义一种 HTTP 长连接协议服务器主动向客户端推送数据。数据格式为data: xxx\n\n。作用实现逐字输出效果。模型每生成一个 Token 就通过 SSE 推送到浏览器用户无需等待完整回复体验类似 ChatGPT 的打字效果。1.5 Flux响应式流含义Project Reactor 提供的异步数据流类型代表0 到 N 个元素的异步序列。作用Spring AI 的流式接口返回FluxChatResponse每个元素是模型生成的一小段文本。配合 SSE实现非阻塞的流式响应。1.6 DashScope OpenAI 兼容接口含义阿里云 DashScope 平台提供的与 OpenAI API 格式兼容的接口地址。作用无需使用专用 SDK直接用 Spring AI 的 OpenAI Starter 即可对接通义千问只需将base-url改为 DashScope 的地址。二、项目搭建2.1 创建 Spring Boot 项目使用 Spring Initializr 创建项目Spring Boot: 4.0.5Java: 21依赖: Spring Web2.2 pom.xml 核心依赖properties java.version21/java.version spring-ai.version2.0.0-M4/spring-ai.version /properties dependencyManagement dependencies dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-bom/artifactId version${spring-ai.version}/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement dependencies !-- Web -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-webmvc/artifactId /dependency !-- WebFlux流式响应 Flux 支持 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-webflux/artifactId /dependency !-- Spring AI OpenAI兼容 DashScope -- dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-openai/artifactId /dependency !-- Lombok -- dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies !-- Spring AI 里程碑仓库 -- repositories repository idspring-milestones/id urlhttps://repo.spring.io/milestone/url /repository /repositories为什么需要 WebFlux虽然主项目用的是 WebMVCServlet但流式返回FluxSSE需要 Reactor 支持引入 WebFlux starter 即可在 MVC 项目中使用 Flux 返回值。2.3 application.yml 配置spring: application: name: testai ai: openai: api-key: ${DASHSCOPE_API_KEY:你的API-Key} base-url: https://dashscope.aliyuncs.com/compatible-mode chat: options: model: qwen-plus temperature: 0.7 max-tokens: 2000 server: port: 8080配置说明base-url指向 DashScope 的 OpenAI 兼容地址而非 OpenAI 官方model: qwen-plus默认使用通义千问 Plus 模型temperature: 0.7控制回答的随机性0 完全确定1 最具创造性max-tokens: 2000单次回复最大 Token 数三、基础对话实现3.1 ChatService — 对话业务层Service Slf4j public class ChatService { private final ChatModel chatModel; public ChatService(ChatModel chatModel) { this.chatModel chatModel; } /** 简单对话支持动态模型 */ public String chat(String userMessage, String model) { if (model ! null !model.isBlank()) { Prompt prompt new Prompt(userMessage, OpenAiChatOptions.builder().model(model).build()); return chatModel.call(prompt).getResult().getOutput().getText(); } return chatModel.call(userMessage); } /** 多轮对话携带历史消息 */ public String multiTurnChat(ListMessage history, String userMessage, String model) { ListMessage messages new ArrayList(history); messages.add(new UserMessage(userMessage)); Prompt prompt; if (model ! null !model.isBlank()) { prompt new Prompt(messages, OpenAiChatOptions.builder().model(model).build()); } else { prompt new Prompt(messages); } return chatModel.call(prompt).getResult().getOutput().getText(); } }要点解读ChatModel由 Spring AI 自动注入根据 yml 配置连接通义千问OpenAiChatOptions.builder().model(model)实现运行时动态切换模型多轮对话的关键把历史消息列表和当前消息一起传给模型3.2 ChatController — REST APIRestController RequestMapping(/api/chat) public class ChatController { private final ChatService chatService; public ChatController(ChatService chatService) { this.chatService chatService; } GetMapping(/models) public ResponseEntityListMapString, String getModels() { return ResponseEntity.ok(List.of( Map.of(id, qwen3.5-plus-2026-02-15, name, 千问3.5, group, Qwen3.5), Map.of(id, qwen3.6-flash-2026-04-16, name, 千问3.6, group, Qwen3.6) )); } PostMapping(/simple) public ResponseEntityChatResponseDTO simpleChat(RequestBody ChatRequest request) { String response chatService.chat(request.getMessage(), request.getModel()); return ResponseEntity.ok(new ChatResponseDTO(response)); } }四、流式对话实现4.1 StreamingChatServiceService Slf4j public class StreamingChatService { private final ChatModel chatModel; private static final int MAX_HISTORY_MESSAGES 20; public StreamingChatService(ChatModel chatModel) { this.chatModel chatModel; } public FluxString streamMultiTurnChat(String systemPrompt, ListMessage history, String message, String model) { ListMessage messages new ArrayList(); if (systemPrompt ! null !systemPrompt.isBlank()) { messages.add(new SystemMessage(systemPrompt)); } if (history ! null) { // 截断防止超 Token 限制 if (history.size() MAX_HISTORY_MESSAGES) { history history.subList(history.size() - MAX_HISTORY_MESSAGES, history.size()); } messages.addAll(history); } messages.add(new UserMessage(message)); Prompt prompt (model ! null !model.isBlank()) ? new Prompt(messages, OpenAiChatOptions.builder().model(model).build()) : new Prompt(messages); return chatModel.stream(prompt) .map(response - { String text response.getResult().getOutput().getText(); return text ! null ? text : ; }) .filter(s - !s.isEmpty()); } }核心方法对比方法返回类型说明chatModel.call(prompt)ChatResponse阻塞等待完整回复chatModel.stream(prompt)FluxChatResponse逐 Token 异步推送4.2 StreamingController — SSE 端点RestController RequestMapping(/api/chat) public class StreamingController { private final StreamingChatService streamingChatService; PostMapping(value /stream, produces MediaType.TEXT_EVENT_STREAM_VALUE) public FluxServerSentEventString streamChatPost(RequestBody ChatRequest request) { return streamingChatService.streamMultiTurnChat( request.getSystemPrompt(), null, request.getMessage(), request.getModel()) .map(content - ServerSentEvent.Stringbuilder() .data(content) .build()) .concatWith(Flux.just(ServerSentEvent.Stringbuilder() .event(complete) .data([DONE]) .build())); } }SSE 协议格式浏览器接收到的原始数据data:你 data:好 data: data:我是 data:AI助手 event:complete data:[DONE]4.3 前端 SSE 接收关键代码// 使用 fetch ReadableStream因为 EventSource 不支持 POST fetch(/api/chat/stream, { method: POST, headers: { Content-Type: application/json, Accept: text/event-stream }, body: JSON.stringify({ message: 你好, model: qwen-plus }) }).then(response { const reader response.body.getReader(); const decoder new TextDecoder(); function read() { reader.read().then(({ done, value }) { if (done) return; const text decoder.decode(value, { stream: true }); // 解析 SSE 格式按行拆分提取 data: 后的内容 text.split(\n).forEach(line { if (line.startsWith(data:)) { const content line.substring(5); if (content ! [DONE]) { bubble.innerHTML content; // 逐字追加 } } }); read(); // 递归读取 }); } read(); });五、DTO 数据传输对象// 请求体 Data public class ChatRequest { private String message; private String model; private String systemPrompt; private ListMessageDTO history; private boolean useRag; } // 消息 DTO Data public class MessageDTO { private String role; // user / assistant / system private String content; } // 响应体 Data AllArgsConstructor NoArgsConstructor public class ChatResponseDTO { private String content; }六、运行验证6.1 启动项目mvn spring-boot:run6.2 测试接口单轮对话curl -X POST http://localhost:8080/api/chat/simple \ -H Content-Type: application/json \ -d {message: 你好介绍一下自己}流式对话curl -X POST http://localhost:8080/api/chat/stream \ -H Content-Type: application/json \ -H Accept: text/event-stream \ -d {message: 用Java写一个冒泡排序}七、常见问题与调试Q1启动报错Could not resolve placeholder DASHSCOPE_API_KEY原因未配置 API Key。解决在application.yml中直接填写 Key或设置环境变量DASHSCOPE_API_KEY。Q2调用接口返回 401 Unauthorized原因API Key 无效或过期。解决登录 DashScope 控制台确认 Key 状态检查是否有余额。Q3流式接口返回了完整内容而不是逐字输出原因缺少produces MediaType.TEXT_EVENT_STREAM_VALUE注解或前端未正确处理 SSE。排查用 curl 测试确认响应头包含Content-Type: text/event-stream。Q4模型回复很慢10s可能原因网络延迟DashScope 服务器在阿里云max-tokens设置过大历史消息过长导致 Prompt 过大建议首次测试用简短问题确认连通后再测试长对话。Q5spring-ai-bom下载失败原因Spring AI 2.0 还在里程碑阶段需要添加 Spring Milestones 仓库。解决确认pom.xml中包含https://repo.spring.io/milestone仓库配置。八、生产环境建议8.1 API Key 安全# 不要在代码中硬编码使用环境变量或配置中心 spring.ai.openai.api-key: ${DASHSCOPE_API_KEY}生产环境通过Kubernetes Secret或Nacos 配置中心注入绝不提交 Key 到 Git 仓库8.2 限流与成本控制添加接口限流如 Bucket4j、Sentinel防止恶意调用记录每次调用的 Token 消耗设置单用户日/月额度上限使用max-tokens限制单次回复长度8.3 超时与重试// 建议配置 HTTP 客户端超时 spring.ai.openai.connect-timeout: 10s spring.ai.openai.read-timeout: 60s流式接口建议设置较长的read-timeout模型生成较慢时需要持续等待非流式接口可配合Spring Retry实现失败重试8.4 日志与监控记录每次请求的耗时、Token 消耗、模型名称接入 Prometheus Grafana 监控 AI 调用指标异常调用自动告警8.5 多模型灰度生产环境建议同时配置多个模型通过A/B 测试对比不同模型的回答质量大模型降级策略主模型不可用时自动切换备用模型