当前位置: 首页 > news >正文

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:run

6.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 SecretNacos 配置中心注入
  • 绝不提交 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 测试对比不同模型的回答质量
  • 大模型降级策略:主模型不可用时自动切换备用模型
http://www.jsqmd.com/news/696764/

相关文章:

  • ONNX模型多线程推理并解决线程踩踏与显存溢出问题
  • AI Agent的“幻觉“问题:从根源到缓解的完整分析
  • 2026年苏州及周边叉车上岗证培训top5机构盘点:姑苏区n1证/姑苏区叉车上岗证/姑苏区叉车证/学叉车/选择指南 - 优质品牌商家
  • QMCDecode终极指南:如何快速解密QQ音乐加密文件实现跨平台播放
  • ARM SME2指令集:矩阵运算加速与AI性能优化
  • 移动应用开发中的跨平台框架选择与性能对比
  • 安全与权限管理:保障模型与数据资产的安全
  • 从理论到实践:基于扩展卡尔曼滤波(EKF)的永磁同步电机无位置传感器FOC控制
  • 别再傻傻用加法器了!Verilog里这个‘分治’数1技巧,帮你省下FPGA的宝贵资源
  • AI Agent Harness Engineering 的元认知:让它学会评估自身能力与知识边界
  • RWKV-7 (1.5B World) 显存优化部署教程:BF16+单卡强制绑定技巧
  • Web3时代的AI量化是什么?Alpha AI 告诉你答案
  • 手把手教你用Debian Live OS救活CentOS 8:GLIBC升级翻车后的机房急救实录
  • Torch MMCV 深度学习模型报错原因及解决方法汇总(长期更新)
  • 实战部署:在云服务器上快速搭建与运行主流大模型
  • WeDLM-7B-Base算力优化案例:单卡24GB实现32K上下文稳定推理的配置
  • Java转Agent,我替你踩所有坑
  • 企业微信智能机器人一键对接OpenClaw教程
  • WrenAI:基于语义层的自然语言数据查询引擎设计与实践
  • 研发leader如何增强自身在外部就业市场的竞争力
  • NiCE5340 SoM模块:高集成度嵌入式系统开发解析
  • GVHMR
  • 如何快速实现手机号码地理位置定位:ASP.NET解决方案实战指南
  • YOLOv11改进系列 | 原创C3k2_ConvFormerCGLU模块,SepConv Token混合叠加卷积门控FFN,特征表达更强
  • 阿里二面:RAG 检索优化策略有哪些?
  • 告别卡顿!用这个Vue3+TS移动端Table组件,轻松渲染1000条数据
  • Phi-3.5-Mini-Instruct 工业视觉应用:与传统OpenCV算法结合实践
  • linux: 银河麒麟v10安装mysql8
  • NotaGen快速部署:一键启动WebUI,5分钟开始音乐创作之旅
  • 【SQL】SQL同环比计算的多种实现方式