SpringBoot接入DeepSeek(硅基流动版)+ 前端页面调试(WebSocket连接模式)
文章目录
- 前言
- 正文
- 一、项目环境
- 二、项目代码
- 2.1 pom.xml
- 2.2 DeepSeekController.java
- 2.3 启动类
- 2.4 logback-spring.xml
- 2.5 application.yaml
- 2.6 WebsocketConfig.java
- 2.7 AiChatWebSocketHandler.java
- 2.8 SaveChatSessionParamRequest.java
- 2.9 index.html
- 三、页面调试
- 3.1 主页
- 3.2 参数调整
- 3.3 问问题(看下效果)
- 四、遗留问题
前言
本文使用SpringBoot提供 WebSocket 对话功能。通过模拟对话的方式,来和DeepSeek进行交互。包含Java后端和一个简单的前端页面。
另见SSE模式的实现: SpringBoot接入DeepSeek(硅基流动版)+ 前端页面调试(SSE连接模式)
硅基流动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
- spring-boot-starter-websocket:2.7.7
项目结构如下:
二、项目代码
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><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-websocket</artifactId><version>2.7.7</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 lombok.extern.slf4j.Slf4j;
import org.pine.ai.client.request.SaveChatSessionParamRequest;
import org.pine.ai.config.AiChatWebSocketHandler;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;@Controller
@Slf4j
@RequestMapping("/deepseek")
public class DeepSeekController {/*** 保存会话参数*/@PostMapping(value = "/saveSessionParam")public ResponseEntity<Void> saveSessionParam(@RequestBody SaveChatSessionParamRequest request) {String sessionId = request.getSessionId();if (!StringUtils.hasText(sessionId)) {throw new IllegalArgumentException("sessionId is empty");}AiChatWebSocketHandler.putSessionParam(sessionId, "model", request.getModel());AiChatWebSocketHandler.putSessionParam(sessionId, "temperature", request.getTemperature());AiChatWebSocketHandler.putSessionParam(sessionId, "frequencyPenalty", request.getFrequencyPenalty());AiChatWebSocketHandler.putSessionParam(sessionId, "user", request.getUser());AiChatWebSocketHandler.putSessionParam(sessionId, "topP", request.getTopP());AiChatWebSocketHandler.putSessionParam(sessionId, "maxCompletionTokens", request.getMaxCompletionTokens());return new ResponseEntity<>(null, null, HttpStatus.OK);}
}
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:# 硅基流动的urlbase-url: https://api.siliconflow.cn/v1# 秘钥(你自己申请的)api-key: sk-ezcxadqecocxisaspring:main:allow-bean-definition-overriding: trueserver:tomcat:keep-alive-timeout: 30000 # 30秒空闲超时max-connections: 100 # 最大连接数uri-encoding: UTF-8servlet:encoding:charset: UTF-8force: trueenabled: truecompression:enabled: false # 禁用压缩(否则流式数据可能被缓冲)
2.6 WebsocketConfig.java
package org.pine.ai.config;import io.github.pigmesh.ai.deepseek.core.DeepSeekClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;import javax.annotation.Resource;@Configuration
@EnableWebSocket
public class WebsocketConfig implements WebSocketConfigurer {@Resourceprivate DeepSeekClient deepSeekClient;@Overridepublic void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {registry.addHandler(aiChatWebSocketHandler(), "/ws/chat").setAllowedOrigins("*");}@Beanpublic WebSocketHandler aiChatWebSocketHandler() {return new AiChatWebSocketHandler(deepSeekClient);}
}
2.7 AiChatWebSocketHandler.java
package org.pine.ai.config;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.ResponseFormatType;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;@Slf4j
public class AiChatWebSocketHandler extends TextWebSocketHandler {private static final Map<String, WebSocketSession> WEB_SOCKET_SESSION_MAP = new ConcurrentHashMap<>();private final DeepSeekClient deepSeekClient;public AiChatWebSocketHandler(DeepSeekClient deepSeekClient) {this.deepSeekClient = deepSeekClient;}public static void putSessionParam(String sessionId, String key, Object value) {WebSocketSession webSocketSession = WEB_SOCKET_SESSION_MAP.get(sessionId);if (webSocketSession == null) {throw new IllegalArgumentException("sessionId is not exist");}Map<String, Object> attributes = webSocketSession.getAttributes();attributes.put(key, value);}@Overridepublic void afterConnectionEstablished(WebSocketSession session) {WEB_SOCKET_SESSION_MAP.put(session.getId(), session);log.info("新连接: {}", session.getId());try {session.sendMessage(new TextMessage("sysLog=连接成功!"));session.sendMessage(new TextMessage("sysLog=sessionId:" + session.getId()));} catch (IOException e) {e.printStackTrace();}}@Overrideprotected void handleTextMessage(WebSocketSession session, TextMessage message) {String payload = message.getPayload();log.info("收到消息: {}", payload);Map<String, Object> attributes = session.getAttributes();String model = attributes.getOrDefault("model", "deepseek-ai/DeepSeek-R1").toString();Double temperature = (Double) attributes.getOrDefault("temperature", 0.7);Double frequencyPenalty = (Double) attributes.getOrDefault("frequencyPenalty", 0.5);String user = attributes.getOrDefault("user", "user").toString();Double topP = (Double) attributes.getOrDefault("topP", 0.7);Integer maxCompletionTokens = (Integer) attributes.getOrDefault("maxCompletionTokens", 1024);log.info("model: {}, temperature: {}, frequencyPenalty: {}, user: {}, topP: {}, maxCompletionTokens: {}", model, temperature, frequencyPenalty, user, topP, maxCompletionTokens);ChatCompletionRequest request = buildRequest(payload, model, temperature, frequencyPenalty, user, topP, maxCompletionTokens);deepSeekClient.chatFluxCompletion(request).doOnNext(responseContent -> {// 发送消息给客户端try {String content = responseContent.choices().get(0).delta().content();String reasoningContent = responseContent.choices().get(0).delta().reasoningContent();if (StringUtils.hasText(reasoningContent) || (reasoningContent != null && reasoningContent.contains("\n"))) {session.sendMessage(new TextMessage("reasonContent=" + reasoningContent));} else if (StringUtils.hasText(content) || (content != null && content.contains("\n"))) {session.sendMessage(new TextMessage("content=" + content));}if ("stop".equals(responseContent.choices().get(0).finishReason())) {session.sendMessage(new TextMessage("\n\n回答结束!"));}} catch (IOException e) {e.printStackTrace();}}).subscribe();}private ChatCompletionRequest buildRequest(String prompt,String model,Double temperature,Double frequencyPenalty,String user,Double topP,Integer maxCompletionTokens) {return 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).maxTokens(maxCompletionTokens)// 响应结果的格式。.responseFormat(ResponseFormatType.TEXT).build();}@Overridepublic void afterConnectionClosed(WebSocketSession session, CloseStatus status) {WEB_SOCKET_SESSION_MAP.remove(session.getId());log.info("连接关闭: {}", session.getId());}
}
2.8 SaveChatSessionParamRequest.java
package org.pine.ai.client.request;import lombok.Data;import java.io.Serializable;@Data
public class SaveChatSessionParamRequest implements Serializable {private String sessionId;private String model;private Double temperature;private Double frequencyPenalty;private String user;private Double topP;private Integer maxCompletionTokens;
}
2.9 index.html
<!DOCTYPE html>
<html>
<head><title>WebSocket DeepSeek Chat</title><style>body {font-family: Arial, sans-serif;background-color: #f4f4f9;margin: 0;padding: 20px;display: flex;flex-direction: column;align-items: center;}.input-button-container {display: flex;align-items: center;margin-bottom: 20px;width: 1060px;}#messageInput {width: 900px;padding: 10px;margin-right: 10px;border: 1px solid #ccc;border-radius: 5px;}button {padding: 10px 20px;border: none;border-radius: 5px;background-color: #007bff;color: white;cursor: pointer;margin-right: 10px;}button:hover {background-color: #0056b3;}.message-container {width: 500px;margin-top: 20px;border: 1px solid #ccc;border-radius: 5px;background-color: white;padding: 10px;overflow-y: auto;height: 200px;}.message-container p {margin: 5px 0;}#messages {border-color: #ccc;}#reasonMessages {border-color: #e89c10;}#sysLog {border-color: #ec1b1b;}.section-title {font-weight: bold;margin-top: 20px;}#paramConfigForm input[type = "text"] {width: 100%;padding: 10px;margin-left: 10px;border: 1px solid #ccc;border-radius: 5px;margin-top: 8px;}#paramConfigForm label {font-weight: bold;margin-top: 20px;}</style>
</head>
<body>
<div class="input-button-container"><input type="text" id="messageInput" placeholder="输入消息"/><button onclick="sendMessage()">发送</button>
</div><div style="display: flex;justify-content: flex-start;width: 1060px;align-items: center;"><button onclick="connectWebSocket()">连接</button><button style="background-color: #ec1b1b" onclick="disconnectWebSocket()">断开连接</button>
</div><div style="display: flex; width: 1100px;"><div style="width: 520px"><div class="section-title">思考过程:</div><div id="reasonMessages" class="message-container"></div></div><div style="width: 520px; margin-left: 20px;"><div class="section-title">正式回答:</div><div id="messages" class="message-container"></div></div>
</div><div style="width: 1100px;margin-top: 40px;"><div class="section-title">系统日志记录:</div><div id="sysLog" class="message-container" style="height: 100px;width: 1040px;"></div>
</div><div style="width: 1000px;margin-top: 40px;border: 1px solid #aba8a8;"><div class="section-title">参数配置:</div><div style="height: 700px;width: 900px;"><form id="paramConfigForm"><div style="margin-top: 20px;"><label for="sessionId">sessionId:</label><input type="text" id="sessionId" name="sessionId" required></div><div style="margin-top: 20px;"><label for="model">model:</label><input type="text" id="model" name="model" value="deepseek-ai/DeepSeek-R1"></div><div style="margin-top: 20px;"><label for="temperature">temperature:</label><input type="text" id="temperature" name="temperature" value="0.7"></div><div style="margin-top: 20px;"><label for="frequencyPenalty">frequencyPenalty:</label><input type="text" id="frequencyPenalty" name="frequencyPenalty" value="0.5"></div><div style="margin-top: 20px;"><label for="user">user:</label><input type="text" id="user" name="user" value="user"></div><div style="margin-top: 20px;"><label for="topP">topP:</label><input type="text" id="topP" name="topP" value="0.7"></div><div style="margin-top: 20px;"><label for="maxCompletionTokens">maxCompletionTokens:</label><input type="text" id="maxCompletionTokens" name="maxCompletionTokens" value="1024"></div><div style="margin-top: 20px;"><button type="button" onclick="submitParamConfigForm()">提交</button></div></form></div>
</div>
<script>let socket;/*** 连接websocket*/function connectWebSocket() {if (socket) {disconnectWebSocket()}// 根据实际服务地址修改const wsUri = "ws://localhost:8080/ws/chat";socket = new WebSocket(wsUri);socket.onopen = function (event) {appendMessage("系统: 连接已建立", "sysLog");};socket.onmessage = function (event) {let message = event.data.toString();console.log("收到消息:" + message);if (message.startsWith("content=")) {appendInlineMessage("messages", "" + message.substring(8))} else if (message.startsWith("reasonContent=")) {appendInlineMessage("reasonMessages", "" + message.substring(14))} else if (message.startsWith("sysLog=")) {appendMessage("系统: " + message.substring(7), "sysLog");}if (message.startsWith("sysLog=sessionId:")) {document.getElementById("sessionId").value = message.substring(17);}};socket.onclose = function (event) {appendMessage("系统: 连接已断开", "sysLog");};socket.onerror = function (error) {console.error("WebSocket错误:", error);appendMessage("系统: 连接发生错误", "sysLog");};}/*** 断开连接*/function disconnectWebSocket() {if (socket) {socket.close();}}/*** 发送消息*/function sendMessage() {const input = document.getElementById("messageInput");if (socket.readyState === WebSocket.OPEN) {socket.send(input.value);appendMessage("我: " + input.value, "sysLog");input.value = "";document.getElementById("reasonMessages").textContent = "";document.getElementById("messages").textContent = "";} else {alert("连接尚未建立");}}function appendMessage(message, divId) {const messagesDiv = document.getElementById(divId);const p = document.createElement("p");p.textContent = message;messagesDiv.appendChild(p);messagesDiv.scrollTop = messagesDiv.scrollHeight;}function appendInlineMessage(divId, messages) {const messagesDiv = document.getElementById(divId);messagesDiv.textContent += messages;messagesDiv.scrollTop = messagesDiv.scrollHeight;}// 初始化连接window.onload = connectWebSocket;/*** 提交参数配置表单*/function submitParamConfigForm() {// 获取表单元素const form = document.getElementById('paramConfigForm');const formData = new FormData(form);// 创建一个对象来存储表单数据const data = {};formData.forEach((value, key) => {data[key] = value;});// 将对象转换为 JSON 字符串const jsonData = JSON.stringify(data);// 使用 fetch API 发送 POST 请求fetch('http://localhost:8080/deepseek/saveSessionParam', {method: 'POST',headers: {'Content-Type': 'application/json'},body: jsonData}).then(response => {if (!response.ok) {throw new Error('Network response was not ok ' + response.statusText);}}).then(data => {console.log('Success:', data);// 处理成功响应alert("参数配置成功")}).catch((error) => {console.error('Error:', error);// 处理错误alert(error)});}</script>
</body>
</html>
三、页面调试
3.1 主页
启动SpringBoot项目后,访问页面:
http://localhost:8080/
3.2 参数调整
可以在主页的下方,进行基本参数的调整,一次调整对应的是当前的session。默认参数是输入框中显示的内容,【提交】表单后,调用deepseek时的基本参数就是你刚刚修改的内容了。
3.3 问问题(看下效果)
问:如何学习Java
四、遗留问题
页面中对换行、markdown的渲染还有些问题,但是这个页面的初衷只是为了调试deepseek的文本对话接口。因此后续可能不会继续完善了!!