SpringBoot接入DeepSeek(硅基流动版)+ 前端页面调试

发布于:2025-02-27 ⋅ 阅读:(18) ⋅ 点赞:(0)

前言

作为一个Java程序员,了解前沿科技技术,也算是份内的事了。
DeepSeek 大模型,从开源到现在,一直在🔥。各个公司也基本都部署了自己的所谓满血版DeepSeek。

虽然官方是免费使用的,但是它太忙了。因此,很多能直接使用的,不太忙的DeepSeek应运而生。

我今天使用的就是“硅基流动版”DeepSeek!

本文的主旨是,使用SpringBoot接入DeepSeek,并提供一个调试页面,用来请求,和展示结果。

硅基流动DeepSeek页面:
https://m.siliconflow.cn/playground/chat
硅基流动推理模型接口文档:
https://docs.siliconflow.cn/cn/userguide/capabilities/reasoning

正文

一、项目环境

  • Java版本:Java1.8
  • SpringBoot版本:2.7.7
  • deepseek-spring-boot-starter:1.1.0

项目结构如下:
在这里插入图片描述

二、项目代码

2.1 pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>org.pine.ai</groupId>
  <artifactId>pine-ai</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>jar</packaging>

  <name>pine-ai-demo</name>
  <url>http://maven.apache.org</url>

  <properties>
    <java.version>1.8</java.version>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <spring-boot.version>2.7.7</spring-boot.version>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <version>1.18.34</version>
      <scope>provided</scope>
      <optional>true</optional>
    </dependency>
    <dependency>
      <groupId>io.github.pig-mesh.ai</groupId>
      <artifactId>deepseek-spring-boot-starter</artifactId>
      <version>1.1.0</version>
    </dependency>

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
      <version>2.7.7</version>
    </dependency>
    <dependency>
      <groupId>ch.qos.logback</groupId>
      <artifactId>logback-classic</artifactId>
      <version>1.2.11</version>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.8.1</version>
        <configuration>
          <source>1.8</source>
          <target>1.8</target>
          <encoding>UTF-8</encoding>
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        <version>2.7.7</version>
        <configuration>
          <mainClass>org.pine.ai.BootDemoApplication</mainClass>
          <skip>true</skip>
        </configuration>
        <executions>
          <execution>
            <id>repackage</id>
            <goals>
              <goal>repackage</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
</project>

2.2 DeepSeekController.java

package org.pine.ai.controller;

import io.github.pigmesh.ai.deepseek.core.DeepSeekClient;
import io.github.pigmesh.ai.deepseek.core.chat.ChatCompletionRequest;
import io.github.pigmesh.ai.deepseek.core.chat.ChatCompletionResponse;
import io.github.pigmesh.ai.deepseek.core.chat.ResponseFormatType;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import reactor.core.publisher.Flux;

import javax.annotation.Resource;
import java.util.List;

@Controller
@Slf4j
@RequestMapping("/deepseek")
public class DeepSeekController {

    @Resource
    private DeepSeekClient deepSeekClient;

    @GetMapping(value = "/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<ChatCompletionResponse> chat(@RequestParam("prompt") String prompt,
                                             @RequestParam(value = "model", defaultValue = "deepseek-ai/DeepSeek-R1") String model,
                                             @RequestParam(value = "temperature", defaultValue = "0.7") Double temperature,
                                             @RequestParam(value = "frequencyPenalty", defaultValue = "0.5") Double frequencyPenalty,
                                             @RequestParam(value = "user", defaultValue = "user") String user,
                                             @RequestParam(value = "topP", defaultValue = "0.7") Double topP,
                                             @RequestParam(value = "maxCompletionTokens", defaultValue = "1024") Integer maxCompletionTokens) {
        log.info("prompt: {}", prompt);
        log.info("model: {}, temperature: {}, frequencyPenalty: {}, user: {}, topP: {}, maxCompletionTokens: {}", model, temperature, frequencyPenalty, user, topP, maxCompletionTokens);

        if (!StringUtils.hasText(prompt)) {
            throw new IllegalArgumentException("prompt is empty");
        }

        ChatCompletionRequest request = ChatCompletionRequest.builder()
                // 添加用户输入的提示词(prompt),即模型生成文本的起点。告诉模型基于什么内容生成文本。
                .addUserMessage(prompt)
                // 指定使用的模型名称。不同模型可能有不同的能力和训练数据,选择合适的模型会影响生成结果。
                .model(model)
                // 是否以流式(streaming)方式返回结果。
                .stream(true)
                // 控制生成文本的随机性。0.0:生成结果非常确定,倾向于选择概率最高的词。1.0:生成结果更具随机性和创造性。
                .temperature(temperature)
                // 控制生成文本中重复内容的惩罚程度。0.0:不惩罚重复内容。1.0 或更高:减少重复内容,增加多样性。
                .frequencyPenalty(frequencyPenalty)
                // 标识请求的用户。用于跟踪和日志记录,通常用于区分不同用户的请求。
                .user(user)
                // 控制生成文本时选择词的范围。0.7:从概率最高的 70% 的词中选择。1.0:不限制选择范围。
                .topP(topP)
                // 控制模型生成的文本的最大长度。这对于防止生成过长的文本或确保响应在预期的范围内非常有用。
                .maxCompletionTokens(maxCompletionTokens)
                // 响应结果的格式。
                .responseFormat(ResponseFormatType.TEXT)
                .build();

        return deepSeekClient.chatFluxCompletion(request);
    }
}

