当前位置: 首页 > news >正文

SpringBoot + VUE3 +deepseek实现的简单的AI对话页面(无广告,不会员)

主要使用SpringBoot + VUE3 +deepseek实现的简单的AI对话页面

页面有点简单

主要写了三种方式
使用前在API_KEY替换成自己的key,模型可以使用V3或者R1

1.单纯的多轮对话

import okhttp3.*;import java.io.IOException;
import java.util.Objects;
import java.util.concurrent.TimeUnit;public class DeepSeekOkHttpClient implements DeepSeekClient {private static final String API_URL = "https://api.deepseek.com/v1/chat/completions";private static final String API_KEY = "sk-xxx"; // 请务必妥善保管API_KEYprivate static final MediaType JSON = MediaType.get("application/json; charset=utf-8");private final OkHttpClient client;// 设置超时常量private static final int CONNECT_TIMEOUT = 10; // 连接超时(秒)private static final int READ_TIMEOUT = 130;    // 读取超时(秒)private static final int WRITE_TIMEOUT = 30;   // 写入超时(秒)public DeepSeekOkHttpClient() {this.client = new OkHttpClient.Builder().connectTimeout(CONNECT_TIMEOUT, TimeUnit.SECONDS) // 设置连接超时.readTimeout(READ_TIMEOUT, TimeUnit.SECONDS)       // 设置读取超时.writeTimeout(WRITE_TIMEOUT, TimeUnit.SECONDS)     // 设置写入超时.build();}@Overridepublic String getChatCompletion(String userMessage) {String requestBody = String.format("{\"model\":\"deepseek-chat\",\"messages\":[{\"role\":\"user\",\"content\":\"%s\"}]}", userMessage);RequestBody body = RequestBody.create(requestBody, JSON);Request request = new Request.Builder().url(API_URL).addHeader("Authorization", "Bearer " + API_KEY).post(body).build();try (Response response = client.newCall(request).execute()) {if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);return Objects.requireNonNull(response.body()).string();} catch (IOException e) {e.printStackTrace();return null;}}
}

2.可以指定角色的对话

在这里指定

 messages.put(new JSONObject().put("role", "system").put("content", "用精简语言回答问题,但是要回答准确全面!"));

完整代码

import okhttp3.*;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.IOException;
import java.util.concurrent.TimeUnit;public class DeepSeekChatWithRole {private static final String API_URL = "https://api.deepseek.com/v1/chat/completions";private static final String API_KEY = "sk-xxx"; // 请妥善保管API_KEYprivate static final MediaType JSON = MediaType.get("application/json; charset=utf-8");// 设置超时常量private static final int CONNECT_TIMEOUT = 10; // 连接超时(秒)private static final int READ_TIMEOUT = 130;    // 读取超时(秒)private static final int WRITE_TIMEOUT = 30;   // 写入超时(秒)private final OkHttpClient client;public DeepSeekChatWithRole() {this.client = new OkHttpClient.Builder().connectTimeout(CONNECT_TIMEOUT, TimeUnit.SECONDS) // 设置连接超时.readTimeout(READ_TIMEOUT, TimeUnit.SECONDS)       // 设置读取超时.writeTimeout(WRITE_TIMEOUT, TimeUnit.SECONDS)     // 设置写入超时.build();}public String getChatCompletion(String userMessage) throws IOException {JSONArray messages = new JSONArray();messages.put(new JSONObject().put("role", "system").put("content", "用精简语言回答问题,但是要回答准确全面!"));messages.put(new JSONObject().put("role", "user").put("content", userMessage));JSONObject requestBody = new JSONObject().put("model", "deepseek-reasoner").put("messages", messages).put("temperature", 0.7);  // 控制回答的随机性(0-1)RequestBody body = RequestBody.create(requestBody.toString(), JSON);Request request = new Request.Builder().url(API_URL).addHeader("Authorization", "Bearer " + API_KEY).post(body).build();try (Response response = client.newCall(request).execute()) {if (!response.isSuccessful()) {throw new IOException("API 请求失败: " + response.code() + " - " + response.message());}return response.body().string();}}public static void main(String[] args) {DeepSeekChatWithRole chatClient = new DeepSeekChatWithRole();try {String response = chatClient.getChatCompletion("Java 的 HashMap 和 LinkedHashMap 有什么区别?");System.out.println("AI 回答: " + response);} catch (IOException e) {e.printStackTrace();}}
}

