从 0 到 1:Spring Boot 与 Spring AI 深度实战(基于深度求索 DeepSeek)

发布于:2025-05-27 ⋅ 阅读:(110) ⋅ 点赞:(0)

        在人工智能技术与企业级开发深度融合的今天,传统软件开发模式与 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: true
        options:
          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: INFO
    com.example.demo: DEBUG
    org.springframework.ai: DEBUG
    org.springframework.ai.deepseek: DEBUG
  pattern:
    console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
    
# 管理端点配置
management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,env
      base-path: /actuator
  endpoint:
    health:
      show-details: always
  server:
    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")
    @ResponseBody
    public 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 资源处理器注册表
     */
    @Override
    public 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;
    }

    @Override
    public 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;
    }

    @Override
    public 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, '&amp;')
                        .replace(/</g, '&lt;')
                        .replace(/>/g, '&gt;')
                        .replace(/"/g, '&quot;')
                        .replace(/'/g, '&#39;')
                        .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 会自动加载以下组件:

  1. DeepSeekProperties 配置类
    读取application.yml中以spring.ai.deepseek开头的配置,转换为可注入的DeepSeekProperties Bean

  2. DeepSeekChatCompletionService 客户端
    基于配置信息创建 HTTP 客户端,支持:

    1. 连接池管理(默认最大连接数 100)
    2. 请求签名自动生成(针对 DeepSeek API 认证机制)
    3. 响应反序列化(将 JSON 响应转为 Java 对象)
  3. 错误处理 Advice
    自动捕获DeepSeekApiException,转换为 Spring MVC 可处理的ResponseEntity,包含:

    • 401 Unauthorized(API 密钥错误)
    • 429 Too Many Requests(速率限制处理)
    • 500 Internal Server Error(模型服务异常)

五、总结

通过本文实践,您已掌握:

  1. Spring AI 与 DeepSeek 的工程化集成方法
  2. 文本生成的同步 / 流式两种实现方式
  3. 自动装配机制与核心接口设计原理

后续可探索的方向:

  • 多模型管理:通过@Primary注解实现模型切换,支持 A/B 测试
  • 上下文管理:维护对话历史(List<ChatMessage>),实现多轮对话
  • 插件扩展:自定义请求拦截器(添加业务参数)或响应处理器(数据清洗)

        Spring AI 与 DeepSeek 的组合,为企业级 AI 应用开发提供了稳定高效的工程化解决方案。随着更多国产化模型的接入,这一生态将持续释放 AI 与传统业务融合的巨大潜力。立即尝试在您的项目中引入这套方案,开启智能开发新征程!