2.3 启动类

package org.pine.ai;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.CrossOrigin;

@SpringBootApplication
@CrossOrigin(
        origins = "*",
        allowedHeaders = "*",
        exposedHeaders = {"Cache-Control", "Connection"}  // 暴露必要头
)
public class BootDemoApplication {

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

}

2.4 logback-spring.xml

<?xml version="1.0" encoding="UTF-8" ?>
<configuration debug="false">
    <!-- 配置控制台输出 -->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <!-- 格式化输出: %d表示日期, %thread表示线程名, %-5level: 级别从左显示5个字符宽度 %msg:日志消息, %n是换行符 -->
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS}[%thread] %-5level %logger{50} - %msg%n</pattern>
        </encoder>
    </appender>

    <!-- 日志输出级别 -->
    <root level="INFO">
        <appender-ref ref="STDOUT"/>
    </root>
</configuration>

2.5 application.yaml

deepseek:
  # 硅基流动的url
  base-url: https://api.siliconflow.cn/v1
  # 秘钥(自己注册硅基的账号,并申请即可)
  api-key: sk-ezcxadqecocxixxxxxxx
spring:
  main:
    allow-bean-definition-overriding: true

server:
  tomcat:
    keep-alive-timeout: 30000  # 30秒空闲超时
    max-connections: 100       # 最大连接数
    uri-encoding: UTF-8
  servlet:
    encoding:
      charset: UTF-8
      force: true
      enabled: true

  compression:
    enabled: false  # 禁用压缩(否则流式数据可能被缓冲)



2.6 index.html

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>调用演示</title>
  <style>
    body {
      padding: 20px;
      font-family: Arial, sans-serif;
    }
    #output {
      height: 400px;
      width: 900px;
      border: 2px solid #c7c1c1;
      padding: 10px;
      overflow-y: auto;
      margin: 20px 0;
      white-space: pre-wrap;
      box-shadow: 0 4px 6px rgba(13, 118, 231, 0.25);
    }
    button {
      padding: 10px 20px;
      background: #007bff;
      color: white;
      border: none;
      border-radius: 4px;
      cursor: pointer;
    }

    .danger-button {
      background-color: #ec1b1b;
    }

    .form-group {
      margin: 20px 0;
      max-width: 900px;
    }

    label[for="promptInput"] {
      display: block;
      margin-bottom: 12px;
      font-size: 1.1em;
      color: #2c3e50;
      font-weight: 600;
      letter-spacing: 0.5px;
    }
    label[for="paramInput"] {
      display: block;
      margin-bottom: 12px;
      font-size: 1.1em;
      color: #2c3e50;
      font-weight: 600;
      letter-spacing: 0.5px;
    }

    #promptInput, #paramInput {
      /* 尺寸调整 */
      width: 100%;
      padding: 14px 20px;

      /* 样式美化 */
      border: 2px solid #007bff;
      border-radius: 8px;
      font-size: 1.1rem;
      background-color: #f8f9fa;
      transition: all 0.3s ease-in-out;
      box-shadow: 0 2px 4px rgba(0,0,0,0.05);
      color: #44878c;
      font-style: italic;
    }

    #promptInput, #paramInput:focus {
      outline: none;
      border-color: #0056b3;
      box-shadow: 0 4px 6px rgba(0,123,255,0.25);
      background-color: white;
    }

    /* 暗色模式适配 */
    @media (prefers-color-scheme: dark) {
      #promptInput, #paramInput {
        background-color: #2d3436;
        border-color: #4a90e2;
        color: #ecf0f1;
      }

      #promptInput, #paramInput:focus {
        background-color: #34495e;
      }
    }

    /* 输入提示动画 */
    @keyframes pulse-shadow {
      0% { box-shadow: 0 0 0 0 rgba(0,123,255,0.4) }
      100% { box-shadow: 0 0 0 10px rgba(0,123,255,0) }
    }

    .input-highlight {
      animation: pulse-shadow 1.5s infinite;
    }
  </style>
