本文手把手教你用Spring Boot 3 + DeepSeek API搭建企业级智能对话服务,从项目初始化、流式SSE实现、上下文管理到Docker部署,全程实战代码+6个踩坑经验,看完就能直接用。
## 一、引言 2026年,AI大模型已经从概念验证进入全面落地阶段。企业级应用集成AI能力的需求呈爆发式增长,而Spring Boot 3作为Java生态中最主流的微服务框架,天然是承载AI能力的理想载体。 很多开发者面临一个现实问题:怎么把大模型API优雅地集成到现有Spring Boot项目中?直接写HttpClient调用?太原始。用WebClient?线程模型要考虑。还有流式响应的SSE(Server-Sent Events)怎么处理、Token怎么管理、上下文怎么维护? 这篇文章我就手把手带你走一遍完整流程,从Spring Boot 3项目搭建、DeepSeek API对接、流式对话实现、上下文管理、到Docker部署上线,全部实战代码+踩坑记录,保证看完就能用。 ## 二、项目初始化与依赖配置 ### 2.1 创建Spring Boot 3项目 我们先创建一个标准的Spring Boot 3项目。推荐用Spring Initializr(https://start.spring.io/)生成,选择以下依赖: ```xml org.springframework.boot spring-boot-starter-parent 3.4.2 ``` 关键依赖如下: ```xml org.springframework.boot spring-boot-starter-webflux org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-data-redis-reactive org.projectlombok lombok true ``` ### 2.2 配置DeepSeek API信息 在 `application.yml` 中配置API密钥和端点: ```yaml deepseek: api: key: ${DEEPSEEK_API_KEY:sk-your-key-here} base-url: https://api.deepseek.com/v1 model: deepseek-chat timeout: 60000 spring: data: redis: host: ${REDIS_HOST:localhost} port: 6379 ``` > **踩坑1**:DeepSeek API的base URL有两个版本 `/v1` 和 `/chat`。实测 `/v1` 兼容OpenAI格式,推荐使用这个路径,方便以后切换其他模型。 ## 三、核心模型定义 ### 3.1 请求/响应DTO ```java @Data @Builder @NoArgsConstructor @AllArgsConstructor public class ChatRequest { private String sessionId; private String message; private List history; } @Data @Builder @NoArgsConstructor @AllArgsConstructor public class ChatMessage { private String role; // system, user, assistant private String content; } @Data @Builder @NoArgsConstructor @AllArgsConstructor public class ChatResponse { private String sessionId; private String reply; private long timestamp; } ``` 这里 `ChatMessage` 的 `role` 字段严格遵循OpenAI消息格式。`system` 角色用于设定AI人格和行为约束,`user` 是用户输入,`assistant` 是AI回复。 ### 3.2 API请求体封装 ```java @Data @Builder @NoArgsConstructor @AllArgsConstructor public class DeepSeekRequest { private String model; private List messages; private boolean stream; @JsonProperty("max_tokens") private Integer maxTokens; private Double temperature; } @Data @Builder @NoArgsConstructor @AllArgsConstructor public class DeepSeekResponse { private String id; private String object; private long created; private String model; private List choices; private Usage usage; @Data @Builder @NoArgsConstructor @AllArgsConstructor public static class Choice { private int index; private ChatMessage message; private String finishReason; } @Data @Builder @NoArgsConstructor @AllArgsConstructor public static class Usage { @JsonProperty("prompt_tokens") private int promptTokens; @JsonProperty("completion_tokens") private int completionTokens; @JsonProperty("total_tokens") private int totalTokens; } } ``` ## 四、核心服务层实现 ### 4.1 构建HTTP客户端 ```java @Configuration public class DeepSeekClientConfig { @Value("${deepseek.api.base-url}") private String baseUrl; @Value("${deepseek.api.key}") private String apiKey; @Value("${deepseek.api.timeout:60000}") private long timeout; @Bean public WebClient deepSeekWebClient(WebClient.Builder builder) { return builder .baseUrl(baseUrl) .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer " + apiKey) .codecs(config -> config .defaultCodecs() .maxInMemorySize(10 * 1024 * 1024)) // 10MB .build(); } } ``` > **踩坑2**:maxInMemorySize不设大的话,当AI回复内容较长时会报 `DataBufferLimitException`。建议至少设到5-10MB。 ### 4.2 非流式对话实现 ```java @Service @RequiredArgsConstructor @Slf4j public class ChatService { private final WebClient deepSeekWebClient; private final SessionManager sessionManager; @Value("${deepseek.api.model}") private String model; public Mono chat(String sessionId, String message) { // 1. 获取或创建会话上下文 List context = sessionManager.getOrCreate(sessionId); // 2. 追加用户消息 context.add(ChatMessage.builder() .role("user") .content(message) .build()); // 3. 构建请求 DeepSeekRequest request = DeepSeekRequest.builder() .model(model) .messages(context) .stream(false) .temperature(0.7) .maxTokens(4096) .build(); // 4. 调用API return deepSeekWebClient .post() .uri("/chat/completions") .bodyValue(request) .retrieve() .bodyToMono(DeepSeekResponse.class) .map(response -> { String reply = response.getChoices().get(0).getMessage().getContent(); // 5. 保存上下文 context.add(ChatMessage.builder() .role("assistant") .content(reply) .build()); sessionManager.save(sessionId, context); log.info("会话{} 消耗tokens: {}", sessionId, response.getUsage().getTotalTokens()); return reply; }) .doOnError(e -> log.error("DeepSeek API调用失败: {}", e.getMessage())); } } ``` 这一段代码看似简单,但有几个设计考量: - 使用**Mono响应式**而非同步调用,避免阻塞Tomcat线程池 - **上下文管理**通过SessionManager抽象,方便后续切换到不同的存储后端 - **Token消耗日志**帮助优化prompt长度,控制成本 ### 4.3 流式对话实现(SSE) 流式回复是AI对话的标配功能。用户打字→AI逐字输出,体验远好于干等几十秒出完整结果。 ```java public Flux> chatStream(String sessionId, String message) { List context = sessionManager.getOrCreate(sessionId); context.add(ChatMessage.builder().role("user").content(message).build()); DeepSeekRequest request = DeepSeekRequest.builder() .model(model) .messages(context) .stream(true) .temperature(0.7) .maxTokens(4096) .build(); StringBuilder fullReply = new StringBuilder(); return deepSeekWebClient .post() .uri("/chat/completions") .bodyValue(request) .retrieve() .bodyToFlux(String.class) .filter(line -> line.startsWith("data: ")) .map(line -> line.substring(6)) // 去掉"data: "前缀 .filter(data -> !"[DONE]".equals(data.trim())) .flatMap(data -> { try { JsonNode node = new ObjectMapper().readTree(data); String delta = node.path("choices").get(0) .path("delta").path("content").asText(""); fullReply.append(delta); return Mono.just(ServerSentEvent.builder() .data(delta) .build()); } catch (Exception e) { return Mono.empty(); } }) .doOnComplete(() -> { // 流结束后保存完整上下文 context.add(ChatMessage.builder() .role("assistant") .content(fullReply.toString()) .build()); sessionManager.save(sessionId, context); }) .doOnError(e -> log.error("SSE流异常: {}", e.getMessage())); } ``` > **踩坑3**:DeepSeek流式返回的数据每行以 `data: ` 开头,结束标志是 `data: [DONE]`。如果不去掉前缀直接解析JSON会报错。另外注意 `retryWhen` 处理网络抖动,建议加指数退避重试。 ## 五、会话上下文管理 ### 5.1 内存+Redis两级缓存 ```java @Service @RequiredArgsConstructor public class SessionManager { private final StringRedisTemplate redisTemplate; private final ObjectMapper objectMapper; private static final int MAX_CONTEXT_LENGTH = 20; // 最大消息轮数 private static final long SESSION_TTL = 3600; // 1小时过期 public List getOrCreate(String sessionId) { String json = redisTemplate.opsForValue().get("chat:session:" + sessionId); if (json != null) { try { return objectMapper.readValue(json, new TypeReference>() {}); } catch (Exception e) { log.warn("反序列化失败,重建会话: {}", e.getMessage()); } } // 新建会话,注入system prompt List initial = new ArrayList<>(); initial.add(ChatMessage.builder() .role("system") .content("你是一个专业的Java技术助手,擅长Spring Boot、微服务和云原生技术。回答简洁准确,必要时给出代码示例。") .build()); return initial; } public void save(String sessionId, List messages) { // 上下文长度控制:保留最近的N轮对话 if (messages.size() > MAX_CONTEXT_LENGTH * 2 + 1) { List trimmed = new ArrayList<>(); trimmed.add(messages.get(0)); // 保留system prompt trimmed.addAll(messages.subList( messages.size() - MAX_CONTEXT_LENGTH * 2, messages.size())); messages = trimmed; } try { redisTemplate.opsForValue().set( "chat:session:" + sessionId, objectMapper.writeValueAsString(messages), Duration.ofSeconds(SESSION_TTL)); } catch (Exception e) { log.error("保存会话失败: {}", e.getMessage()); } } } ``` > **踩坑4**:上下文太长会爆Token!DeepSeek的上下文窗口虽然大(128K),但多轮对话下来容易撑爆。一定要做**上下文裁剪**。我的策略是保留system prompt + 最近N轮对话(N=10即20条消息),效果和成本平衡得不错。 ### 5.2 Token用量统计 ```java @Component @RequiredArgsConstructor public class TokenUsageTracker { private final ReactiveRedisTemplate redisTemplate; private static final String COUNTER_KEY = "stats:token:usage:daily"; public Mono recordUsage(String model, int promptTokens, int completionTokens) { String today = LocalDate.now().toString(); String key = COUNTER_KEY + ":" + today + ":" + model; return redisTemplate.opsForHash() .increment(key, "prompt", promptTokens) .then(redisTemplate.opsForHash() .increment(key, "completion", completionTokens)) .then(); } public Mono> getDailyUsage() { String today = LocalDate.now().toString(); String key = COUNTER_KEY + ":" + today + ":*"; // 通过keys或scan获取所有模型统计 // 这里简化为直接读取固定key return Mono.just(Map.of()); } } ``` 有了这个统计,你可以在管理面板上清楚地看到每天花了多少Token、哪个模型最烧钱。 ## 六、REST API控制器 ### 6.1 普通对话接口 ```java @RestController @RequestMapping("/api/ai") @RequiredArgsConstructor public class ChatController { private final ChatService chatService; @PostMapping("/chat") public Mono chat(@RequestBody ChatRequest request) { return chatService.chat(request.getSessionId(), request.getMessage()) .map(reply -> ChatResponse.builder() .sessionId(request.getSessionId()) .reply(reply) .timestamp(System.currentTimeMillis()) .build()); } } ``` ### 6.2 流式对话接口(前端实时打字效果) ```java @GetMapping(value = "/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux> chatStream( @RequestParam String sessionId, @RequestParam String message) { return chatService.chatStream(sessionId, message); } ``` 前端调用也很简单,原生EventSource就够用: ```javascript const evtSource = new EventSource(`/api/ai/chat/stream?sessionId=${sessionId}&message=${encodeURIComponent(text)}`); evtSource.onmessage = (event) => { // event.data 就是逐字输出的内容 outputDiv.textContent += event.data; }; evtSource.onerror = () => { console.log('流结束或出错'); evtSource.close(); }; ``` ## 七、Docker部署与性能优化 ### 7.1 多阶段Dockerfile ```dockerfile # 构建阶段 FROM eclipse-temurin:21-jdk-alpine AS builder WORKDIR /build COPY pom.xml . COPY src ./src RUN ./mvnw package -DskipTests -q # 运行阶段 FROM eclipse-temurin:21-jre-alpine WORKDIR /app COPY --from=builder /build/target/*.jar app.jar # JVM调优参数 ENV JAVA_OPTS="-Xms512m -Xmx1024m \ -XX:+UseZGC \ -XX:MaxGCPauseMillis=100 \ -XX:+HeapDumpOnOutOfMemoryError \ -Duser.timezone=Asia/Shanghai" EXPOSE 8080 ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"] ``` > **踩坑5**:为什么用ZGC?因为WebFlux+SSE场景下GC停顿会直接卡住流式输出,用户看到的文字突然停住几秒钟,体验很差。ZGC的低停顿时间(一般<2ms)完美适合这种场景。 ### 7.2 docker-compose一键部署 ```yaml version: '3.8' services: ai-service: build: . ports: - "8080:8080" environment: - DEEPSEEK_API_KEY=${DEEPSEEK_API_KEY} - REDIS_HOST=redis depends_on: - redis restart: unless-stopped redis: image: redis:7-alpine ports: - "6379:6379" volumes: - redis-data:/data restart: unless-stopped volumes: redis-data: ``` ## 八、性能压测与踩坑总结 ### 8.1 核心指标 用wrk简单压了一下(非流式接口): ```bash wrk -t4 -c50 -d30s http://localhost:8080/api/ai/chat \ -s post.lua # post.lua里构造JSON body ``` 结果: - 平均延迟:~1.2s(主要取决于DeepSeek API响应时间) - QPS:~40(受限于API并发限流) - 服务端CPU:<20% 瓶颈在DeepSeek API的并发限制,而不是我们的服务。如果要提高并发,可以做请求队列+限流熔断。 ### 8.2 完整踩坑清单 | 坑 | 症状 | 解决方案 | |:--|:-----|:---------| | 最大内存不足 | DataBufferLimitException | codec配置maxInMemorySize=10MB | | SSE流卡住 | 客户端收不到[DONE] | 加readTimeout和重试机制 | | 上下文爆炸 | 每次请求token翻倍 | 上下文裁剪+滑动窗口策略 | | GC停顿 | 流式输出中断 | 使用ZGC代替G1 | | API限流 | 429 Too Many Requests | 加布隆过滤器+令牌桶限流 | | 连接泄露 | 连接数持续增长 | 使用连接池+超时释放 | ## 九、小结与下期预告 这篇文章从零搭建了一个生产级别的Spring Boot 3 + DeepSeek AI对话服务,涵盖了: - ✅ Spring Boot 3 + WebFlux项目搭建 - ✅ DeepSeek流式/非流式API对接 - ✅ 会话上下文管理(Redis持久化+长上下文裁剪) - ✅ SSE流式响应实现 - ✅ Docker多阶段构建+JVM调优 - ✅ 6个实战踩坑总结 这些代码可以直接拿到生产环境使用。如果你在实际集成中遇到其他问题,欢迎评论区交流。 **下期预告:** 《Spring AI框架深度解析:一行代码切换LLM供应商,适配OpenAI/DeepSeek/Claude》——关注我,不迷路。 --- *如果本文对你有帮助,请点赞👍收藏⭐评论💬,让更多人看到。你的支持是我持续输出的动力!*
