【后端】使用 Spring Boot 调用 DeepSeek API:非流式实现

目录

  • 1. 前言
  • 2. 示例代码概述
    • 2.1. 工程结构
    • 2.2. 功能描述
  • 3. 核心代码解析
    • 3.1. 控制器层
    • 3.2. 服务层
    • 3.3. 调用API工具类
    • 3.4 前端页面
  • 4. 代码的不足与改进方向
    • 4.1. 当前实现的问题
    • 4.2 改进方向
  • 5. 执行结果
  • 5. 总结
  • 6. 福利资源


1. 前言

这是关于如何使用 Spring Boot 调用 DeepSeek API 的第一篇博客。在本文中,我们将介绍一个简单的示例代码,展示如何通过 HTTP 请求调用 DeepSeek API 并返回完整的响应内容。同时,我们会分析当前代码的不足,并提出改进方向——例如支持流式(streaming)处理,以及使用 SseEmitter 将结果逐步发送到前端。

2. 示例代码概述

2.1. 工程结构

本工程是一个基于 Spring Boot 的简单示例,主要功能是接收用户输入的消息并调用 DeepSeek API 返回响应。以下是核心文件及其作用:

  • DeepseekController.java:控制器层,处理前端请求并调用服务层。
  • DeepseekService.java:服务层,负责调用 DeepSeek API 客户端。
  • DeepseekApiClient.java:工具类,封装了对 DeepSeek API 的 HTTP 请求逻辑。
  • index.html:前端页面,提供用户输入框和显示响应的区域。
  • application.properties:配置文件,设置超时时间等参数。

2.2. 功能描述

  1. 用户在前端页面输入消息并点击“发送”按钮。
  2. 前端通过 /deepseek/chat 接口将消息传递给后端。
  3. 后端调用 DeepSeek API 获取响应,并将完整内容返回给前端。
  4. 前端显示响应内容。

3. 核心代码解析

3.1. 控制器层

略。可直接参考附件的源码

3.2. 服务层

DeepseekService 负责调用 DeepseekApiClient 并处理异常。

@Service
public class DeepseekService {

    private final DeepseekApiClient deepseekApiClient;

    public DeepseekService(DeepseekApiClient deepseekApiClient) {
        this.deepseekApiClient = deepseekApiClient;
    }

    public String getChatResponse(String message) {
        try {
            return deepseekApiClient.sendDeepseekChat(message);
        } catch (Exception e) {
            throw new RuntimeException("Failed to get chat response from DeepSeek API", e);
        }
    }
}

3.3. 调用API工具类

DeepseekApiClient 封装了对 DeepSeek API 的 HTTP 请求逻辑。

public class DeepseekApiClient {

    private static final Logger logger = LoggerFactory.getLogger(DeepseekApiClient.class);

    private static final String DEEPSEEK_API_URL_COMPLETIONS = "https://api.deepseek.com/chat/completions"; // API地址
    private static final String DEEPSEEK_API_KEY = "替换成你的key"; // 官网申请的API Key

    /**
     * 发送消息到 DeepSeek API 并获取响应
     *
     * @param message 用户输入的消息
     * @return API 的完整响应内容
     * @throws IOException 如果请求失败
     */
    public String sendDeepseekChat(String message) throws IOException {
        OkHttpClient client = createUnsafeOkHttpClient(); // 创建忽略SSL证书验证的OkHttpClient实例

        // 构造请求体
        JSONObject requestBody = buildRequestBody(message);

        // 将JSON对象转换为RequestBody,指定媒体类型为application/json
        RequestBody body = RequestBody.create(requestBody.toJSONString(), MediaType.get("application/json; charset=utf-8"));

        // 构造HTTP POST请求
        Request request = new Request.Builder()
                .url(DEEPSEEK_API_URL_COMPLETIONS) // 设置API URL
                .post(body) // 设置POST请求体
                .addHeader("Authorization", "Bearer " + DEEPSEEK_API_KEY) // 添加授权头
                .addHeader("Content-Type", "application/json") // 设置内容类型为JSON
                .addHeader("Accept", "application/json") // 设置接受的内容类型为JSON
                .build();


        System.out.println("jsonBody: " + requestBody.toJSONString());

        // 执行请求并处理响应
        try (Response response = client.newCall(request).execute()) {
            if (!response.isSuccessful()) {
                throw new IOException("Unexpected code " + response); // 如果响应不成功,抛出异常
            }

            String responseBody = response.body().string(); // 获取响应体字符串
            System.out.println("responseBody: " + responseBody);

            return parseResponse(responseBody); // 解析响应并返回消息内容
        }
    }

