Spring集成DeepSeek方法1:自定义Client集成

概述

本文介绍如何通过自定义HTTP客户端的方式,在Spring Boot应用中集成DeepSeek API。这种方法提供了最大的灵活性和控制力,适合需要深度定制或特殊需求的场景。

前置条件

  • Java 17+
  • Spring Boot 3.x
  • DeepSeek API密钥(从 https://platform.deepseek.com 获取)
  • Maven或Gradle构建工具

项目依赖

Maven (pom.xml)

<dependencies>
    <!-- Spring Boot Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    
    <!-- Spring Boot WebFlux (推荐用于异步HTTP调用) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-webflux</artifactId>
    </dependency>
    
    <!-- Jackson用于JSON处理 -->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
    </dependency>
    
    <!-- Lombok (可选,简化代码) -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    
    <!-- Spring Boot Configuration Processor -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-configuration-processor</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>

Gradle (build.gradle)

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-webflux'
    implementation 'com.fasterxml.jackson.core:jackson-databind'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
}

配置类

application.yml

deepseek:
  api:
    base-url: https://api.deepseek.com
    api-key: ${DEEPSEEK_API_KEY:your-api-key-here}
    model: deepseek-chat
    timeout: 60000
    max-retries: 3

DeepSeekProperties.java

package com.example.deepseek.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

@Data
@Configuration
@ConfigurationProperties(prefix = "deepseek.api")
public class DeepSeekProperties {
    
    private String baseUrl = "https://api.deepseek.com";
    private String apiKey;
    private String model = "deepseek-chat";
    private Long timeout = 60000L;
    private Integer maxRetries = 3;
}

DeepSeekConfig.java

package com.example.deepseek.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.ExchangeStrategies;

@Configuration
public class DeepSeekConfig {
    
    @Bean
    public WebClient deepSeekWebClient(DeepSeekProperties properties) {
        // 配置内存缓冲区大小,用于处理大响应
        ExchangeStrategies strategies = ExchangeStrategies.builder()
                .codecs(configurer -> configurer
                        .defaultCodecs()
                        .maxInMemorySize(16 * 1024 * 1024)) // 16MB
                .build();
        
        return WebClient.builder()
                .baseUrl(properties.getBaseUrl())
                .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
                .defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer " + properties.getApiKey())
                .exchangeStrategies(strategies)
                .build();
    }
}

数据模型

ChatMessage.java

package com.example.deepseek.model;

import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ChatMessage {
    
    public enum Role {
        SYSTEM, USER, ASSISTANT
    }
    
    private Role role;
    private String content;
}

ChatRequest.java

package com.example.deepseek.model;

import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.List;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ChatRequest {
    
    private String model;
    private List<ChatMessage> messages;
    private Double temperature;
    private Integer maxTokens;
    private Double topP;
    private Boolean stream;
    
    public static ChatRequest of(String model, List<ChatMessage> messages) {
        return ChatRequest.builder()
                .model(model)
                .messages(messages)
                .build();
    }
}

ChatResponse.java

package com.example.deepseek.model;

import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.List;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ChatResponse {
    
    private String id;
    private String object;
    private Long created;
    private String model;
    private List<Choice> choices;
    private Usage usage;
    
    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    public static class Choice {
        private Integer index;
        private ChatMessage message;
        private String finishReason;
    }
    
    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    public static class Usage {
        private Integer promptTokens;
        private Integer completionTokens;
        private Integer totalTokens;
    }
}

ErrorResponse.java

package com.example.deepseek.model;

import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ErrorResponse {
    
    private Error error;
    
    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    public static class Error {
        private String message;
        private String type;
        private String code;
    }
}

服务层实现

DeepSeekService.java

package com.example.deepseek.service;

import com.example.deepseek.config.DeepSeekProperties;
import com.example.deepseek.model.*;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.WebClientResponseException;
import reactor.core.publisher.Mono;
import reactor.util.retry.Retry;

import java.time.Duration;
import java.util.List;

@Slf4j
@Service
@RequiredArgsConstructor
public class DeepSeekService {
    
    private final WebClient deepSeekWebClient;
    private final DeepSeekProperties properties;
    
    /**
     * 发送聊天请求
     */
    public Mono<String> chat(String userMessage) {
        return chat(userMessage, null);
    }
    
    /**
     * 发送带系统提示的聊天请求
     */
    public Mono<String> chat(String userMessage, String systemPrompt) {
        List<ChatMessage> messages = buildMessages(userMessage, systemPrompt);
        ChatRequest request = ChatRequest.builder()
                .model(properties.getModel())
                .messages(messages)
                .temperature(0.7)
                .maxTokens(4096)
                .build();
        
        return chat(request);
    }
    