</head>
<body>

<div class="form-group">
  <label for="promptInput">URL + Prompt 提示词:</label>
  <input type="text"
         id="promptInput"
         value="http://localhost:8080/deepseek/chat?prompt="
         class="styled-input">
</div>

<div class="form-group">
  <label for="paramInput">参数串:</label>
  <input type="text"
         id="paramInput"
         value="model=deepseek-ai/DeepSeek-R1&temperature=0.7&frequencyPenalty=0.5&user=user&topP=0.7&maxCompletionTokens=1024"
         class="styled-input">
</div>

<button onclick="start()">开始请求</button>
<button onclick="disconnectAiServer()" class="danger-button">断开连接</button>
<button onclick="tips()">参数提示</button>
<div id="output"></div>


<script>
  let eventSource;
  function connectAiServer(url) {
    eventSource = new EventSource(url);
  }

  function disconnectAiServer() {
    if (eventSource) {
      eventSource.close()
    }
  }


  function start() {
    const output = document.getElementById('output')
    const promptInput = document.getElementById('promptInput')
    const paramInput = document.getElementById('paramInput')
    output.textContent = '' // 清空内容

    try {
      // 创建 EventSource 连接
      connectAiServer(promptInput.value + "&" + paramInput.value)

      // 监听默认事件(无事件名的消息)
      eventSource.onmessage = (event) => {
        const value = event.data;
        const data = JSON.parse(value);

        if (data.choices[0].delta.reasoning_content === '') {
          output.textContent += '开始思考:\n\t'
        }
        if (data.choices[0].delta.content === '\n\n') {
          output.textContent += '思考结束!!\n\n\n\n'
          output.textContent += '以下是正式回答:\n\n'
        }
        if(data.choices[0].delta.content === "" && data.choices[0].finish_reason === "stop") {
          output.textContent += '\r\n\r\n回答结束!!'
          disconnectAiServer()
        }

        // 拼接思考内容
        if (data.choices[0].delta.reasoning_content) {
          output.textContent += data.choices[0].delta.reasoning_content
        }

        // 拼接回答内容
        if (data.choices[0].delta.content) {
          output.textContent += data.choices[0].delta.content;
        }

        // 自动滚动到底部
        output.scrollTop = output.scrollHeight
      };

    } catch (error) {
      console.error('请求失败:', error)
      output.textContent = '请求失败: ' + error.message
      disconnectAiServer()
    }
  }

  /* 回车触发请求 */
  document.querySelector('#promptInput').addEventListener('keypress', (e) => {
    if(e.key === 'Enter') {
      start()
    }
  })

  function tips() {
    const output = document.getElementById('output')
    output.textContent = `
      参数说明:
      prompt:添加用户输入的提示词(prompt),即模型生成文本的起点。告诉模型基于什么内容生成文本。默认为:空\n
      model:不同模型可能有不同的能力和训练数据,选择合适的模型会影响生成结果。默认为:deepseek-ai/DeepSeek-R1\n
      temperature:控制生成文本的随机性。0.0:生成结果非常确定,倾向于选择概率最高的词。1.0:生成结果更具随机性和创造性。默认为:0.7\n
      frequencyPenalty:控制生成文本中重复内容的惩罚程度。0.0:不惩罚重复内容。1.0 或更高:减少重复内容,增加多样性。默认为:0.5\n
      user:标识请求的用户。用于跟踪和日志记录,通常用于区分不同用户的请求。默认为:user\n
      topP:控制生成文本时选择词的范围。0.7:从概率最高的 70% 的词中选择。1.0:不限制选择范围。默认为:0.7\n
      maxCompletionTokens:控制模型生成的文本的最大长度。这对于防止生成过长的文本或确保响应在预期的范围内非常有用。默认为:1024\n`
  }


</script>
</body>
</html>

三、页面调试

启动项目后,访问:http://localhost:8080/

3.1 参数提示

点击参数提示
在这里插入图片描述

3.2 开始请求

在【URL + Prompt参数提示】的输入框中,追加你要问的问题。然后点击【开始请求】或直接【回车】。
在这里插入图片描述

3.3 手动断开

点击【断开连接】。将会停止回答。