    /**
     * 解析 API 响应
     *
     * @param responseBody API 的响应内容
     * @return 解析后的消息内容
     */
    private String parseResponse(String responseBody) {
        JSONObject jsonResponse = JSON.parseObject(responseBody); // 将响应体字符串解析为JSON对象
        JSONArray choices = jsonResponse.getJSONArray("choices"); // 获取choices数组

        if (choices != null && !choices.isEmpty()) { // 检查choices数组是否非空
            JSONObject message = choices.getJSONObject(0).getJSONObject("message"); // 获取第一个choice中的message对象
            return message.getString("content"); // 返回message中的content字段
        }

        return "模型无响应或结果解析失败"; // 如果没有有效响应,返回null
    }

    /**
     * 构建请求体
     *
     * @param userMessage 用户输入的消息
     * @return 请求体JSON对象
     */
    private JSONObject buildRequestBody(String userMessage) {
        JSONObject requestBody = new JSONObject(); // 创建一个空的JSON对象作为请求体

        // 构建messages数组,包含系统消息和用户消息
        JSONArray messages = new JSONArray();
        messages.add(new JSONObject().fluentPut("content", "欢迎加入CSDN博客").fluentPut("role", "system").fluentPut("name", "personal"));
        // fluentPut是FastJSON提供的链式方法,用于快速设置键值对
        // content: 系统消息的内容
        // role: 消息的角色,这里是"system"表示系统消息
        // name: 消息的名称,这里是"personal"

        messages.add(new JSONObject().fluentPut("content", userMessage).fluentPut("role", "user").fluentPut("name", "路人甲"));
        // content: 用户输入的消息内容
        // role: 消息的角色,这里是"user"表示用户消息
        // name: 消息的名称,这里是"路人甲"

        // 使用fluentPut方法为请求体添加多个键值对
        requestBody.fluentPut("messages", messages)
                .fluentPut("model", "deepseek-chat"); // 指定使用的模型名称
        // model: 指定要使用的模型,这里是"deepseek-chat"

        requestBody.fluentPut("frequency_penalty", 0); // 控制生成文本的重复度惩罚因子,默认为0
        // frequency_penalty: 用于减少重复词的出现频率,范围通常为0到2

        requestBody.fluentPut("max_tokens", 2048); // 设置最大生成token数
        // max_tokens: 指定生成的最大token数量,这里是2048

        requestBody.fluentPut("presence_penalty", 0); // 控制生成文本的新颖性惩罚因子,默认为0
        // presence_penalty: 用于鼓励生成新的主题或概念,范围通常为0到2

        requestBody.fluentPut("response_format", new JSONObject().fluentPut("type", "text")); // 指定响应格式为文本
        // response_format: 指定API返回的格式,这里是"text"表示纯文本

        requestBody.fluentPut("stop", null); // 指定停止序列,null表示无特定停止条件
        // stop: 可以是一个字符串或字符串数组,当生成的文本遇到这些序列时停止

        requestBody.fluentPut("stream", false); // 是否开启流式响应,默认为false
        // requestBody.fluentPut("stream", true); // 是否开启流式响应,默认为false
        // stream: true表示开启流式响应,false表示一次性返回所有结果

        requestBody.fluentPut("stream_options", null); // 流式响应的选项,null表示无特定选项
        // stream_options: 配置流式响应的相关参数

        requestBody.fluentPut("temperature", 1); // 控制生成文本的随机性,默认为1
        // temperature: 影响生成文本的创造性,值越高越随机,值越低越保守

        requestBody.fluentPut("top_p", 1); // 核采样概率阈值,默认为1
        // top_p: 用于控制生成文本的概率分布,值越小越集中于高概率词汇

        requestBody.fluentPut("tools", null); // 自定义工具列表,null表示无工具
        // tools: 可以定义一些自定义工具供模型使用

        requestBody.fluentPut("tool_choice", "none"); // 工具选择策略,默认为"none"
        // tool_choice: 指定如何选择工具,"none"表示不使用工具

        requestBody.fluentPut("logprobs", false); // 是否返回每个token的对数概率,默认为false
        // logprobs: true表示返回每个token的对数概率,false表示不返回

        requestBody.fluentPut("top_logprobs", null); // 返回每个token的最高对数概率数,null表示不返回
        // top_logprobs: 指定返回每个token的最高对数概率的数量

        return requestBody; // 返回构建好的请求体
    }

