Spring AI 实战教程(一):基础对话与流式输出 —— 让你的应用接入大模型
本篇目标:搭建 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 SSE(Server-Sent Events)
含义:一种 HTTP 长连接协议,服务器主动向客户端推送数据。数据格式为data: xxx\n\n。
作用:实现"逐字输出"效果。模型每生成一个 Token 就通过 SSE 推送到浏览器,用户无需等待完整回复,体验类似 ChatGPT 的打字效果。
1.5 Flux(响应式流)
含义:Project Reactor 提供的异步数据流类型,代表"0 到 N 个元素的异步序列"。
作用:Spring AI 的流式接口返回Flux<ChatResponse>,每个元素是模型生成的一小段文本。配合 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.5
- Java: 21
- 依赖: Spring Web
2.2 pom.xml 核心依赖
<properties> <java.version>21</java.version> <spring-ai.version>2.0.0-M4</spring-ai.version> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-bom</artifactId> <version>${spring-ai.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <!-- Web --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webmvc</artifactId> </dependency> <!-- WebFlux(流式响应 Flux 支持) --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency> <!-- Spring AI OpenAI(兼容 DashScope) --> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-model-openai</artifactId> </dependency> <!-- Lombok --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies> <!-- Spring AI 里程碑仓库 --> <repositories> <repository> <id>spring-milestones</id> <url>https://repo.spring.io/milestone</url> </repository> </repositories>为什么需要 WebFlux?虽然主项目用的是 WebMVC(Servlet),但流式返回
Flux<SSE>需要 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(List<Message> history, String userMessage, String model) { List<Message> 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 API
@RestController @RequestMapping("/api/chat") public class ChatController { private final ChatService chatService; public ChatController(ChatService chatService) { this.chatService = chatService; } @GetMapping("/models") public ResponseEntity<List<Map<String, 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 ResponseEntity<ChatResponseDTO> simpleChat(@RequestBody ChatRequest request) { String response = chatService.chat(request.getMessage(), request.getModel()); return ResponseEntity.ok(new ChatResponseDTO(response)); } }四、流式对话实现
4.1 StreamingChatService
@Service @Slf4j public class StreamingChatService { private final ChatModel chatModel; private static final int MAX_HISTORY_MESSAGES = 20; public StreamingChatService(ChatModel chatModel) { this.chatModel = chatModel; } public Flux<String> streamMultiTurnChat(String systemPrompt, List<Message> history, String message, String model) { List<Message> 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) | Flux<ChatResponse> | 逐 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 Flux<ServerSentEvent<String>> streamChatPost(@RequestBody ChatRequest request) { return streamingChatService.streamMultiTurnChat( request.getSystemPrompt(), null, request.getMessage(), request.getModel()) .map(content -> ServerSentEvent.<String>builder() .data(content) .build()) .concatWith(Flux.just(ServerSentEvent.<String>builder() .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 List<MessageDTO> 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 过大
建议:首次测试用简短问题,确认连通后再测试长对话。
Q5:spring-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 测试对比不同模型的回答质量
- 大模型降级策略:主模型不可用时自动切换备用模型
