从 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 与传统业务融合的巨大潜力。立即尝试在您的项目中引入这套方案,开启智能开发新征程!
相关文章:

从 0 到 1:Spring Boot 与 Spring AI 深度实战(基于深度求索 DeepSeek)
在人工智能技术与企业级开发深度融合的今天,传统软件开发模式与 AI 工程化开发的差异日益显著。作为 Spring 生态体系中专注于 AI 工程化的核心框架,Spring AI通过标准化集成方案大幅降低 AI 应用开发门槛。本文将以国产大模型代表 ** 深度求索ÿ…...

upload-labs通关笔记-第20关 文件上传之杠点绕过
系列目录 upload-labs通关笔记-第1关 文件上传之前端绕过(3种渗透方法) upload-labs通关笔记-第2关 文件上传之MIME绕过-CSDN博客 upload-labs通关笔记-第3关 文件上传之黑名单绕过-CSDN博客 upload-labs通关笔记-第4关 文件上传之.htacess绕过-CSDN…...

Vscode +Keil Assistant编译报错处理
Vscode Keil Assistant编译报错处理 1.报错图片内容 所在位置 行:1 字符: 25 chcp.com 65001 -Command & c:\Users\92170.vscode\extensions\cl.keil-a … ~ 不允许使用与号(&)。& 运算符是为将来使用而保留的;请用双引号将与号引起来(“&”)&…...
记录python在excel中添加一列新的列
思路是,先将需要添加为新的列存储到一个暂时的列表中,然后用到以下函数来存储 data_.loc[:, "新列的名字"] save_list_ 上面的save_list_就是暂时存储了信息的列表了。 以下是我的代码,供以后快速回忆。 schools_data {"98…...
WebRTC:实时通信的未来之路
WebRTC:实时通信的未来之路 目录 WebRTC:实时通信的未来之路一、背景介绍二、使用方式三、前途展望 一、背景介绍 随着互联网的飞速发展,实时音视频通信需求日益增长。传统的音视频通信多依赖于专有协议和插件(如Flash、ActiveX等…...
探索产品经理的MVP:从概念到实践
在产品开发的世界里,MVP(Minimum Viable Product,最小可行产品)是一个至关重要的概念。它不仅帮助团队快速验证假设,还能降低失败风险,为后续的产品迭代奠定坚实的基础。本文将深入探讨MVP的概念、重要性及…...
用python实现中国象棋
一.象棋规则 象棋是二人对弈的棋类游戏,棋盘由 9 条竖线和 10 条横线交叉构成,中间 “河界” 分楚汉,两端 “九宫” 各 9 个交叉点。棋子分红黑,各 16 枚,含 7 兵种。 1.棋子走法 1.1 红方棋子 帅:1 个…...
GO 语言基础3 struct 结构体
更多个人笔记见: github个人笔记仓库 gitee 个人笔记仓库 个人学习,学习过程中还会不断补充~ (后续会更新在github上) 文章目录 strcut结构体基本例子传入数值和指针的区别初始化方法汇总结构体特点结构体方法定义基于…...

VSCode C/C++ 开发环境完整配置及一些扩展用途(自用)update:2025/3/31
这里主要记录了一些与配置相关的内容。由于网上教程众多,部分解决方法并不能完全契合我遇到的问题,因此我选择以自己偏好的方式,对 VSCode 进行完整的配置,并记录在使用过程中遇到的问题及解决方案。后续内容也会持续更新和完善。…...
iOS 上线前的性能与稳定性检查流程实录:开发者的“最后一公里”(含 KeyMob 应用经验)
一个 iOS 项目写完功能、跑完测试,离上线只差一步了——但很多问题恰恰就在“这最后一公里”暴露:某些设备发热严重,部分流程偶发卡顿,某些崩溃只有长时间运行后才出现。 今天我分享的是我在多个 iOS 项目上线前实际执行过的性能…...

Docker系列(二):开机自启动与基础配置、镜像加速器优化与疑难排查指南
引言 docker 的快速部署与高效运行依赖于两大核心环节:基础环境搭建与镜像生态优化。本期博文从零开始,系统讲解 docker 服务的管理配置与镜像加速实践。第一部分聚焦 docker 服务的安装、权限控制与自启动设置,确保环境稳定可用;…...

a16z:AI带来了全新的9种开发软件的模式
非常有启发的9条新兴模式,推荐给已经上手 vibeCoding 的读者们。 开发者正在将 AI 从简单的工具转变为构建软件的新基础。许多核心概念,如版本控制、模板、文档,甚至用户的定义,都在被重新思考。代理(Agent)…...
20.迭代器模式:思考与解读
原文地址:迭代器模式:思考与解读 更多内容请关注:深入思考与解读设计模式 引言 在软件开发中,尤其是在处理集合数据时,你是否曾经遇到过这样的问题:你需要遍历一个集合(如数组、列表、集合等)…...
Java 学习笔记:注解、泛型与 IO 流
目录 课程目标 Java 注解(Annotation) 1. 概念与作用 2. 自定义注解示例 3. JDK 内置注解 4.注释 Java 泛型(Generics) 1. 基本语法 2. 通配符与上下限 3. 常见应用场景 Java IO 流 1. 流的分类1.File文件类 2. 字节流与字符流 3. 经典示例:文件拷贝 总结与…...

在 Excel 使用macro————仙盟创梦IDE
Dim filePath As StringDim fileContent As StringDim lines() As StringDim dataArray() As StringDim lineCount As LongDim maxCols As LongDim i As Long, j As Long 文件路径filePath "" 检查文件是否存在If Dir(filePath) "" ThenMsgBox "文件…...
【MySQL】08.视图
视图就是一个由查询到的内容定义的虚拟表。它和真实的表一样,视图包含一系列带有名称的列和行数据。视图的数据变化会影响到基表,基表的数据变化也会影响到视图。 1. 基本使用 mysql> select * from user; -------------------- | id | age | name …...

鸿蒙devEco studio如何创建模拟器
官网原文链接:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/ide-emulator-create 操作步骤 点击菜单栏的Tools > Device Manager,点击右下角的Edit设置模拟器实例的存储路径Local Emulator Location,Mac默认存储在~/…...

鸿蒙路由参数传递
页面test.ets 代码如下: import router from ohos.router Entry Component struct Test {State message: string Hello WorldState username: string huState password: string 1build() {Row() {Column() {Text(this.message).fontSize(50).fontWeight(FontWe…...

springboot 控制层调用业务逻辑层,注入报错,无法自动装配 解决办法
报错: 解决:愿意是业务逻辑层,即service层的具体实现类没有加注解Service导致的,加上解决了!!...

MySQL:11_事务
事务 一.CURD不加控制,会有什么问题? 二.什么是事务? 事务就是一组DML语句组成,这些语句在逻辑上存在相关性,这一组DML语句要么全部成功,要么全部失败,是一个整体。MySQL提供一种机制…...

Linux中的文件系统和软硬连接
磁盘的访问方式 CHS(柱面,磁头,扇区) 法(磁盘硬件查找): 确定柱面(C) 磁头臂移动到对应的柱面位置。例如,柱面号为 5,则磁头移动到第 5 个磁道组…...
并发容器(Collections)
一、并发安全问题根源 1. List(如ArrayList) 问题表现:多线程同时调用add、remove等方法时,可能抛出ConcurrentModificationException或导致数据不一致。根本原因: 非原子性操作:如add操作的流程…...
SPA模式下的es6如何加快宿主页的显示速度
SPA的模式下,宿主页是首先加载的页面,会需要一些主要的组件,如element-plus,easyui,devextreme,ant-design等,这些组件及其依赖组件,文件多,代码量大,可能导致…...
windows powershell 判断 进程号是否存在
在 Windows PowerShell 中,你可以使用多种方法来检查一个特定的进程号(PID)是否存在。以下是几种常用的方法: 方法1:使用 Get-Process 命令 你可以尝试获取具有特定 PID 的进程。如果该进程存在,Get-Proce…...
c# 解码 encodeURIComponent
在C#中,如果你需要解码由encodeURIComponent方法编码的URL,你可以使用System.Web命名空间中的HttpUtility.UrlDecode方法。这个方法可以处理由JavaScript的encodeURIComponent方法编码的字符串。 首先,确保你的项目中引用了System.Web命名空…...

Spring AI:Java开发者的AI开发新利器
目录 一、引言 二、Spring AI 是什么 三、核心功能与特性 3.1 统一的 API 抽象 3.2 丰富的模型支持 3.3 低代码集成 3.4 结构化数据输出 3.5 流式数据响应 四、应用场景 4.1 智能客服系统 4.2 图像识别应用 4.3 数据分析与预测 五、快速上手 5.1 环境搭建 5.2 创…...
Android System UI 深度解析:从架构演进到车载 / TV 场景的全维度定制
Android System UI 是 Android 操作系统的核心组件,负责管理设备的系统级用户界面和交互逻辑。它通过状态栏、导航栏、通知面板、快速设置等功能,为用户提供与系统功能直接交互的入口,并与硬件、应用程序深度协同,构建完整的用户体验。以下是其核心架构、功能演进及定制化能…...

Spring Cloud Sleuth与Zipkin深度整合指南:微服务链路追踪实战
上篇文章简单介绍了SpringCloud系列熔断器:Sentinel的搭建及基本用法,今天继续讲解下SpringCloud的微服务链路追踪:Zipkin的使用!在分享之前继续回顾下本次SpringCloud的专题要讲的内容: 前置知识说明 在开始本教程前…...
React从基础入门到高级实战:React 基础入门 - 列表渲染与条件渲染
列表渲染与条件渲染 在 React 开发中,列表渲染 和 条件渲染 是处理动态数据和用户交互的基础技术。通过列表渲染,你可以根据数据动态生成 UI 元素;而条件渲染则让你根据特定条件展示不同的内容。这两个技能在实际项目中非常常见,…...
在 stm32 中 volatile unsigned signed 分别有什么作用,分别在什么场景下使用?
在STM32开发中, plaintext 复制 volatile 、 plaintext 复制 unsigned 和 plaintext 复制 signed 是三个关键的关键字,它们的用途和场景如下: 1. plaintext 复制 volatile 关键字 作用: 禁止编译器优化ÿ…...