3.带上下文关联,指定角色回复
这里主要思路是:用redis存对话的历史,然后每次会话会获取生成一个随机的不重复的key,用这个key存这次会话的历史,在当前会话,每次进行对话时,都会传入key 作为获取历史的参数,以达到上下文关联的目的

import okhttp3.*;
import org.json.JSONArray;
import org.json.JSONObject;
import redis.clients.jedis.Jedis;import java.io.IOException;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;public class ChatRedisContext {private static final String API_URL = "https://api.deepseek.com/v1/chat/completions";private static final String API_KEY = "sk-xxx";private static final MediaType JSON = MediaType.get("application/json; charset=utf-8");// 超时设置private static final int CONNECT_TIMEOUT = 10;private static final int READ_TIMEOUT = 130;private static final int WRITE_TIMEOUT = 30;private final OkHttpClient client;private final String redisHost = "127.0.0.1"; // Redis 服务器地址private final int redisPort = 6379; // Redis 服务器端口private static final String ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";private static final SecureRandom random = new SecureRandom();public ChatRedisContext() {this.client = new OkHttpClient.Builder().connectTimeout(CONNECT_TIMEOUT, TimeUnit.SECONDS).readTimeout(READ_TIMEOUT, TimeUnit.SECONDS).writeTimeout(WRITE_TIMEOUT, TimeUnit.SECONDS).build();}private static class Message {String role;String content;public Message(String role, String content) {this.role = role;this.content = content;}public JSONObject toJson() {return new JSONObject().put("role", this.role).put("content", this.content);}}private void saveConversationHistory(String conversationKey, List<Message> history) {JSONArray messagesJson = new JSONArray();for (Message msg : history) {messagesJson.put(msg.toJson());}try (Jedis jedis = new Jedis(redisHost, redisPort)) {// 设置键值并设置过期时间为 12 小时(43200 秒)jedis.setex(conversationKey, 43200, messagesJson.toString());}}private List<Message> getConversationHistory(String conversationKey) {try (Jedis jedis = new Jedis(redisHost, redisPort)) {String historyJson = jedis.get(conversationKey);List<Message> history = new ArrayList<>();if (historyJson != null) {JSONArray messagesJson = new JSONArray(historyJson);for (int i = 0; i < messagesJson.length(); i++) {JSONObject msgJson = messagesJson.getJSONObject(i);history.add(new Message(msgJson.getString("role"), msgJson.getString("content")));}}return history;}}public String chat(String conversationKey, String userMessage) throws IOException {List<Message> conversationHistory = getConversationHistory(conversationKey);if (conversationHistory.isEmpty()) {conversationHistory.add(new Message("system", "用精简语言回答问题,但是要回答准确全面!"));}conversationHistory.add(new Message("user", userMessage));JSONArray messages = new JSONArray();for (Message msg : conversationHistory) {messages.put(msg.toJson());}JSONObject requestBody = new JSONObject().put("model", "deepseek-chat").put("messages", messages).put("temperature", 0.7);RequestBody body = RequestBody.create(requestBody.toString(), JSON);Request request = new Request.Builder().url(API_URL).addHeader("Authorization", "Bearer " + API_KEY).post(body).build();try (Response response = client.newCall(request).execute()) {if (!response.isSuccessful()) {throw new IOException("API 请求失败: " + response.code() + " - " + response.message());}String responseBody = response.body().string();JSONObject jsonResponse = new JSONObject(responseBody);String aiResponse = jsonResponse.getJSONArray("choices").getJSONObject(0).getJSONObject("message").getString("content");conversationHistory.add(new Message("assistant", aiResponse));saveConversationHistory(conversationKey, conversationHistory);return aiResponse;}}public static String generateUniqueKey() {long timestamp = System.currentTimeMillis();StringBuilder randomString = new StringBuilder();for (int i = 0; i < 8; i++) { // 生成8个随机字母int index = random.nextInt(ALPHABET.length());randomString.append(ALPHABET.charAt(index));}return timestamp + "_" + randomString.toString();}public void clearConversation(String conversationKey) {try (Jedis jedis = new Jedis(redisHost, redisPort)) {if (null != conversationKey && jedis.exists(conversationKey)){jedis.del(conversationKey);}}}}

普通对话我还多写了一个接口方法,其实也不需要的

/*** <描述>** @author lj* @date 2025/5/26  9:34*/
public interface DeepSeekClient {String getChatCompletion(String userMessage);}

主要的实现类

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.zkzm.dpDemo.client.*;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;import java.io.IOException;
import java.util.HashMap;
import java.util.Map;/*** <描述>** @author lj* @date 2025/5/26  9:35*/
@RestController
public class DpController {public static void main(String[] args) {DeepSeekClient deepSeekClient = new DeepSeekOkHttpClient();String userMessage = "你好,你是谁?";String response = deepSeekClient.getChatCompletion(userMessage);System.out.println("Response: " + response);}/*** 普通多轮对话* */@GetMapping("getText")public ResponseEntity<Map<String, Object>> getText(String message) {DeepSeekClient deepSeekClient = new DeepSeekOkHttpClient();Map<String, Object> responseMap = new HashMap<>();try {String result = deepSeekClient.getChatCompletion(message);JSONObject nestedJson = JSON.parseObject(result);responseMap.put("message", nestedJson);responseMap.put("status", "success");return ResponseEntity.ok(responseMap);} catch (Exception e) {responseMap.put("message", "Error: " + e.getMessage());responseMap.put("status", "error");return ResponseEntity.status(500).body(responseMap);}}/*** 带角色对话* */@GetMapping("getTextWithRole")public String getTextWithRole(String message) throws IOException {DeepSeekChatWithRole client = new DeepSeekChatWithRole();String response = client.getChatCompletion(message);return response;}@GetMapping("getTextWithContext")public String getTextWithContext(String message) throws IOException {DeepSeekChatWithContext client = new DeepSeekChatWithContext();String response = client.chat(message);return response;}/*** 基于redis的上下文* */@GetMapping("getChatRedisContext")public String getChatRedisContext(String message,String conversationKey) throws IOException {ChatRedisContext chatClient = new ChatRedisContext();String response = chatClient.chat(conversationKey, message);return response;}/*** 生成redis的key* */@GetMapping("getKey")public String getKey() throws IOException {return ChatRedisContext.generateUniqueKey();}/*** 清空redis的key* */@GetMapping("clearConversation")public void clearConversation(String conversationKey) throws IOException {ChatRedisContext chatClient = new ChatRedisContext();chatClient.clearConversation(conversationKey);}
}

pom.xml

  <dependency><groupId>com.squareup.okhttp3</groupId><artifactId>okhttp</artifactId><version>4.9.2</version></dependency><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.83</version></dependency><!-- https://mvnrepository.com/artifact/org.json/json --><dependency><groupId>org.json</groupId><artifactId>json</artifactId><version>20240303</version></dependency><dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId><version>4.3.1</version> <!-- 确保使用的是最新版本 --></dependency>

接下来是前端代码,主要使用的是第三种方式(基于redis实现上下文)

1.每次页面加载,会默认创建一个会话,即调用获取key的接口
2.每次新增一个会话都会调用获取key的接口,确保每个会话的key是唯一的,达到上下文的目的
3.删除会话,会调用后台删除key的接口,同步删除了redis中的key

完整代码
.vue

<template><div id="app"><div class="sidebar"><h2>会话列表</h2><ul><li v-for="(session, index) in sessions" :key="index"><span @click="loadSession(index)">{{ session.title }}</span><button @click="deleteSession(index)">删除</button></li></ul><button @click="startNewSession" :disabled="sessions.length >= 10">+ 新会话</button></div><div class="chat-container"><div class="chat-title">{{ currentSession.title }}</div><div class="messages" ref="messagesContainer"><div v-for="(msg, index) in currentSession.messages" :key="index" class="message" :class="msg.role"><span class="role">{{ msg.role }}:</span><span class="content">{{ msg.content }}</span></div><div v-if="currentSession.loading" class="message assistant"><span class="role">小助手:</span><span class="content">加载中...</span></div><div v-if="currentSession.timeout && !currentSession.loading" class="message assistant"><span class="role">小助手:</span><span class="content">请求超时,请稍后再试。</span></div></div><div class="input-container"><input v-model="userMessage" @keyup.enter="sendMessage" type="text" placeholder="输入你的消息..." /><button @click="sendMessage":disabled="!currentSession.conversationKey || currentSession.loading || currentSession.timeout">发送</button></div></div></div>
</template><script>
import { chat, getKey, clearConversation } from '@/api/main';
import { ref, nextTick, onMounted } from 'vue';export default {setup() {const userMessage = ref('');const sessions = ref([]); // 存储会话列表const currentSession = ref({ title: '请开始新的会话', messages: [], loading: false, timeout: false, conversationKey: '' }); // 当前会话const sendMessage = async () => {if (!userMessage.value.trim()) return;const params = {conversationKey: currentSession.value.conversationKey,message: userMessage.value,};currentSession.value.messages.push({ role: 'user', content: userMessage.value });userMessage.value = '';currentSession.value.loading = true;currentSession.value.timeout = false;const timeoutPromise = new Promise((_, reject) => {setTimeout(() => {currentSession.value.timeout = true;currentSession.value.loading = false;reject(new Error('请求超时'));}, 120000);});try {const response = await Promise.race([chat(params), timeoutPromise]);currentSession.value.messages.push({ role: 'assistant', content: response.data });} catch (error) {console.error("发送消息失败:", error.message);if (error.message !== '请求超时') {currentSession.value.messages.push({ role: 'assistant', content: '请求失败,请重试。' });}} finally {currentSession.value.loading = false;nextTick(() => {const messagesContainer = document.querySelector('.messages');messagesContainer.scrollTop = messagesContainer.scrollHeight;});}};const startNewSession = async () => {if (sessions.value.length >= 10) {alert('最多只能创建10个会话!');return;}try {const key = await getKey();if (!key || !key.data) {console.error("获取的会话密钥无效");alert('无法创建会话,获取的密钥无效,请稍后再试。');return;}const newSession = {title: `会话 ${sessions.value.length + 1}`,messages: [],loading: false,timeout: false,conversationKey: key.data,};sessions.value.push(newSession);loadSession(sessions.value.length - 1);} catch (error) {console.error("获取会话密钥失败:", error.message);}};const loadSession = (index) => {currentSession.value = sessions.value[index];};const deleteSession = async (index) => {try {const params = {conversationKey: sessions.value[index].conversationKey}await clearConversation(params);sessions.value.splice(index, 1);if (sessions.value.length > 0) {loadSession(0);} else {currentSession.value = { title: '请开始新的会话', messages: [], loading: false, timeout: false, conversationKey: '' };}} catch (error) {console.error("删除会话失败:", error.message);}};onMounted(() => {startNewSession();});return {userMessage,sendMessage,sessions,currentSession,startNewSession,loadSession,deleteSession,};},
};
</script><style>
#app {margin-top: 50px;display: flex;justify-content: center; /* 水平居中 */height: 80%;width: 70%; /* 设置宽度 */
}.sidebar {width: 250px;border-right: 1px solid #ccc;padding: 10px;
}.chat-container {flex: 1;display: flex;flex-direction: column;padding: 10px;
}.chat-title {font-size: 24px;font-weight: bold;margin-bottom: 20px;
}.messages {flex: 1;overflow-y: auto; /* 允许垂直滚动 */margin-bottom: 10px;max-height: 500px; /* 设置最大高度 */
}.message {margin: 5px 0;
}.message.user {text-align: right;color: green;
}.message.assistant {margin: 10px;text-align: left;color: #333;
}.input-container {display: flex;
}input {flex: 1;padding: 10px;border: 1px solid #ccc;border-radius: 4px;height: 30px;
}button {padding: 10px;margin-left: 5px;border: none;background-color: #42b983;color: white;border-radius: 4px;cursor: pointer;
}button:disabled {background-color: #ccc;cursor: not-allowed;
}button:hover:not(:disabled) {background-color: #369e77;
}
</style>

.ts

import { get, post } from "@/config/request";interface ContextParams {conversationKey: string;message: string;
}export function chat(params: ContextParams) {return get("api/getChatRedisContext", params);
}export function getKey() {return get("api/getKey");
}export function clearConversation(params: ContextParams) {return get("api/clearConversation", params);
}

config

/* eslint-disable @typescript-eslint/no-explicit-any */
import axios, {type AxiosInstance,type AxiosRequestConfig,type AxiosResponse,
} from "axios";const commonurl = import.meta.env.VITE_APP_BASE_API;
const envType = import.meta.env.VITE_ENV_SET;
// const router = useRouter()
interface resType {code: number;msg: string;data: any;
}const getBaseUrl = (): string => {if (envType === "dev") {return commonurl;}return `https://api.example.com`;
};const instance = axios.create({baseURL: "",timeout: 120000,
});// 添加请求拦截器
instance.interceptors.request.use((config: AxiosRequestConfig): any => {// const { userDetil } = storeToRefs(userInfoStore());// const Authorization = userDetil.value.Authorization;// if (!Authorization && config.url?.indexOf("sys/login") === -1) {//   const herf = window.location.href;//   const baseUrl = window.location.href.split("#")[0];//   const url = `${baseUrl}#/login`;//   window.location.replace(url);//   showError("用户未登录请重新登录!");//   return {};// }config.headers = {"Content-Type": "application/json",// Authorization: String(Authorization),...config.headers,};return config;},(error: unknown): Promise<any> => {// 对请求错误做些什么return Promise.reject(error);}
);instance.interceptors.response.use((response: AxiosResponse<resType>): any => {const data = response.data;if (data.code === 501) {showError("账号密码错误!" + `系统提示:${data.msg}`);return Promise.reject();}if (data.code === 500) {showError(`系统提示:${data.msg}`);return Promise.reject();}return response;},(error: any) => {if (error.response &&(error.response.status === 501 ||error.response.status === 404 ||error.response.status === 401)) {const herf = window.location.href;const baseUrl = window.location.href.split("#")[0];const url = `${baseUrl}#/login`;window.location.replace(url);showError(error.message);return Promise.reject();}return Promise.reject(error);}
);
export const get = <T>(url: string, params?: any): any => {return instance.get<T>(url, { params });
};
export const post = <T>(url: string, data?: any): any => {return instance.post<T>(url, data);
};

这里的封装配置可能不同,可以自行使用,主要就是调用上面的几个接口

功能比较简陋,还在持续完善中,希望大佬多多指教(手动抱拳)

相关文章:

  • 用QT写一个车速表
  • 如何在 CentOS / RHEL 上修改 MySQL 默认数据目录 ?
  • 【从零开始学习QT】Qt 概述
  • mysql prepare statement
  • Flink 状态管理深度解析:类型与后端的全面探索
  • DrissionPage:重新定义Python网页自动化,让爬虫与浏览器控制合二为一
  • Spring AI Alibaba 发布企业级 MCP 分布式部署方案
  • day12 leetcode-hot100-19(矩阵2)
  • 中山大学无人机具身导航新突破!FlightGPT:迈向通用性和可解释性的无人机视觉语言导航
  • ICDMC 2025:创新媒体模式,迎接数字时代的挑战
  • SpringBoot+tabula+pdfbox解析pdf中的段落和表格数据
  • 算力卡上部署OCR文本识别服务与测试
  • 基于深度学习的工业OCR实践:仪器仪表数字识别技术详解
  • Tesseract OCR 安装与中文+英文识别实现
  • c++设计模式-单例模式
  • 【Microsoft 365可用】PPT一键取消所有超链接
  • 私有化部署DeepSeek后行业数据模型的训练步骤
  • “顶点着色器”和“片元着色器”是先处理完所有顶点再统一进入片元阶段,还是一个顶点处理完就去跑它的片元?
  • 说说线程有几种创建方式
  • 嵌入式自学第三十天(5.28)
  • 化妆品网站建设策划书/网站设计公司模板
  • 福建漳州网站建设公司/网络营销推广系统
  • 免费申请网站/seo推广的网站和平台有哪些
  • wordpress建站方法/最近营销热点
  • 贵阳专用网站建设/京津冀协同发展
  • 华为自助建站/成品人和精品人的区别在哪