    /**
     * 创建忽略SSL证书验证的OkHttpClient实例
     *
     * @return OkHttpClient实例
     */
    private OkHttpClient createUnsafeOkHttpClient() {
        try {
            // 创建信任所有主机的TrustManager
            TrustManager[] trustAllCerts = new TrustManager[]{
                    new X509TrustManager() {
                        @Override
                        public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {}

                        @Override
                        public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {}

                        @Override
                        public X509Certificate[] getAcceptedIssuers() {
                            return new X509Certificate[]{};
                        }
                    }
            };

            SSLContext sslContext = SSLContext.getInstance("TLS"); // 初始化SSL上下文
            sslContext.init(null, trustAllCerts, new java.security.SecureRandom()); // 使用信任所有证书的TrustManager初始化SSL上下文
            SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory(); // 获取SSLSocketFactory

            // 忽略主机名验证
            HostnameVerifier hostnameVerifier = (hostname, session) -> true;

            return new OkHttpClient.Builder()
                    .sslSocketFactory(sslSocketFactory, (X509TrustManager) trustAllCerts[0]) // 设置SSLSocketFactory和TrustManager
                    .hostnameVerifier(hostnameVerifier) // 设置主机名验证器
                    .connectTimeout(60, TimeUnit.SECONDS) // 设置连接超时时间为1分钟
                    .readTimeout(60, TimeUnit.SECONDS)    // 设置读取超时时间为1分钟
                    .build(); // 构建并返回OkHttpClient实例
        } catch (Exception e) {
            throw new RuntimeException(e); // 捕获异常并抛出运行时异常
        }
    }
}

3.4 前端页面

前端页面通过 JavaScript 发起请求,并显示响应内容。代码略。

4. 代码的不足与改进方向

4.1. 当前实现的问题

4.1.1. 非流式处理
当前实现是同步的,即后端需要等待 API 返回完整响应后才能将其发送给前端。这种方式在处理大段文本或耗时较长的请求时,用户体验较差。
4.1.2. 缺乏实时性
如果 API 支持流式输出(如逐字返回),我们可以利用这一特性提升交互体验。然而,当前代码并未启用流式处理。
4.1.3. 未充分利用 Spring 的异步能力
Spring 提供了多种异步处理机制(如 SseEmitter 和 WebFlux),可以更好地支持流式数据传输。

4.2 改进方向

4.2.1 启用流式处理
DeepSeek API 通常支持流式输出(通过 stream=true 参数)。我们可以在请求中启用该选项,并逐步接收响应数据。
4.2.2. 使用 SseEmitter
Spring 提供了SseEmitter类,可以用于服务器向客户端推送事件。我们可以将 API 的响应逐步发送到前端,从而实现更流畅的用户体验。
以下是一个改进后的流式简单处理方法:

public void streamChatResponse(String message, SseEmitter emitter) {
    executorService.submit(() -> {
        try {
            // 假设 API 支持流式输出
            String fullResponse = deepseekApiClient.sendDeepseekChat(message);

            // 模拟逐字发送
            for (int i = 0; i < fullResponse.length(); i++) {
                char c = fullResponse.charAt(i);
                emitter.send(SseEmitter.event().data(String.valueOf(c)));
                Thread.sleep(50); // 模拟延迟
            }

            emitter.send(SseEmitter.event().data("[DONE]"));
        } catch (Exception e) {
            emitter.completeWithError(e);
        } finally {
            emitter.complete();
        }
    });
}

4.2.3. 前端适配
前端也需要调整为支持 SSE(Server-Sent Events)。可以通过监听 EventSource 来接收服务器推送的数据

const eventSource = new EventSource('/deepseek/stream-chat?message=' + encodeURIComponent(message));
eventSource.onmessage = function(event) {
    const responseDiv = document.getElementById('response');
    responseDiv.textContent += event.data;
};
eventSource.onerror = function() {
    eventSource.close();
};

5. 执行结果

【后端】使用 Spring Boot 调用 DeepSeek API:非流式实现_第1张图片

5. 总结

本文介绍了如何使用 Spring Boot 调用 DeepSeek API,并展示了非流式的实现方式。尽管当前代码能够满足基本需求,但在实际应用中仍有较大的优化空间。下一节中,我们将详细介绍如何实现流式处理,并进一步提升用户体验。
如果你对本文有任何疑问或建议,欢迎在评论区留言!

6. 福利资源

完整的代码示例已上传,可在Intellij IDEA中导入并直接运行 免费下载

你可能感兴趣的:(人工智能,spring,boot,后端)