从 0 到 1:Spring Boot 与 Spring AI 深度实战(基于深度求索 DeepSeek)
在人工智能技术与企业级开发深度融合的今天,传统软件开发模式与 AI 工程化开发的差异日益显著。作为 Spring 生态体系中专注于 AI 工程化的核心框架,Spring AI通过标准化集成方案大幅降低 AI 应用开发门槛。本文将以国产大模型代表 ** 深度求索(DeepSeek)** 为例,完整演示从环境搭建到核心机制解析的全流程,带您掌握企业级 AI 应用开发的核心能力。
一、传统开发 vs AI 工程化:范式革命与技术挑战
1. 开发模式对比
维度 | 传统软件开发 | AI 工程化开发 |
---|---|---|
核心驱动 | 业务逻辑与算法实现 | 数据驱动的模型训练与推理 |
输出特性 | 确定性结果(基于固定规则) | 概率性结果(基于统计学习) |
核心资产 | 业务代码与数据结构 | 高质量数据集与训练好的模型 |
迭代方式 | 功能模块增量开发 | 数据标注→模型训练→推理优化的闭环迭代 |
2. AI 工程化核心挑战
- 数据治理难题:需解决数据采集(如爬虫反爬)、清洗(异常值处理)、标注(实体识别)等全链路问题
- 模型工程复杂度:涉及模型选型(如选择 DeepSeek-R1 还是 Llama 系列)、训练调优(超参数搜索)、量化压缩(模型轻量化)
- 生产级部署要求:需支持高并发推理(如 Token 级流输出)、多模型管理(A/B 测试)、实时监控(延迟 / 成功率指标)
传统 Spring Boot 的 MVC 架构难以直接应对这些挑战,而Spring AI通过标准化接口封装与生态整合,将 AI 能力转化为可插拔的工程组件。
二、Spring AI x DeepSeek:国产化 AI 工程解决方案
1. DeepSeek 模型优势
作为国内领先的 AGI 公司,深度求索(DeepSeek)提供:
- 高性能推理引擎:支持长上下文(8K/32K tokens 可选)与流式输出
- 企业级安全合规:数据本地化部署方案(支持私有化云)
- 多模态能力扩展:后续可无缝集成图像 / 语音处理模块
通过spring-ai-deepseek
模块,Spring Boot 应用可通过注解驱动方式调用 DeepSeek 模型,底层自动处理 HTTP 连接池管理、请求重试、响应解析等工程化问题。
三、实战开发:基于 DeepSeek 的智能文本生成系统
1. 项目搭建
目录结构
通过 Spring Initializr 创建项目时,添加 DeepSeek 专用依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-ai-deepseek</artifactId>
</dependency>
或在 pom.xml
中手动添加上述依赖,Maven 会自动解析 DeepSeek 集成所需的全部组件。
2. 配置 DeepSeek
在 application.yml
中配置 DeepSeek 服务信息(含注册指引):
# DeepSeek 服务配置(官方文档:https://docs.spring.io/spring-ai/reference/api/chat/deepseek-chat.html)
spring:ai:deepseek:# 必需:在DeepSeek控制台申请的API密钥(注册地址:https://platform.deepseek.com/register)api-key: ${DEEPSEEK_API_KEY:your-deepseek-api-key}# API基础地址(私有化部署需修改)base-url: https://api.deepseek.com# 聊天模型配置chat:enabled: trueoptions:model: deepseek-chat # 使用deepseek-chat模型temperature: 0.8 # 生成随机性控制(0.0-1.0,值越高越随机)max-tokens: 512 # 单次生成最大Token数top-p: 0.9 # Nucleus采样参数(0.0-1.0,控制生成词汇的概率分布)frequency-penalty: 0.0 # 频率惩罚(-2.0到2.0)presence-penalty: 0.0 # 存在惩罚(-2.0到2.0)stop: ["###", "END"] # 生成停止序列# 重试配置retry:max-attempts: 3 # 最大重试次数backoff:initial-interval: 2s # 初始重试间隔multiplier: 2 # 重试间隔倍数max-interval: 10s # 最大重试间隔on-client-errors: false # 是否对4xx错误重试# 应用服务器配置
server:port: 8080 # 服务端口servlet:context-path: / # 上下文路径encoding:charset: UTF-8 # 字符编码force: true # 强制编码# 日志配置
logging:level:root: INFOcom.example.demo: DEBUGorg.springframework.ai: DEBUGorg.springframework.ai.deepseek: DEBUGpattern:console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"# 管理端点配置
management:endpoints:web:exposure:include: health,info,metrics,envbase-path: /actuatorendpoint:health:show-details: alwaysserver:port: 8080
3. 编写代码
(1)DeepSeek 服务封装(SmartGeneratorService.java
)
package com.example.demo.service;import com.example.demo.dto.AiRequest;
import com.example.demo.dto.AiResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.PromptTemplate;
import org.springframework.ai.deepseek.DeepSeekChatOptions;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;import java.util.Map;/*** 智能生成服务* 提供营销文案生成、代码生成、智能问答等功能* * @author Spring AI Demo*/
@Service
public class SmartGeneratorService {private static final Logger logger = LoggerFactory.getLogger(SmartGeneratorService.class);private final ChatModel chatModel;public SmartGeneratorService(ChatModel chatModel) {this.chatModel = chatModel;}/*** 生成营销文案* * @param request 请求参数* @return AI响应*/public AiResponse generateMarketingContent(AiRequest request) {logger.info("开始生成营销文案,输入:{}", request.getContent());long startTime = System.currentTimeMillis();try {String systemPrompt = """你是一位专业的营销文案专家,擅长创作吸引人的营销内容。请根据用户的需求,生成具有以下特点的营销文案:1. 吸引眼球的标题2. 突出产品/服务的核心价值3. 使用情感化的语言4. 包含明确的行动号召5. 语言简洁有力,易于理解请用中文回复,格式清晰,内容富有创意。""";PromptTemplate promptTemplate = new PromptTemplate(systemPrompt + "\n\n用户需求:{content}");Prompt prompt = promptTemplate.create(Map.of("content", request.getContent()));// 设置营销文案生成的参数(创意性较高)DeepSeekChatOptions options = DeepSeekChatOptions.builder().temperature(request.getTemperature() != null ? request.getTemperature() : 1.3).maxTokens(request.getMaxTokens() != null ? request.getMaxTokens() : 800).build();var response = chatModel.call(new Prompt(prompt.getInstructions(), options));String content = response.getResult().getOutput().getText();long processingTime = System.currentTimeMillis() - startTime;logger.info("营销文案生成完成,耗时:{}ms", processingTime);AiResponse aiResponse = AiResponse.success(content, "deepseek-chat");aiResponse.setProcessingTimeMs(processingTime);return aiResponse;} catch (Exception e) {logger.error("营销文案生成失败", e);return AiResponse.error("营销文案生成失败:" + e.getMessage());}}/*** 生成代码* * @param request 请求参数* @return AI响应*/public AiResponse generateCode(AiRequest request) {logger.info("开始生成代码,需求:{}", request.getContent());long startTime = System.currentTimeMillis();try {String systemPrompt = """你是一位资深的软件工程师,精通多种编程语言和技术栈。请根据用户的需求,生成高质量的代码,要求:1. 代码结构清晰,逻辑合理2. 包含必要的注释说明3. 遵循最佳实践和编码规范4. 考虑错误处理和边界情况5. 如果需要,提供使用示例请用中文注释,代码要完整可运行。""";PromptTemplate promptTemplate = new PromptTemplate(systemPrompt + "\n\n编程需求:{content}");Prompt prompt = promptTemplate.create(Map.of("content", request.getContent()));// 设置代码生成的参数(准确性优先)DeepSeekChatOptions options = DeepSeekChatOptions.builder().temperature(request.getTemperature() != null ? request.getTemperature() : 0.1).maxTokens(request.getMaxTokens() != null ? request.getMaxTokens() : 1500).build();var response = chatModel.call(new Prompt(prompt.getInstructions(), options));String content = response.getResult().getOutput().getText();long processingTime = System.currentTimeMillis() - startTime;logger.info("代码生成完成,耗时:{}ms", processingTime);AiResponse aiResponse = AiResponse.success(content, "deepseek-chat");aiResponse.setProcessingTimeMs(processingTime);return aiResponse;} catch (Exception e) {logger.error("代码生成失败", e);return AiResponse.error("代码生成失败:" + e.getMessage());}}/*** 智能问答* * @param request 请求参数* @return AI响应*/public AiResponse answerQuestion(AiRequest request) {logger.info("开始智能问答,问题:{}", request.getContent());long startTime = System.currentTimeMillis();try {String systemPrompt = """你是一位知识渊博的AI助手,能够回答各种领域的问题。请根据用户的问题,提供准确、详细、有用的回答:1. 回答要准确可靠,基于事实2. 解释要清晰易懂,层次分明3. 如果涉及专业术语,请适当解释4. 如果问题复杂,可以分步骤说明5. 如果不确定答案,请诚实说明请用中文回复,语言友好专业。""";PromptTemplate promptTemplate = new PromptTemplate(systemPrompt + "\n\n用户问题:{content}");Prompt prompt = promptTemplate.create(Map.of("content", request.getContent()));// 设置问答的参数(平衡准确性和流畅性)DeepSeekChatOptions options = DeepSeekChatOptions.builder().temperature(request.getTemperature() != null ? request.getTemperature() : 0.7).maxTokens(request.getMaxTokens() != null ? request.getMaxTokens() : 1000).build();var response = chatModel.call(new Prompt(prompt.getInstructions(), options));String content = response.getResult().getOutput().getText();long processingTime = System.currentTimeMillis() - startTime;logger.info("智能问答完成,耗时:{}ms", processingTime);AiResponse aiResponse = AiResponse.success(content, "deepseek-chat");aiResponse.setProcessingTimeMs(processingTime);return aiResponse;} catch (Exception e) {logger.error("智能问答失败", e);return AiResponse.error("智能问答失败:" + e.getMessage());}}/*** 通用聊天* * @param request 请求参数* @return AI响应*/public AiResponse chat(AiRequest request) {logger.info("开始聊天对话,消息:{}", request.getContent());long startTime = System.currentTimeMillis();try {String systemPrompt = request.getSystemPrompt() != null ? request.getSystemPrompt() : """你是一位友好、有帮助的AI助手。请以自然、亲切的方式与用户对话:1. 保持友好和礼貌的语调2. 根据上下文提供有用的回复3. 如果用户需要帮助,尽力提供支持4. 保持对话的连贯性和趣味性请用中文回复,语言自然流畅。""";PromptTemplate promptTemplate = new PromptTemplate(systemPrompt + "\n\n用户:{content}");Prompt prompt = promptTemplate.create(Map.of("content", request.getContent()));// 设置聊天的参数(自然对话)DeepSeekChatOptions options = DeepSeekChatOptions.builder().temperature(request.getTemperature() != null ? request.getTemperature() : 0.9).maxTokens(request.getMaxTokens() != null ? request.getMaxTokens() : 800).build();var response = chatModel.call(new Prompt(prompt.getInstructions(), options));String content = response.getResult().getOutput().getText();long processingTime = System.currentTimeMillis() - startTime;logger.info("聊天对话完成,耗时:{}ms", processingTime);AiResponse aiResponse = AiResponse.success(content, "deepseek-chat");aiResponse.setProcessingTimeMs(processingTime);return aiResponse;} catch (Exception e) {logger.error("聊天对话失败", e);return AiResponse.error("聊天对话失败:" + e.getMessage());}}/*** 流式聊天* * @param message 用户消息* @return 流式响应*/public Flux<String> streamChat(String message) {logger.info("开始流式聊天,消息:{}", message);try {String systemPrompt = """你是一位友好、有帮助的AI助手。请以自然、亲切的方式与用户对话,用中文回复。""";PromptTemplate promptTemplate = new PromptTemplate(systemPrompt + "\n\n用户:{content}");Prompt prompt = promptTemplate.create(Map.of("content", message));DeepSeekChatOptions options = DeepSeekChatOptions.builder().temperature(0.9).maxTokens(800).build();return chatModel.stream(new Prompt(prompt.getInstructions(), options)).map(response -> response.getResult().getOutput().getText()).doOnNext(chunk -> logger.debug("流式响应块:{}", chunk)).doOnComplete(() -> logger.info("流式聊天完成")).doOnError(error -> logger.error("流式聊天失败", error));} catch (Exception e) {logger.error("流式聊天启动失败", e);return Flux.error(e);}}
}
(2)Web 控制器实现(AiController.java
)
package com.example.demo.controller;import com.example.demo.dto.AiRequest;
import com.example.demo.dto.AiResponse;
import com.example.demo.service.SmartGeneratorService;
import jakarta.validation.Valid;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;/*** AI功能控制器* 提供营销文案生成、代码生成、智能问答、聊天对话等API* * @author Spring AI Demo*/
@RestController
@RequestMapping("/api/ai")
@CrossOrigin(origins = "*")
public class AiController {private static final Logger logger = LoggerFactory.getLogger(AiController.class);private final SmartGeneratorService smartGeneratorService;public AiController(SmartGeneratorService smartGeneratorService) {this.smartGeneratorService = smartGeneratorService;}/*** 营销文案生成API* * @param request 请求参数* @return 生成的营销文案*/@PostMapping("/marketing")public ResponseEntity<AiResponse> generateMarketingContent(@Valid @RequestBody AiRequest request) {logger.info("收到营销文案生成请求:{}", request.getContent());try {AiResponse response = smartGeneratorService.generateMarketingContent(request);return ResponseEntity.ok(response);} catch (Exception e) {logger.error("营销文案生成API调用失败", e);return ResponseEntity.internalServerError().body(AiResponse.error("服务器内部错误:" + e.getMessage()));}}/*** 代码生成API* * @param request 请求参数* @return 生成的代码*/@PostMapping("/code")public ResponseEntity<AiResponse> generateCode(@Valid @RequestBody AiRequest request) {logger.info("收到代码生成请求:{}", request.getContent());try {AiResponse response = smartGeneratorService.generateCode(request);return ResponseEntity.ok(response);} catch (Exception e) {logger.error("代码生成API调用失败", e);return ResponseEntity.internalServerError().body(AiResponse.error("服务器内部错误:" + e.getMessage()));}}/*** 智能问答API* * @param request 请求参数* @return 问题的答案*/@PostMapping("/qa")public ResponseEntity<AiResponse> answerQuestion(@Valid @RequestBody AiRequest request) {logger.info("收到智能问答请求:{}", request.getContent());try {AiResponse response = smartGeneratorService.answerQuestion(request);return ResponseEntity.ok(response);} catch (Exception e) {logger.error("智能问答API调用失败", e);return ResponseEntity.internalServerError().body(AiResponse.error("服务器内部错误:" + e.getMessage()));}}/*** 聊天对话API* * @param request 请求参数* @return 聊天回复*/@PostMapping("/chat")public ResponseEntity<AiResponse> chat(@Valid @RequestBody AiRequest request) {logger.info("收到聊天对话请求:{}", request.getContent());try {AiResponse response = smartGeneratorService.chat(request);return ResponseEntity.ok(response);} catch (Exception e) {logger.error("聊天对话API调用失败", e);return ResponseEntity.internalServerError().body(AiResponse.error("服务器内部错误:" + e.getMessage()));}}/*** 简单文本生成API(GET方式,用于快速测试)* * @param message 用户消息* @param temperature 温度参数(可选)* @return 生成的回复*/@GetMapping("/simple")public ResponseEntity<AiResponse> simpleChat(@RequestParam String message,@RequestParam(required = false) Double temperature) {logger.info("收到简单聊天请求:{}", message);try {AiRequest request = new AiRequest(message, temperature);AiResponse response = smartGeneratorService.chat(request);return ResponseEntity.ok(response);} catch (Exception e) {logger.error("简单聊天API调用失败", e);return ResponseEntity.internalServerError().body(AiResponse.error("服务器内部错误:" + e.getMessage()));}}/*** 健康检查API* * @return 服务状态*/@GetMapping("/health")public ResponseEntity<String> health() {return ResponseEntity.ok("AI服务运行正常 ✅");}/*** 获取支持的功能列表* * @return 功能列表*/@GetMapping("/features")public ResponseEntity<Object> getFeatures() {var features = new Object() {public final String[] supportedFeatures = {"营销文案生成 (POST /api/ai/marketing)","代码生成 (POST /api/ai/code)", "智能问答 (POST /api/ai/qa)","聊天对话 (POST /api/ai/chat)","简单对话 (GET /api/ai/simple?message=你好)","流式聊天 (GET /api/stream/chat?message=你好)"};public final String model = "deepseek-chat";public final String version = "1.0.0";public final String description = "Spring AI + DeepSeek 智能文本生成服务";};return ResponseEntity.ok(features);}
}
(3)流式响应处理(StreamController.java
)
package com.example.demo.controller;import com.example.demo.service.SmartGeneratorService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;import java.time.Duration;
import java.util.HashMap;
import java.util.Map;/*** 流式响应控制器* 提供Server-Sent Events (SSE) 流式聊天功能* * @author Spring AI Demo*/
@RestController
@RequestMapping("/api/stream")
@CrossOrigin(origins = "*")
public class StreamController {private static final Logger logger = LoggerFactory.getLogger(StreamController.class);private final SmartGeneratorService smartGeneratorService;public StreamController(SmartGeneratorService smartGeneratorService) {this.smartGeneratorService = smartGeneratorService;}/*** 流式聊天API* 使用Server-Sent Events (SSE) 实现实时流式响应* * @param message 用户消息* @return 流式响应*/@GetMapping(value = "/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE)public Flux<String> streamChat(@RequestParam String message) {logger.info("收到流式聊天请求:{}", message);return smartGeneratorService.streamChat(message).filter(chunk -> chunk != null && !chunk.trim().isEmpty()) // 过滤空内容.doOnNext(chunk -> logger.debug("原始数据块: '{}'", chunk)).map(chunk -> chunk.trim()) // 只清理空白字符.filter(chunk -> !chunk.isEmpty()) // 再次过滤空内容.concatWith(Flux.just("[DONE]")).doOnSubscribe(subscription -> logger.info("开始流式响应")).doOnComplete(() -> logger.info("流式响应完成")).doOnError(error -> logger.error("流式响应出错", error)).onErrorReturn("[ERROR] 流式响应出现错误");}/*** 流式聊天API(JSON格式)* 返回JSON格式的流式数据* * @param message 用户消息* @return JSON格式的流式响应*/@GetMapping(value = "/chat-json", produces = MediaType.APPLICATION_NDJSON_VALUE)public Flux<Map<String, Object>> streamChatJson(@RequestParam String message) {logger.info("收到JSON流式聊天请求:{}", message);// 创建完成响应Map<String, Object> doneResponse = new HashMap<>();doneResponse.put("type", "done");doneResponse.put("content", "");doneResponse.put("timestamp", System.currentTimeMillis());// 创建错误响应Map<String, Object> errorResponse = new HashMap<>();errorResponse.put("type", "error");errorResponse.put("content", "流式响应出现错误");errorResponse.put("timestamp", System.currentTimeMillis());return smartGeneratorService.streamChat(message).map(chunk -> {Map<String, Object> response = new HashMap<>();response.put("type", "chunk");response.put("content", chunk);response.put("timestamp", System.currentTimeMillis());return response;}).concatWith(Flux.just(doneResponse)).doOnSubscribe(subscription -> logger.info("开始JSON流式响应")).doOnComplete(() -> logger.info("JSON流式响应完成")).doOnError(error -> logger.error("JSON流式响应出错", error)).onErrorReturn(errorResponse);}/*** 模拟打字机效果的流式响应* * @param message 用户消息* @return 带延迟的流式响应*/@GetMapping(value = "/typewriter", produces = MediaType.TEXT_EVENT_STREAM_VALUE)public Flux<String> typewriterChat(@RequestParam String message) {logger.info("收到打字机效果聊天请求:{}", message);return smartGeneratorService.streamChat(message).delayElements(Duration.ofMillis(50)) // 添加50ms延迟模拟打字机效果.map(chunk -> "data: " + chunk + "\n\n").concatWith(Flux.just("data: [DONE]\n\n")).doOnSubscribe(subscription -> logger.info("开始打字机效果流式响应")).doOnComplete(() -> logger.info("打字机效果流式响应完成")).doOnError(error -> logger.error("打字机效果流式响应出错", error)).onErrorReturn("data: [ERROR] 流式响应出现错误\n\n");}/*** 流式响应健康检查* * @return 测试流式响应*/@GetMapping(value = "/health", produces = MediaType.TEXT_EVENT_STREAM_VALUE)public Flux<String> streamHealth() {return Flux.interval(Duration.ofSeconds(1)).take(5).map(i -> "data: 流式服务正常运行 - " + (i + 1) + "/5\n\n").concatWith(Flux.just("data: [DONE] 健康检查完成\n\n")).doOnSubscribe(subscription -> logger.info("开始流式健康检查")).doOnComplete(() -> logger.info("流式健康检查完成"));}/*** 测试用的简单流式聊天(修复版本)* * @param message 用户消息* @return 流式响应*/@GetMapping(value = "/chat-fixed", produces = MediaType.TEXT_EVENT_STREAM_VALUE)public Flux<String> streamChatFixed(@RequestParam String message) {logger.info("收到修复版流式聊天请求:{}", message);return smartGeneratorService.streamChat(message).filter(chunk -> chunk != null && !chunk.trim().isEmpty()).doOnNext(chunk -> logger.debug("修复版数据块: '{}'", chunk)).map(chunk -> chunk.trim()).filter(chunk -> !chunk.isEmpty()).concatWith(Flux.just("[DONE]")).doOnSubscribe(subscription -> logger.info("开始修复版流式响应")).doOnComplete(() -> logger.info("修复版流式响应完成")).doOnError(error -> logger.error("修复版流式响应出错", error)).onErrorReturn("[ERROR] 修复版流式响应出现错误");}/*** 获取流式API使用说明* * @return 使用说明*/@GetMapping("/info")public Map<String, Object> getStreamInfo() {Map<String, Object> info = new HashMap<>();info.put("description", "Spring AI DeepSeek 流式响应服务");info.put("endpoints", new String[]{"GET /api/stream/chat?message=你好 - 基础流式聊天","GET /api/stream/chat-fixed?message=你好 - 修复版流式聊天","GET /api/stream/chat-json?message=你好 - JSON格式流式聊天","GET /api/stream/typewriter?message=你好 - 打字机效果流式聊天","GET /api/stream/health - 流式服务健康检查"});info.put("usage", "使用curl测试: curl -N 'http://localhost:8080/api/stream/chat-fixed?message=你好'");info.put("browser", "浏览器访问: http://localhost:8080/api/stream/chat-fixed?message=你好");info.put("contentType", "text/event-stream");return info;}
}
(4)主页控制器(HomeController.java
)
package com.example.demo.controller;import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;/*** 主页控制器* 处理根路径访问和页面跳转* * @author Spring AI Demo*/
@Controller
public class HomeController {/*** 根路径重定向到主页* * @return 重定向到index.html*/@GetMapping("/")public String home() {return "redirect:/index.html";}/*** 主页访问* * @return index页面*/@GetMapping("/index")public String index() {return "redirect:/index.html";}/*** 演示页面访问* * @return index页面*/@GetMapping("/demo")public String demo() {return "redirect:/index.html";}
}
(5)自定义错误处理控制器(CustomErrorController.java
)
package com.example.demo.controller;import org.springframework.boot.web.servlet.error.ErrorController;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;import jakarta.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;/*** 自定义错误处理控制器* 提供友好的错误页面和API错误响应* * @author Spring AI Demo*/
@Controller
public class CustomErrorController implements ErrorController {/*** 处理错误请求* * @param request HTTP请求* @return 错误响应*/@RequestMapping("/error")@ResponseBodypublic Map<String, Object> handleError(HttpServletRequest request) {Map<String, Object> errorResponse = new HashMap<>();// 获取错误状态码Integer statusCode = (Integer) request.getAttribute("jakarta.servlet.error.status_code");String requestUri = (String) request.getAttribute("jakarta.servlet.error.request_uri");if (statusCode == null) {statusCode = HttpStatus.INTERNAL_SERVER_ERROR.value();}errorResponse.put("status", statusCode);errorResponse.put("error", getErrorMessage(statusCode));errorResponse.put("path", requestUri);errorResponse.put("timestamp", System.currentTimeMillis());// 根据错误类型提供帮助信息switch (statusCode) {case 404:errorResponse.put("message", "页面未找到");errorResponse.put("suggestions", new String[]{"访问主页: http://localhost:8080","查看API文档: http://localhost:8080/api/ai/features","健康检查: http://localhost:8080/actuator/health"});break;case 500:errorResponse.put("message", "服务器内部错误");errorResponse.put("suggestions", new String[]{"检查应用日志","确认API密钥配置正确","重启应用服务"});break;default:errorResponse.put("message", "请求处理失败");errorResponse.put("suggestions", new String[]{"检查请求格式","查看API文档","联系技术支持"});}return errorResponse;}/*** 根据状态码获取错误消息* * @param statusCode HTTP状态码* @return 错误消息*/private String getErrorMessage(int statusCode) {switch (statusCode) {case 400:return "Bad Request";case 401:return "Unauthorized";case 403:return "Forbidden";case 404:return "Not Found";case 500:return "Internal Server Error";case 502:return "Bad Gateway";case 503:return "Service Unavailable";default:return "Unknown Error";}}
}
(6)Web配置类(WebConfig.java
)
package com.example.demo.config;import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;/*** Web配置类* 配置静态资源处理* * @author Spring AI Demo*/
@Configuration
public class WebConfig implements WebMvcConfigurer {/*** 配置静态资源处理器* * @param registry 资源处理器注册表*/@Overridepublic void addResourceHandlers(ResourceHandlerRegistry registry) {// 配置静态资源路径registry.addResourceHandler("/**").addResourceLocations("classpath:/static/").setCachePeriod(3600); // 缓存1小时// 确保index.html可以被访问registry.addResourceHandler("/index.html").addResourceLocations("classpath:/static/index.html").setCachePeriod(0); // 不缓存主页}
}
(7)AI服务请求DTO(AiRequest.java
)
package com.example.demo.dto;import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import jakarta.validation.constraints.DecimalMax;
import jakarta.validation.constraints.DecimalMin;/*** AI服务请求DTO* * @author Spring AI Demo*/
public class AiRequest {/*** 用户输入内容*/@NotBlank(message = "输入内容不能为空")@Size(max = 2000, message = "输入内容不能超过2000个字符")private String content;/*** 温度参数(可选)* 控制生成文本的随机性,0.0表示确定性,1.0表示最大随机性*/@DecimalMin(value = "0.0", message = "温度参数不能小于0.0")@DecimalMax(value = "2.0", message = "温度参数不能大于2.0")private Double temperature;/*** 最大生成Token数(可选)*/private Integer maxTokens;/*** 系统提示词(可选)*/private String systemPrompt;// 构造函数public AiRequest() {}public AiRequest(String content) {this.content = content;}public AiRequest(String content, Double temperature) {this.content = content;this.temperature = temperature;}// Getter和Setter方法public String getContent() {return content;}public void setContent(String content) {this.content = content;}public Double getTemperature() {return temperature;}public void setTemperature(Double temperature) {this.temperature = temperature;}public Integer getMaxTokens() {return maxTokens;}public void setMaxTokens(Integer maxTokens) {this.maxTokens = maxTokens;}public String getSystemPrompt() {return systemPrompt;}public void setSystemPrompt(String systemPrompt) {this.systemPrompt = systemPrompt;}@Overridepublic String toString() {return "AiRequest{" +"content='" + content + '\'' +", temperature=" + temperature +", maxTokens=" + maxTokens +", systemPrompt='" + systemPrompt + '\'' +'}';}
}
(8)AI服务响应DTO(AiResponse.java
)
package com.example.demo.dto;import java.time.LocalDateTime;/*** AI服务响应DTO* * @author Spring AI Demo*/
public class AiResponse {/*** 生成的内容*/private String content;/*** 请求是否成功*/private boolean success;/*** 错误信息(如果有)*/private String errorMessage;/*** 响应时间戳*/private LocalDateTime timestamp;/*** 使用的模型名称*/private String model;/*** 消耗的Token数量*/private Integer tokensUsed;/*** 处理耗时(毫秒)*/private Long processingTimeMs;// 构造函数public AiResponse() {this.timestamp = LocalDateTime.now();}public AiResponse(String content) {this();this.content = content;this.success = true;}public AiResponse(String content, String model) {this(content);this.model = model;}// 静态工厂方法public static AiResponse success(String content) {return new AiResponse(content);}public static AiResponse success(String content, String model) {return new AiResponse(content, model);}public static AiResponse error(String errorMessage) {AiResponse response = new AiResponse();response.success = false;response.errorMessage = errorMessage;return response;}// Getter和Setter方法public String getContent() {return content;}public void setContent(String content) {this.content = content;}public boolean isSuccess() {return success;}public void setSuccess(boolean success) {this.success = success;}public String getErrorMessage() {return errorMessage;}public void setErrorMessage(String errorMessage) {this.errorMessage = errorMessage;}public LocalDateTime getTimestamp() {return timestamp;}public void setTimestamp(LocalDateTime timestamp) {this.timestamp = timestamp;}public String getModel() {return model;}public void setModel(String model) {this.model = model;}public Integer getTokensUsed() {return tokensUsed;}public void setTokensUsed(Integer tokensUsed) {this.tokensUsed = tokensUsed;}public Long getProcessingTimeMs() {return processingTimeMs;}public void setProcessingTimeMs(Long processingTimeMs) {this.processingTimeMs = processingTimeMs;}@Overridepublic String toString() {return "AiResponse{" +"content='" + content + '\'' +", success=" + success +", errorMessage='" + errorMessage + '\'' +", timestamp=" + timestamp +", model='" + model + '\'' +", tokensUsed=" + tokensUsed +", processingTimeMs=" + processingTimeMs +'}';}
}
(5)Spring Boot与Spring AI集成DeepSeek的主应用类(DeepSeekApplication.java
)
package com.example.demo;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;/*** Spring Boot与Spring AI集成DeepSeek的主应用类* * @author Spring AI Demo* @version 1.0.0*/
@SpringBootApplication
public class DeepSeekApplication {public static void main(String[] args) {SpringApplication.run(DeepSeekApplication.class, args);}/*** 应用启动完成后的事件处理*/@EventListener(ApplicationReadyEvent.class)public void onApplicationReady() {System.out.println("\n" +"=================================================================\n" +"🚀 Spring AI DeepSeek 演示应用启动成功!\n" +"=================================================================\n" +"📖 API文档地址:\n" +" • 测试页面:POST http://localhost:8080\n" +" • 营销文案生成:POST http://localhost:8080/api/ai/marketing\n" +" • 代码生成: POST http://localhost:8080/api/ai/code\n" +" • 智能问答: POST http://localhost:8080/api/ai/qa\n" +" • 聊天对话: POST http://localhost:8080/api/ai/chat\n" +" • 流式聊天: GET http://localhost:8080/api/stream/chat?message=你好\n" +"=================================================================\n" +"💡 使用提示:\n" +" 1. 请确保在application.yml中配置了有效的DeepSeek API密钥\n" +" 2. 或者设置环境变量:DEEPSEEK_API_KEY=your-api-key\n" +" 3. 访问 http://localhost:8080/actuator/health 检查应用健康状态\n" +"=================================================================\n");}
}
(5)前段展示页面(index.html
)
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Spring AI DeepSeek 演示</title><style>* {margin: 0;padding: 0;box-sizing: border-box;}body {font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);min-height: 100vh;padding: 20px;}.container {max-width: 1200px;margin: 0 auto;background: white;border-radius: 15px;box-shadow: 0 20px 40px rgba(0,0,0,0.1);overflow: hidden;}.header {background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);color: white;padding: 30px;text-align: center;}.header h1 {font-size: 2.5em;margin-bottom: 10px;}.header p {font-size: 1.2em;opacity: 0.9;}.main-content {padding: 30px;}.api-section {padding: 0;}.api-title {font-size: 1.5em;color: #333;margin-bottom: 15px;display: flex;align-items: center;}.api-title::before {content: "🚀";margin-right: 10px;font-size: 1.2em;}.stream-section {background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);color: white;padding: 30px;border-radius: 15px;box-shadow: 0 10px 30px rgba(102, 126, 234, 0.3);}.stream-section .api-title {color: white;font-size: 1.8em;margin-bottom: 20px;}.stream-section .api-title::before {content: "🌊";}.input-group {margin-bottom: 20px;}.input-group label {display: block;margin-bottom: 8px;font-weight: 600;color: #555;}.stream-section .input-group label {color: white;}.input-group textarea,.input-group input {width: 100%;padding: 12px;border: 2px solid #e0e0e0;border-radius: 8px;font-size: 14px;transition: border-color 0.3s ease;}.input-group textarea:focus,.input-group input:focus {outline: none;border-color: #4facfe;}.input-group textarea {min-height: 100px;resize: vertical;}.btn {background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);color: white;border: none;padding: 12px 25px;border-radius: 8px;cursor: pointer;font-size: 16px;font-weight: 600;transition: all 0.3s ease;margin-right: 10px;margin-bottom: 10px;}.btn:hover {transform: translateY(-2px);box-shadow: 0 5px 15px rgba(79, 172, 254, 0.3);}.btn:disabled {opacity: 0.6;cursor: not-allowed;transform: none;}.btn-danger {background: linear-gradient(135deg, #ff6b6b 0%, #ee5a52 100%);}.btn-success {background: linear-gradient(135deg, #51cf66 0%, #40c057 100%);}.response-area {margin-top: 20px;padding: 20px;background: #f8f9fa;border-radius: 8px;border-left: 4px solid #4facfe;min-height: 100px;white-space: pre-wrap;font-family: 'Courier New', monospace;font-size: 14px;line-height: 1.5;}.loading {display: none;text-align: center;padding: 20px;color: #666;}.loading::after {content: "";display: inline-block;width: 20px;height: 20px;border: 3px solid #f3f3f3;border-top: 3px solid #4facfe;border-radius: 50%;animation: spin 1s linear infinite;margin-left: 10px;}@keyframes spin {0% { transform: rotate(0deg); }100% { transform: rotate(360deg); }}.stream-output {background: #1a202c;color: #e2e8f0;padding: 25px;border-radius: 12px;min-height: 300px;font-family: 'Courier New', monospace;font-size: 15px;line-height: 1.8;overflow-y: auto;max-height: 500px;border: 2px solid rgba(255,255,255,0.1);position: relative;}.stream-output::-webkit-scrollbar {width: 8px;}.stream-output::-webkit-scrollbar-track {background: #2d3748;border-radius: 4px;}.stream-output::-webkit-scrollbar-thumb {background: #4a5568;border-radius: 4px;}.stream-output::-webkit-scrollbar-thumb:hover {background: #718096;}.stream-status {position: absolute;top: 10px;right: 15px;padding: 5px 10px;background: rgba(0,0,0,0.3);border-radius: 15px;font-size: 12px;color: #a0aec0;}.stream-status.connecting {color: #fbb6ce;}.stream-status.streaming {color: #9ae6b4;animation: pulse 2s infinite;}.stream-status.completed {color: #90cdf4;}.stream-status.error {color: #feb2b2;}@keyframes pulse {0%, 100% { opacity: 1; }50% { opacity: 0.5; }}.stream-controls {display: flex;gap: 10px;flex-wrap: wrap;margin-top: 15px;}.footer {background: #f8f9fa;padding: 20px;text-align: center;color: #666;border-top: 1px solid #e0e0e0;}.tab-container {background: white;border-radius: 15px;overflow: hidden;box-shadow: 0 5px 15px rgba(0,0,0,0.1);}.tab-nav {display: flex;background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);border-bottom: 2px solid #e0e0e0;overflow-x: auto;}.tab-btn {flex: 1;min-width: 150px;padding: 15px 20px;border: none;background: transparent;color: #666;font-size: 14px;font-weight: 600;cursor: pointer;transition: all 0.3s ease;border-bottom: 3px solid transparent;white-space: nowrap;}.tab-btn:hover {background: rgba(79, 172, 254, 0.1);color: #4facfe;}.tab-btn.active {background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);color: white;border-bottom-color: #0066cc;}.tab-content {display: none;padding: 30px;min-height: 500px;}.tab-content.active {display: block;}.typing-indicator {display: inline-block;color: #9ae6b4;}.typing-indicator::after {content: '|';animation: blink 1s infinite;}@keyframes blink {0%, 50% { opacity: 1; }51%, 100% { opacity: 0; }}.stream-message {margin-bottom: 15px;padding: 10px 0;border-bottom: 1px solid rgba(255,255,255,0.1);}.stream-message:last-child {border-bottom: none;}.message-timestamp {color: #a0aec0;font-size: 12px;margin-bottom: 5px;}.message-content {color: #e2e8f0;line-height: 1.6;}</style>
</head>
<body><div class="container"><div class="header"><h1>🤖 Spring AI DeepSeek 演示</h1><p>智能文本生成系统 - 营销文案、代码生成、智能问答、聊天对话</p></div><div class="main-content"><!-- Tab导航 --><div class="tab-container"><div class="tab-nav"><button class="tab-btn active" onclick="switchTab('stream')">🌊 实时流式聊天</button><button class="tab-btn" onclick="switchTab('marketing')">📝 营销文案生成</button><button class="tab-btn" onclick="switchTab('code')">💻 代码生成</button><button class="tab-btn" onclick="switchTab('qa')">❓ 智能问答</button><button class="tab-btn" onclick="switchTab('chat')">💬 聊天对话</button></div><!-- 实时流式聊天演示 --><div id="stream-tab" class="tab-content active"><div class="stream-section"><div class="api-title">实时流式聊天演示</div><p style="margin-bottom: 20px; opacity: 0.9;">体验AI实时生成文本的魅力,支持打字机效果和流式响应</p><div class="input-group"><label for="stream-input">💬 输入您的消息:</label><textarea id="stream-input" placeholder="例如:讲一个有趣的科幻故事,或者解释一下量子计算的原理" style="background: rgba(255,255,255,0.95); color: #333;"></textarea></div><div class="stream-controls"><button class="btn btn-success" onclick="startStream()">🚀 开始流式对话</button><button class="btn" onclick="pauseStream()" id="pauseBtn" disabled>⏸️ 暂停</button><button class="btn btn-danger" onclick="stopStream()">⏹️ 停止</button><button class="btn" onclick="clearStream()">🗑️ 清空</button><button class="btn" onclick="saveStream()">💾 保存对话</button><button class="btn" onclick="testStreamEndpoint()" style="background: #ffa726;">🔧 测试端点</button></div><div class="stream-output" id="stream-output"><div class="stream-status" id="stream-status">等待开始...</div><div id="stream-content"><div class="message-content">🌟 欢迎使用流式聊天演示!<br><br>✨ 特色功能:<br>• 实时流式响应,逐字显示<br>• 支持暂停/继续/停止控制<br>• 自动滚动到最新内容<br>• 对话历史保存<br><br>💡 请在上方输入框中输入您的问题,然后点击"开始流式对话"按钮开始体验!</div></div></div></div></div><!-- 营销文案生成 --><div id="marketing-tab" class="tab-content"><div class="api-section"><div class="api-title">营销文案生成</div><div class="input-group"><label for="marketing-input">产品描述或需求:</label><textarea id="marketing-input" placeholder="例如:为智能手表的心率监测功能生成营销文案"></textarea></div><div class="input-group"><label for="marketing-temp">创意度 (0.0-2.0):</label><input type="number" id="marketing-temp" value="1.2" min="0" max="2" step="0.1"></div><button class="btn" onclick="generateMarketing()">生成营销文案</button><div class="loading" id="marketing-loading">生成中...</div><div class="response-area" id="marketing-response">点击按钮开始生成营销文案...</div></div></div><!-- 代码生成 --><div id="code-tab" class="tab-content"><div class="api-section"><div class="api-title">代码生成</div><div class="input-group"><label for="code-input">编程需求:</label><textarea id="code-input" placeholder="例如:用Java实现一个简单的计算器类"></textarea></div><div class="input-group"><label for="code-temp">精确度 (0.0-1.0):</label><input type="number" id="code-temp" value="0.1" min="0" max="1" step="0.1"></div><button class="btn" onclick="generateCode()">生成代码</button><div class="loading" id="code-loading">生成中...</div><div class="response-area" id="code-response">点击按钮开始生成代码...</div></div></div><!-- 智能问答 --><div id="qa-tab" class="tab-content"><div class="api-section"><div class="api-title">智能问答</div><div class="input-group"><label for="qa-input">您的问题:</label><textarea id="qa-input" placeholder="例如:什么是Spring Boot的自动配置原理?"></textarea></div><button class="btn" onclick="answerQuestion()">获取答案</button><div class="loading" id="qa-loading">思考中...</div><div class="response-area" id="qa-response">输入问题获取智能回答...</div></div></div><!-- 聊天对话 --><div id="chat-tab" class="tab-content"><div class="api-section"><div class="api-title">聊天对话</div><div class="input-group"><label for="chat-input">聊天消息:</label><textarea id="chat-input" placeholder="例如:你好,今天天气怎么样?"></textarea></div><button class="btn" onclick="chat()">发送消息</button><div class="loading" id="chat-loading">回复中...</div><div class="response-area" id="chat-response">开始与AI聊天...</div></div></div></div></div><div class="footer"><p>🚀 Spring AI + DeepSeek 智能文本生成演示 | 版本 1.0.1</p><p>💡 提示:请确保已配置有效的DeepSeek API密钥</p></div></div><script>// 全局变量let currentEventSource = null;let isPaused = false;let streamBuffer = '';let conversationHistory = [];// Tab切换功能function switchTab(tabName) {// 隐藏所有tab内容const allTabs = document.querySelectorAll('.tab-content');allTabs.forEach(tab => tab.classList.remove('active'));// 移除所有tab按钮的active状态const allBtns = document.querySelectorAll('.tab-btn');allBtns.forEach(btn => btn.classList.remove('active'));// 显示选中的tab内容document.getElementById(tabName + '-tab').classList.add('active');// 激活对应的tab按钮event.target.classList.add('active');console.log(`切换到 ${tabName} 标签页`);}// 通用API调用函数async function callAPI(endpoint, data, loadingId, responseId) {const loading = document.getElementById(loadingId);const response = document.getElementById(responseId);loading.style.display = 'block';response.textContent = '处理中...';try {const result = await fetch(endpoint, {method: 'POST',headers: {'Content-Type': 'application/json',},body: JSON.stringify(data)});const jsonResponse = await result.json();if (jsonResponse.success) {response.textContent = jsonResponse.content;} else {response.textContent = `错误: ${jsonResponse.errorMessage || '请求失败'}`;}} catch (error) {response.textContent = `网络错误: ${error.message}`;} finally {loading.style.display = 'none';}}// 营销文案生成function generateMarketing() {const content = document.getElementById('marketing-input').value;const temperature = parseFloat(document.getElementById('marketing-temp').value);if (!content.trim()) {alert('请输入产品描述或需求');return;}callAPI('/api/ai/marketing', {content: content,temperature: temperature,maxTokens: 800}, 'marketing-loading', 'marketing-response');}// 代码生成function generateCode() {const content = document.getElementById('code-input').value;const temperature = parseFloat(document.getElementById('code-temp').value);if (!content.trim()) {alert('请输入编程需求');return;}callAPI('/api/ai/code', {content: content,temperature: temperature,maxTokens: 1500}, 'code-loading', 'code-response');}// 智能问答function answerQuestion() {const content = document.getElementById('qa-input').value;if (!content.trim()) {alert('请输入您的问题');return;}callAPI('/api/ai/qa', {content: content,temperature: 0.7,maxTokens: 1000}, 'qa-loading', 'qa-response');}// 聊天对话function chat() {const content = document.getElementById('chat-input').value;if (!content.trim()) {alert('请输入聊天消息');return;}callAPI('/api/ai/chat', {content: content,temperature: 0.9,maxTokens: 800}, 'chat-loading', 'chat-response');}// 更新流式状态function updateStreamStatus(status, message) {const statusElement = document.getElementById('stream-status');statusElement.className = `stream-status ${status}`;statusElement.textContent = message;}// 添加消息到流式输出function addStreamMessage(content, isUser = false) {const streamContent = document.getElementById('stream-content');const timestamp = new Date().toLocaleTimeString();const messageDiv = document.createElement('div');messageDiv.className = 'stream-message';messageDiv.innerHTML = `<div class="message-timestamp">${timestamp} ${isUser ? '👤 您' : '🤖 AI'}</div><div class="message-content">${content}</div>`;streamContent.appendChild(messageDiv);// 滚动到底部const output = document.getElementById('stream-output');output.scrollTop = output.scrollHeight;}// 流式聊天function startStream() {const message = document.getElementById('stream-input').value;if (!message.trim()) {alert('请输入流式消息');return;}// 停止之前的连接if (currentEventSource) {currentEventSource.close();}// 添加用户消息addStreamMessage(message, true);// 清空输入框document.getElementById('stream-input').value = '';// 重置状态isPaused = false;streamBuffer = '';// 更新状态和按钮updateStreamStatus('connecting', '连接中...');document.querySelector('button[onclick="startStream()"]').disabled = true;document.getElementById('pauseBtn').disabled = false;// 创建新的EventSource连接const encodedMessage = encodeURIComponent(message);const streamUrl = `/api/stream/chat-fixed?message=${encodedMessage}`;console.log('连接流式端点:', streamUrl);currentEventSource = new EventSource(streamUrl);// 添加AI响应容器const aiMessageDiv = document.createElement('div');aiMessageDiv.className = 'stream-message';aiMessageDiv.innerHTML = `<div class="message-timestamp">${new Date().toLocaleTimeString()} 🤖 AI</div><div class="message-content"><span class="typing-indicator"></span></div>`;document.getElementById('stream-content').appendChild(aiMessageDiv);const aiContentDiv = aiMessageDiv.querySelector('.message-content');currentEventSource.onopen = function() {console.log('SSE连接已建立');updateStreamStatus('streaming', '正在接收...');};currentEventSource.onmessage = function(event) {if (isPaused) return;console.log('收到SSE数据:', event.data);// 检查是否是完成信号if (event.data === '[DONE]') {console.log('流式响应完成');updateStreamStatus('completed', '完成');// 移除打字指示器const typingIndicator = aiContentDiv.querySelector('.typing-indicator');if (typingIndicator) {typingIndicator.remove();}// 保存到历史记录conversationHistory.push({user: message,ai: streamBuffer,timestamp: new Date().toISOString()});// 清理连接currentEventSource.close();currentEventSource = null;document.querySelector('button[onclick="startStream()"]').disabled = false;document.getElementById('pauseBtn').disabled = true;return;}// 检查是否是错误信号if (event.data.startsWith('[ERROR]')) {console.log('流式响应错误:', event.data);updateStreamStatus('error', '错误');const errorMsg = event.data.replace('[ERROR]', '').trim();aiContentDiv.innerHTML = `❌ ${errorMsg || '流式响应出现错误'}`;// 清理连接currentEventSource.close();currentEventSource = null;document.querySelector('button[onclick="startStream()"]').disabled = false;document.getElementById('pauseBtn').disabled = true;return;}// 处理正常的流式数据if (event.data && event.data.trim() !== '') {console.log('处理流式数据块:', event.data);// 累积响应内容streamBuffer += event.data;// 移除打字指示器并更新内容const typingIndicator = aiContentDiv.querySelector('.typing-indicator');if (typingIndicator) {typingIndicator.remove();}// 转义HTML内容并保持换行const escapedContent = streamBuffer.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''').replace(/\n/g, '<br>');aiContentDiv.innerHTML = escapedContent + '<span class="typing-indicator"></span>';// 滚动到底部const output = document.getElementById('stream-output');output.scrollTop = output.scrollHeight;}};currentEventSource.onerror = function(event) {console.error('SSE连接错误:', event);// 如果连接已经被正常关闭,不处理错误if (!currentEventSource) {console.log('连接已正常关闭,忽略错误事件');return;}console.log('连接状态:', currentEventSource.readyState);updateStreamStatus('error', '连接错误');// 检查连接状态if (currentEventSource.readyState === EventSource.CONNECTING) {aiContentDiv.innerHTML = '❌ 正在重新连接...';} else if (currentEventSource.readyState === EventSource.CLOSED) {aiContentDiv.innerHTML = '❌ 连接已关闭,请检查网络或API配置';} else {aiContentDiv.innerHTML = '❌ 连接错误,请检查服务器状态';}// 清理连接if (currentEventSource) {currentEventSource.close();currentEventSource = null;}// 重置按钮状态document.querySelector('button[onclick="startStream()"]').disabled = false;document.getElementById('pauseBtn').disabled = true;};}// 暂停/继续流式响应function pauseStream() {const pauseBtn = document.getElementById('pauseBtn');if (isPaused) {isPaused = false;pauseBtn.textContent = '⏸️ 暂停';updateStreamStatus('streaming', '继续接收...');} else {isPaused = true;pauseBtn.textContent = '▶️ 继续';updateStreamStatus('paused', '已暂停');}}// 停止流式响应function stopStream() {if (currentEventSource) {currentEventSource.close();currentEventSource = null;}updateStreamStatus('completed', '已停止');document.querySelector('button[onclick="startStream()"]').disabled = false;document.getElementById('pauseBtn').disabled = true;isPaused = false;document.getElementById('pauseBtn').textContent = '⏸️ 暂停';}// 清空流式输出function clearStream() {document.getElementById('stream-content').innerHTML = `<div class="message-content">🌟 欢迎使用流式聊天演示!<br><br>✨ 特色功能:<br>• 实时流式响应,逐字显示<br>• 支持暂停/继续/停止控制<br>• 自动滚动到最新内容<br>• 对话历史保存<br><br>💡 请在上方输入框中输入您的问题,然后点击"开始流式对话"按钮开始体验!</div>`;updateStreamStatus('ready', '等待开始...');streamBuffer = '';}// 保存对话历史function saveStream() {if (conversationHistory.length === 0) {alert('暂无对话历史可保存');return;}const content = conversationHistory.map(item => `时间: ${new Date(item.timestamp).toLocaleString()}\n用户: ${item.user}\nAI: ${item.ai}\n${'='.repeat(50)}\n`).join('\n');const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });const url = URL.createObjectURL(blob);const a = document.createElement('a');a.href = url;a.download = `AI对话历史_${new Date().toISOString().slice(0,10)}.txt`;document.body.appendChild(a);a.click();document.body.removeChild(a);URL.revokeObjectURL(url);alert('对话历史已保存到文件');}// 测试流式端点async function testStreamEndpoint() {updateStreamStatus('connecting', '测试中...');try {// 测试基础健康检查console.log('测试基础健康检查...');const healthResponse = await fetch('/api/ai/health');const healthText = await healthResponse.text();console.log('健康检查结果:', healthText);// 测试流式信息端点console.log('测试流式信息端点...');const infoResponse = await fetch('/api/stream/info');const infoData = await infoResponse.json();console.log('流式信息:', infoData);// 测试流式健康检查console.log('测试流式健康检查...');const streamHealthResponse = await fetch('/api/stream/health');const streamHealthText = await streamHealthResponse.text();console.log('流式健康检查结果:', streamHealthText);// 显示测试结果const output = document.getElementById('stream-content');output.innerHTML = `<div class="message-content">🔧 端点测试结果:<br><br>✅ 基础健康检查: ${healthText}<br><br>✅ 流式信息端点: 正常<br>• 描述: ${infoData.description}<br>• 可用端点: ${infoData.endpoints.length} 个<br><br>✅ 流式健康检查: 正常<br>• 响应长度: ${streamHealthText.length} 字符<br><br>💡 所有端点测试通过,流式聊天应该可以正常工作!</div>`;updateStreamStatus('completed', '测试完成');} catch (error) {console.error('端点测试失败:', error);const output = document.getElementById('stream-content');output.innerHTML = `<div class="message-content">❌ 端点测试失败:<br><br>错误信息: ${error.message}<br><br>💡 可能的原因:<br>• 应用未完全启动<br>• API密钥未正确配置<br>• 网络连接问题<br>• 服务器内部错误<br><br>🔧 建议解决方案:<br>1. 检查控制台日志<br>2. 运行 test-stream-endpoint.bat<br>3. 确认API密钥配置<br>4. 重启应用</div>`;updateStreamStatus('error', '测试失败');}}// 页面加载完成后的初始化document.addEventListener('DOMContentLoaded', function() {console.log('🚀 Spring AI DeepSeek 演示页面加载完成');// 检查服务状态fetch('/api/ai/health').then(response => response.text()).then(data => {console.log('✅ 服务状态:', data);updateStreamStatus('ready', '服务就绪');}).catch(error => {console.warn('⚠️ 服务检查失败:', error);updateStreamStatus('error', '服务异常');});// 添加键盘快捷键document.getElementById('stream-input').addEventListener('keydown', function(e) {if (e.ctrlKey && e.key === 'Enter') {startStream();}});});</script>
</body>
</html>
3. 预览(http://localhost:8080/index.html)
四、核心机制解析:从自动装配到接口设计
1. Spring AI 自动装配原理
当引入spring-boot-starter-ai-deepseek
后,Spring Boot 会自动加载以下组件:
-
DeepSeekProperties 配置类
读取application.yml
中以spring.ai.deepseek
开头的配置,转换为可注入的DeepSeekProperties
Bean -
DeepSeekChatCompletionService 客户端
基于配置信息创建 HTTP 客户端,支持:- 连接池管理(默认最大连接数 100)
- 请求签名自动生成(针对 DeepSeek API 认证机制)
- 响应反序列化(将 JSON 响应转为 Java 对象)
-
错误处理 Advice
自动捕获DeepSeekApiException
,转换为 Spring MVC 可处理的ResponseEntity
,包含:- 401 Unauthorized(API 密钥错误)
- 429 Too Many Requests(速率限制处理)
- 500 Internal Server Error(模型服务异常)
五、总结
通过本文实践,您已掌握:
- Spring AI 与 DeepSeek 的工程化集成方法
- 文本生成的同步 / 流式两种实现方式
- 自动装配机制与核心接口设计原理
后续可探索的方向:
- 多模型管理:通过
@Primary
注解实现模型切换,支持 A/B 测试 - 上下文管理:维护对话历史(
List<ChatMessage>
),实现多轮对话 - 插件扩展:自定义请求拦截器(添加业务参数)或响应处理器(数据清洗)
Spring AI 与 DeepSeek 的组合,为企业级 AI 应用开发提供了稳定高效的工程化解决方案。随着更多国产化模型的接入,这一生态将持续释放 AI 与传统业务融合的巨大潜力。立即尝试在您的项目中引入这套方案,开启智能开发新征程!