    /**
     * 发送自定义聊天请求
     */
    public Mono<String> chat(ChatRequest request) {
        return deepSeekWebClient.post()
                .uri("/v1/chat/completions")
                .bodyValue(request)
                .retrieve()
                .bodyToMono(ChatResponse.class)
                .retryWhen(Retry.backoff(properties.getMaxRetries(), Duration.ofSeconds(1))
                        .filter(this::isRetryableException))
                .map(response -> {
                    if (response.getChoices() == null || response.getChoices().isEmpty()) {
                        throw new RuntimeException("No response choices returned");
                    }
                    return response.getChoices().get(0).getMessage().getContent();
                })
                .doOnError(error -> log.error("DeepSeek API call failed", error));
    }
    
    /**
     * 流式聊天
     */
    public Mono<String> chatStream(String userMessage) {
        return chatStream(userMessage, null);
    }
    
    /**
     * 流式聊天(带系统提示)
     */
    public Mono<String> chatStream(String userMessage, String systemPrompt) {
        List<ChatMessage> messages = buildMessages(userMessage, systemPrompt);
        ChatRequest request = ChatRequest.builder()
                .model(properties.getModel())
                .messages(messages)
                .temperature(0.7)
                .stream(true)
                .build();
        
        return deepSeekWebClient.post()
                .uri("/v1/chat/completions")
                .bodyValue(request)
                .retrieve()
                .bodyToFlux(String.class)
                .collectList()
                .map(chunks -> String.join("", chunks));
    }
    
    private List<ChatMessage> buildMessages(String userMessage, String systemPrompt) {
        List<ChatMessage> messages = new java.util.ArrayList<>();
        
        if (systemPrompt != null && !systemPrompt.isBlank()) {
            messages.add(ChatMessage.builder()
                    .role(ChatMessage.Role.SYSTEM)
                    .content(systemPrompt)
                    .build());
        }
        
        messages.add(ChatMessage.builder()
                .role(ChatMessage.Role.USER)
                .content(userMessage)
                .build());
        
        return messages;
    }
    
    private boolean isRetryableException(Throwable throwable) {
        if (throwable instanceof WebClientResponseException) {
            WebClientResponseException ex = (WebClientResponseException) throwable;
            int statusCode = ex.getStatusCode().value();
            // 重试 429 (Too Many Requests), 500, 502, 503, 504
            return statusCode == 429 || statusCode >= 500;
        }
        return false;
    }
}

控制器层

DeepSeekController.java

package com.example.deepseek.controller;

import com.example.deepseek.model.ChatRequest;
import com.example.deepseek.service.DeepSeekService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import reactor.core.publisher.Mono;

import java.io.IOException;

@RestController
@RequestMapping("/api/deepseek")
@RequiredArgsConstructor
public class DeepSeekController {
    
    private final DeepSeekService deepSeekService;
    
    /**
     * 简单聊天接口
     */
    @PostMapping("/chat")
    public Mono<String> chat(@RequestBody ChatRequest request) {
        return deepSeekService.chat(request);
    }
    
    /**
     * 快速聊天接口
     */
    @GetMapping("/chat")
    public Mono<String> quickChat(@RequestParam String message,
                                   @RequestParam(required = false) String systemPrompt) {
        return deepSeekService.chat(message, systemPrompt);
    }
    
    /**
     * 流式聊天接口 (SSE)
     */
    @GetMapping(value = "/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public SseEmitter chatStream(@RequestParam String message,
                                 @RequestParam(required = false) String systemPrompt) {
        SseEmitter emitter = new SseEmitter(60000L);
        
        deepSeekService.chatStream(message, systemPrompt)
                .subscribe(
                        response -> {
                            try {
                                emitter.send(SseEmitter.event().data(response));
                                emitter.complete();
                            } catch (IOException e) {
                                emitter.completeWithError(e);
                            }
                        },
                        error -> emitter.completeWithError(error)
                );
        
        return emitter;
    }
}

异常处理

DeepSeekExceptionHandler.java

package com.example.deepseek.exception;

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.reactive.function.client.WebClientResponseException;

import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;

@Slf4j
@RestControllerAdvice
public class DeepSeekExceptionHandler {
    
    @ExceptionHandler(WebClientResponseException.class)
    public ResponseEntity<Map<String, Object>> handleWebClientResponseException(WebClientResponseException ex) {
        log.error("DeepSeek API error: {} - {}", ex.getStatusCode(), ex.getResponseBodyAsString());
        
        Map<String, Object> error = new HashMap<>();
        error.put("timestamp", LocalDateTime.now());
        error.put("status", ex.getStatusCode().value());
        error.put("error", ex.getStatusText());
        error.put("message", ex.getMessage());
        
        return ResponseEntity.status(ex.getStatusCode()).body(error);
    }
    
