????????在人工智能技術與企業級開發深度融合的今天,傳統軟件開發模式與 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 與傳統業務融合的巨大潛力。立即嘗試在您的項目中引入這套方案,開啟智能開發新征程!