文章目录
- 前言
- 正文
- 一、项目环境
- 二、项目代码
- 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:
# 硅基流动的url
base-url: https://api.siliconflow.cn/v1
# 秘钥(你自己申请的)
api-key: sk-ezcxadqecocxisa
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 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 {
@Resource
private DeepSeekClient deepSeekClient;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(aiChatWebSocketHandler(), "/ws/chat")
.setAllowedOrigins("*");
}
@Bean
public 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);
}
@Override
public 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();
}
}
@Override
protected 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();
}
@Override
public 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的文本对话接口。因此后续可能不会继续完善了!!