    @ExceptionHandler(Exception.class)
    public ResponseEntity<Map<String, Object>> handleGenericException(Exception ex) {
        log.error("Unexpected error", ex);
        
        Map<String, Object> error = new HashMap<>();
        error.put("timestamp", LocalDateTime.now());
        error.put("status", HttpStatus.INTERNAL_SERVER_ERROR.value());
        error.put("error", "Internal Server Error");
        error.put("message", ex.getMessage());
        
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
    }
}

完整示例:Spring Boot主类

DeepSeekApplication.java

package com.example.deepseek;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DeepSeekApplication {
    
    public static void main(String[] args) {
        SpringApplication.run(DeepSeekApplication.class, args);
    }
}

使用示例

测试类

package com.example.deepseek;

import com.example.deepseek.service.DeepSeekService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import reactor.test.StepVerifier;

@SpringBootTest
class DeepSeekServiceTest {
    
    @Autowired
    private DeepSeekService deepSeekService;
    
    @Test
    void testChat() {
        StepVerifier.create(deepSeekService.chat("你好,请介绍一下你自己"))
                .expectNextMatches(response -> response != null && !response.isEmpty())
                .verifyComplete();
    }
    
    @Test
    void testChatWithSystemPrompt() {
        StepVerifier.create(deepSeekService.chat(
                "解释什么是量子计算",
                "你是一位物理学教授,请用通俗易懂的语言解释复杂概念"
        ))
                .expectNextMatches(response -> response != null && response.length() > 50)
                .verifyComplete();
    }
}

高级特性

1. 请求拦截器

package com.example.deepseek.interceptor;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.ClientRequest;
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
import reactor.core.publisher.Mono;

@Slf4j
@Component
public class DeepSeekLoggingFilter {
    
    public ExchangeFilterFunction logRequest() {
        return ExchangeFilterFunction.ofRequestProcessor(request -> {
            log.info("DeepSeek API Request: {} {}", request.method(), request.url());
            log.debug("Request Headers: {}", request.headers());
            return Mono.just(request);
        });
    }
    
    public ExchangeFilterFunction logResponse() {
        return ExchangeFilterFunction.ofResponseProcessor(response -> {
            log.info("DeepSeek API Response Status: {}", response.statusCode());
            return Mono.just(response);
        });
    }
}

2. 缓存装饰器

package com.example.deepseek.cache;

import com.example.deepseek.model.ChatRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;

@Slf4j
@Component
public class DeepSeekCacheService {
    
    private final DeepSeekService deepSeekService;
    
    public DeepSeekCacheService(DeepSeekService deepSeekService) {
        this.deepSeekService = deepSeekService;
    }
    
    @Cacheable(value = "deepseekResponses", key = "#request.hashCode()")
    public Mono<String> chatWithCache(ChatRequest request) {
        log.debug("Cache miss, calling DeepSeek API");
        return deepSeekService.chat(request);
    }
}

3. 限流器

package com.example.deepseek.ratelimit;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;

import java.util.concurrent.atomic.AtomicInteger;

@Slf4j
@Component
public class DeepSeekRateLimiter {
    
    private final AtomicInteger requestCount = new AtomicInteger(0);
    private final int maxRequestsPerMinute = 60;
    private volatile long resetTime = System.currentTimeMillis() + 60000;
    
    public Mono<Boolean> tryAcquire() {
        long currentTime = System.currentTimeMillis();
        
        if (currentTime > resetTime) {
            synchronized (this) {
                if (currentTime > resetTime) {
                    requestCount.set(0);
                    resetTime = currentTime + 60000;
                }
            }
        }
        
        int current = requestCount.incrementAndGet();
        if (current > maxRequestsPerMinute) {
            log.warn("Rate limit exceeded: {} requests in current minute", current);
            return Mono.just(false);
        }
        
        return Mono.just(true);
    }
}

优点与缺点

优点

  1. 完全控制:可以完全控制请求和响应的处理逻辑
  2. 灵活性高:易于实现自定义功能如缓存、限流、重试等
  3. 无额外依赖:只需Spring Boot基础依赖
  4. 易于调试:代码透明,便于问题排查
  5. 性能优化:可以使用WebFlux实现非阻塞IO

缺点

  1. 开发成本高:需要编写更多代码
  2. 维护负担:需要自行维护API兼容性
  3. 功能有限:需要自行实现流式输出、函数调用等高级功能
  4. 测试复杂:需要编写更多测试用例

适用场景

  • 需要深度定制API调用逻辑的场景
  • 对性能有极致要求的场景
  • 需要实现特殊功能(如复杂的重试策略、缓存策略)的场景
  • 学习和理解DeepSeek API的工作原理
Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