## 一、引言 2026年AI大模型已经从概念验证进入全面落地阶段。企业级应用集成AI能力的需求呈爆发式增长而Spring Boot 3作为Java生态中最主流的微服务框架天然是承载AI能力的理想载体。 很多开发者面临一个现实问题怎么把大模型API优雅地集成到现有Spring Boot项目中直接写HttpClient调用太原始。用WebClient线程模型要考虑。还有流式响应的SSEServer-Sent Events怎么处理、Token怎么管理、上下文怎么维护 这篇文章我就手把手带你走一遍完整流程从Spring Boot 3项目搭建、DeepSeek API对接、流式对话实现、上下文管理、到Docker部署上线全部实战代码踩坑记录保证看完就能用。 ## 二、项目初始化与依赖配置 ### 2.1 创建Spring Boot 3项目 我们先创建一个标准的Spring Boot 3项目。推荐用Spring Initializrhttps://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**上下文太长会爆TokenDeepSeek的上下文窗口虽然大128K但多轮对话下来容易撑爆。一定要做**上下文裁剪**。我的策略是保留system prompt 最近N轮对话N10即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 --frombuilder /build/target/*.jar app.jar # JVM调优参数 ENV JAVA_OPTS-Xms512m -Xmx1024m \ -XX:UseZGC \ -XX:MaxGCPauseMillis100 \ -XX:HeapDumpOnOutOfMemoryError \ -Duser.timezoneAsia/Shanghai EXPOSE 8080 ENTRYPOINT [sh, -c, java $JAVA_OPTS -jar app.jar] **踩坑5**为什么用ZGC因为WebFluxSSE场景下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_HOSTredis 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并发限流 - 服务端CPU20% 瓶颈在DeepSeek API的并发限制而不是我们的服务。如果要提高并发可以做请求队列限流熔断。 ### 8.2 完整踩坑清单 | 坑 | 症状 | 解决方案 | |:--|:-----|:---------| | 最大内存不足 | DataBufferLimitException | codec配置maxInMemorySize10MB | | 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》——关注我不迷路。 --- *如果本文对你有帮助请点赞收藏⭐评论让更多人看到。你的支持是我持续输出的动力*