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

【大模型应用开发】SpringBoot 整合基于 Ollama 的 DeepSeek,并对接前端( 全部代码 !!!)

文章目录

  • 1. 前述
  • 2. AI 基础对话
  • 3. AI 会话日志
  • 4. 前端样式
  • 5. 对接前端对话,并实现 AI 会话记忆
  • 7. 存在的问题及未来的改进

1. 前述


在前面的内容中,我们已经在本地部署了deepseek-r1:1.5b,详情可参考文章:
【大模型应用开发】Ollama 介绍及基于 Ollama 部署本地 DeepSeek

那么下面我们需要将该大模型整合到SpringBoot中,实现接口访问,同时对接前端代码。本文拥有全部的代码 !!!

2. AI 基础对话


在创建AI大模型机器人时,我们现需要了解几个重要概念

  • system:表示系统的默认设置,即给大模型设置任务背景
  • user:即用户的提问
  • assistant:大模型生成的历史消息

首先创建一个项目,其中Java的版本要在17以上

image-20250611160157100

下载相关依赖,其中SpringBoot版本不能过低,否则没有AI依赖,可按需选择deepseek或者openai

在这里插入图片描述

创建好项目后,编辑配置文件application.yaml

spring:application:name: SpringAIai:ollama:# ollama固定的本地访问地址base-url: http://localhost:11434chat:model: deepseek-r1:1.5b

SpringAI利用ChatClient来访问大模型,创建配置文件类CommonConfiguration

@Configuration
public class CommonConfiguration {@Beanpublic ChatClient chatClint(OllamaChatModel ollamaModel){return ChatClient.builder(ollamaModel).build();}
}

创建控制器ChatController,利用阻塞访问输出结果。所谓阻塞式访问,就是需要等待全部回答结束后才会展现给用户

// 用来实例化 chatClient
@RequiredArgsConstructor
@RestController
@RequestMapping("/ai")
public class ChatController {private final ChatClient chatClient;// 其中call表示阻塞式调用,即答案会一口气出来@RequestMapping("/chat")// prompt是提示词,就是发送给ai的问题public String chat(String prompt){return chatClient.prompt().user(prompt).call().content();}
}

启动springboot项目,此时要保证ollama在后台运行,在浏览器中测试访问

http://localhost:8080/ai/chat?prompt=你是谁?

image-20250611172403877

倘若需要使用流式输出(更常用,也就是符合现在的流行的回答方式),即回答是一个字一个字展示出来的,则可利用下面的方法

// 用来实例化 chatClient
@RequiredArgsConstructor
@RestController
@RequestMapping("/ai")
public class ChatController {private final ChatClient chatClient;// 流式访问,答案会一个字一个字的回答@RequestMapping(value = "/chat",produces = "text/html;charset=utf-8")public Flux<String> streamChat(String prompt){return chatClient.prompt().user(prompt).stream().content();}
}

再次测试访问即可

http://localhost:8080/ai/chat?prompt=你是谁?

于此同时,我们可以修改配置文件类CommonConfiguration,保留默认的系统设置

@Configuration
public class CommonConfiguration {@Beanpublic ChatClient chatClint(OllamaChatModel ollamaModel){return ChatClient.builder(ollamaModel).defaultSystem("你是一个计算机研究生,你叫作小新,请你以他的身份和我对话").build();}
}

此时,再次询问大模型你是谁,它便会按照默认的defaultSystem设置,回答你它是小新

3. AI 会话日志


日志的重要性不言而喻,SpringAI利用AOP原理提供了AI会话时的拦截、增强等功能,也就是Advisor

修改配置文件类CommonConfiguration

@Configuration
public class CommonConfiguration {@Beanpublic ChatClient chatClint(OllamaChatModel ollamaModel){return ChatClient.builder(ollamaModel)   //构建ai.defaultAdvisors(new SimpleLoggerAdvisor())  //日志记录.defaultSystem("你是一个计算机研究生,你叫作小新,请你以他的身份和口吻与我对话").build();}
}

并且修改application.yaml来设定需要日志记录的项目包位置

spring:application:name: SpringAIai:ollama:# ollama固定的本地访问地址base-url: http://localhost:11434chat:model: deepseek-r1:1.5b
logging:level:# 日志记录的项目包位置org.springframework.ai.chat.client.advisor: debugcom.yzx.springai: debug

再次测试访问,控制台可输出对应的日志信息

http://localhost:8080/ai/chat?prompt=今天天气非常好

image-20250612120508853

4. 前端样式


我用官网的DeepSeek生成了几个简单的前端页面,以及对应的样式,在项目中是这样的位置
在这里插入图片描述

前端我并不是很懂,所以下面代码可能并不是很完善,或者说存在部分缺陷,但可以运行,大家可自行修改。首先是style.css

:root {--primary-color: #6c63ff;--sidebar-width: 260px;
}
* {box-sizing: border-box;margin: 0;padding: 0;
}
body {font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;background-color: #f7f7f8;color: #333;display: flex;height: 100vh;overflow: hidden;
}
/* 侧边栏样式 */
.sidebar {width: var(--sidebar-width);background-color: #f0f0f0;border-right: 1px solid #e0e0e0;display: flex;flex-direction: column;height: 100%;
}
.new-chat-btn {margin: 10px;padding: 10px 15px;background-color: var(--primary-color);color: white;border: none;border-radius: 5px;cursor: pointer;display: flex;align-items: center;gap: 8px;font-size: 14px;
}
.new-chat-btn:hover {background-color: #5a52d6;
}
.new-chat-btn i {font-size: 16px;
}
.history-list {flex: 1;overflow-y: auto;padding: 10px;
}
.history-item {padding: 10px 12px;border-radius: 5px;margin-bottom: 5px;cursor: pointer;font-size: 14px;white-space: nowrap;overflow: hidden;text-overflow: ellipsis;
}
.history-item:hover {background-color: #e0e0e0;
}
.history-item.active {background-color: #d6d6ff;
}
/* 主聊天区域 */
.main-content {flex: 1;display: flex;flex-direction: column;height: 100%;
}
.chat-header {padding: 15px 20px;border-bottom: 1px solid #e0e0e0;background-color: white;font-weight: bold;
}
.chat-messages {flex: 1;overflow-y: auto;padding: 20px;background-color: white;
}
.message {max-width: 80%;margin-bottom: 20px;padding: 12px 16px;border-radius: 8px;line-height: 1.5;
}
.user-message {margin-left: auto;background-color: var(--primary-color);color: white;border-bottom-right-radius: 2px;
}
.ai-message {margin-right: auto;background-color: #f0f0f0;color: #333;border-bottom-left-radius: 2px;
}
.input-area {padding: 15px;border-top: 1px solid #e0e0e0;background-color: white;
}
.input-container {max-width: 800px;margin: 0 auto;display: flex;position: relative;
}
#user-input {flex: 1;padding: 12px 15px;border: 1px solid #ddd;border-radius: 20px;outline: none;font-size: 1em;padding-right: 50px;
}
#send-button {position: absolute;right: 5px;top: 5px;width: 36px;height: 36px;background-color: var(--primary-color);color: white;border: none;border-radius: 50%;cursor: pointer;display: flex;align-items: center;justify-content: center;
}
#send-button:hover {background-color: #5a52d6;
}
#send-button:disabled {background-color: #ccc;cursor: not-allowed;
}
/* 打字指示器 */
.typing-indicator {display: inline-block;padding: 10px 15px;background-color: #f0f0f0;border-radius: 18px;margin-bottom: 15px;
}
.typing-dot {display: inline-block;width: 8px;height: 8px;border-radius: 50%;background-color: #999;margin: 0 2px;animation: typingAnimation 1.4s infinite ease-in-out;
}
.typing-dot:nth-child(2) {animation-delay: 0.2s;
}
.typing-dot:nth-child(3) {animation-delay: 0.4s;
}
@keyframes typingAnimation {0%, 60%, 100% { transform: translateY(0); }30% { transform: translateY(-5px); }
}
/* 图标字体 */
.icon {display: inline-block;width: 1em;height: 1em;stroke-width: 0;stroke: currentColor;fill: currentColor;
}
/* 响应式调整 */
@media (max-width: 768px) {.sidebar {width: 220px;}
}/* 历史记录项样式 */
.history-item {cursor: pointer;display: flex;align-items: center;transition: background-color 0.2s;
}.history-item:hover .delete-chat-btn {opacity: 1;
}/* 删除按钮样式 */
.delete-chat-btn {margin-left: 8px;padding: 4px;border-radius: 4px;transition: all 0.2s;
}.delete-chat-btn:hover {background-color: rgba(255, 99, 71, 0.1);
}/* 标题和ID样式 */
.history-item-title {font-size: 1rem;white-space: nowrap;overflow: hidden;text-overflow: ellipsis;
}.history-item-id {font-size: 0.8rem;color: #666;
}

其次是app.js

// DOM元素
const chatMessages = document.getElementById('chat-messages');
const userInput = document.getElementById('user-input');
const sendButton = document.getElementById('send-button');
const newChatBtn = document.getElementById('new-chat-btn');
const historyList = document.getElementById('history-list');
const chatTitle = document.getElementById('chat-title');// 对话状态管理(不再使用localStorage)
let currentChatId = generateChatId();
let chats = {[currentChatId]: {title: '新对话',messages: [],createdAt: new Date().toISOString()}
};// 初始化
renderHistoryList();
displayChat(currentChatId);// 生成更随机的聊天ID(6位字母数字组合)
function generateChatId() {const chars = '0123456789';let result = '';for (let i = 0; i < 6; i++) {result += chars.charAt(Math.floor(Math.random() * chars.length));}return result;
}// 渲染历史记录列表(显示会话ID)
function renderHistoryList() {historyList.innerHTML = '';// 按创建时间倒序排列const sortedChats = Object.entries(chats).sort((a, b) =>new Date(b[1].createdAt) - new Date(a[1].createdAt));sortedChats.forEach(([id, chat]) => {const item = document.createElement('div');item.className = 'history-item';if (id === currentChatId) {item.classList.add('active');}const content = document.createElement('div');content.className = 'history-item-content';const title = document.createElement('span');title.className = 'history-item-title';title.textContent = chat.title || '新对话';const idSpan = document.createElement('span');idSpan.className = 'history-item-id';idSpan.textContent = ` #${id}`;content.appendChild(title);content.appendChild(idSpan);item.appendChild(content);item.addEventListener('click', () => {if (currentChatId !== id) {currentChatId = id;displayChat(id);document.querySelectorAll('.history-item').forEach(el =>el.classList.remove('active'));item.classList.add('active');}});// 添加删除按钮const deleteBtn = document.createElement('span');deleteBtn.className = 'history-item-delete';deleteBtn.innerHTML = '&times;';deleteBtn.addEventListener('click', (e) => {e.stopPropagation();if (confirm('确定要删除此对话吗?')) {delete chats[id];if (currentChatId === id) {currentChatId = generateChatId();chats[currentChatId] = {title: '新对话',messages: [],createdAt: new Date().toISOString()};}renderHistoryList();displayChat(currentChatId);}});item.appendChild(deleteBtn);historyList.appendChild(item);});
}// 显示指定聊天记录
function displayChat(chatId) {chatMessages.innerHTML = '';const chat = chats[chatId];chatTitle.textContent = chat.title;chat.messages.forEach(msg => {if (msg.role === 'user') {addUserMessage(msg.content, false); // 不触发重新渲染} else {addAIMessage(msg.content, false); // 不触发重新渲染}});chatMessages.scrollTop = chatMessages.scrollHeight;
}// 添加用户消息
function addUserMessage(message, saveToHistory = true) {const el = document.createElement('div');el.className = 'message user-message';el.textContent = message;chatMessages.appendChild(el);if (saveToHistory) {const chat = chats[currentChatId];chat.messages.push({role: 'user',content: message,timestamp: new Date().toISOString()});// 如果是第一条消息,设置为标题if (chat.messages.length === 1) {chat.title = message.length > 20? message.substring(0, 20) + '...': message;chatTitle.textContent = chat.title;renderHistoryList();}}chatMessages.scrollTop = chatMessages.scrollHeight;
}// 添加AI消息
function addAIMessage(message, saveToHistory = true) {const el = document.createElement('div');el.className = 'message ai-message';el.textContent = message;chatMessages.appendChild(el);if (saveToHistory && chats[currentChatId]) {chats[currentChatId].messages.push({role: 'assistant',content: message,timestamp: new Date().toISOString()});}chatMessages.scrollTop = chatMessages.scrollHeight;
}// 流式消息处理
function addAIMessageStream() {const el = document.createElement('div');el.className = 'message ai-message';chatMessages.appendChild(el);const typingIndicator = document.createElement('div');typingIndicator.className = 'typing-indicator';typingIndicator.innerHTML = `<span class="typing-dot"></span><span class="typing-dot"></span><span class="typing-dot"></span>`;chatMessages.appendChild(typingIndicator);chatMessages.scrollTop = chatMessages.scrollHeight;return {append: (text) => {if (typingIndicator.parentNode) {chatMessages.removeChild(typingIndicator);}el.textContent += text;chatMessages.scrollTop = chatMessages.scrollHeight;},complete: () => {if (typingIndicator.parentNode) {chatMessages.removeChild(typingIndicator);}if (chats[currentChatId]) {chats[currentChatId].messages.push({role: 'assistant',content: el.textContent,timestamp: new Date().toISOString()});}}};
}// 发送消息
async function sendMessage() {const prompt = userInput.value.trim();if (!prompt) return;userInput.value = '';userInput.disabled = true;sendButton.disabled = true;addUserMessage(prompt);const aiMessage = addAIMessageStream();try {const eventSource = new EventSource(`/ai/chat?prompt=${encodeURIComponent(prompt)}`);eventSource.onmessage = (e) => {if (e.data === '[DONE]') {eventSource.close();aiMessage.complete();} else {aiMessage.append(e.data);}};eventSource.onerror = () => {eventSource.close();aiMessage.append('\n\n【对话结束】');aiMessage.complete();};} catch (error) {aiMessage.append(`\n\n【错误: ${error.message}`);aiMessage.complete();} finally {userInput.disabled = false;sendButton.disabled = false;userInput.focus();}
}// 新建对话(修改后的核心功能)
newChatBtn.addEventListener('click', () => {currentChatId = generateChatId();chats[currentChatId] = {title: '新对话',messages: [],createdAt: new Date().toISOString()};displayChat(currentChatId);renderHistoryList();
});// 事件监听
sendButton.addEventListener('click', sendMessage);
userInput.addEventListener('keypress', (e) => {if (e.key === 'Enter') sendMessage();
});
userInput.focus();

最后是index.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>AI 聊天助手</title><link rel="stylesheet" href="/css/style.css">  <!-- 引用静态CSS -->
</head>
<body>
<!-- 侧边栏 -->
<div class="sidebar"><button class="new-chat-btn" id="new-chat-btn"><svg class="icon" viewBox="0 0 24 24"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>新对话</button><div class="history-list" id="history-list"><!-- 历史记录会在这里动态添加 --></div>
</div><!-- 主聊天区域 -->
<div class="main-content"><div class="chat-header" id="chat-title">新对话</div><div class="chat-messages" id="chat-messages"><!-- 消息会在这里动态添加 --></div><div class="input-area"><div class="input-container"><input type="text" id="user-input" placeholder="输入您的问题..." autocomplete="off"><button id="send-button"><svg class="icon" viewBox="0 0 24 24"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg></button></div></div>
</div>
<script src="/js/app.js"></script>  <!-- 引用静态JS -->
</body>
</html>

创建控制器,用于访问前端页面

@RequiredArgsConstructor
@Controller
@RequestMapping("/view")
public class IndexController {@GetMapping("/chat")public String chatPage() {return "index";  // 返回模板名称(自动查找templates/index.html)}}

此时,可以尝试启动项目,访问前端 http://localhost:8080/view/chat,大概就是下面这个样子,还挺那么一回事儿的

在这里插入图片描述

5. 对接前端对话,并实现 AI 会话记忆


正如前文所述,我们需要对话可以保留上次的记忆,则需要调整assistant,否则每次的对话都是一个新的开始

修改配置文件类CommonConfiguration,创建记忆仓库

@Configuration
public class CommonConfiguration {@Bean//记忆存储public ChatMemoryRepository chatMemoryRepository(){return new InMemoryChatMemoryRepository();};//记忆实例化@Beanpublic ChatMemory chatMemory(ChatMemoryRepository chatMemoryRepository){return MessageWindowChatMemory.builder().chatMemoryRepository(chatMemoryRepository).maxMessages(20).build();}//默认设置@Beanpublic ChatClient chatClint(OllamaChatModel ollamaModel, ChatMemory chatMemory){return ChatClient.builder(ollamaModel)   //构建ai.defaultAdvisors(new SimpleLoggerAdvisor(),//将记忆历史导入MessageChatMemoryAdvisor.builder(chatMemory).build())  //日志记录.defaultSystem("你是一个计算机研究生,你叫作小新").build();}
}

于此同时,为了记忆不混乱,还需要保证每次对话只保存到对应的记忆中

即每次对话产生一个对话id,只把当前历史保存到相应的记忆中,调整控制器ChatController,这里需要做出更多的修改

  1. 首先修改输出样式produces 以适配html
  2. 添加注解@RequestParam 用以接受相关参数
  3. String prompt用以接受用户输入问题, String chatId用于接收当前对话id
  4. 将当前对话id绑定到ChatMemory.CONVERSATION_ID
  5. 为了保证不和前端冲突,此时的访问url/ai/chat
@RequiredArgsConstructor
@RestController
@RequestMapping("/ai")
public class ChatController {private final ChatClient chatClient;// 流式接口(修正SSE格式)@GetMapping(value = "/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE)public Flux<ServerSentEvent<String>> streamChat(@RequestParam String prompt, @RequestParam String chatId) {return chatClient.prompt().user(prompt)//历史对话id.advisors(a->a.param(ChatMemory.CONVERSATION_ID, chatId)).stream().content().map(content -> ServerSentEvent.builder(content).build()).concatWithValues(ServerSentEvent.builder("[DONE]").build());}
}

与此同时,前端js部分也需要做出部分修改。当发送对话时,应该将随机生成的对话id一同发送到后端。这里修改很简单,找到sendMessage()方法,修改发送的请求参数,添加当前对话id

 const eventSource = new EventSource(`/ai/chat?prompt=${encodeURIComponent(prompt)}&chatId=${currentChatId}`);

此时的逻辑是,用户访问前端页面http://localhost:8080/view/chat,输入问题,点击发送后,请求将发送到后端的http://localhost:8080/ai/chat进行处理,最后将回答的问题发聩给前端。测试访问如下

在这里插入图片描述

7. 存在的问题及未来的改进


  • 我不是很懂前端,所以前端传递的id好像没有随机性?
  • 没有做历史记录功能,也没有持久化到数据库
  • 没有文件上传和解析功能
http://www.xdnf.cn/news/14120.html

相关文章:

  • TensorFlow 2.0 与 Python 3.11 兼容性
  • 查找PPT中引用的图表在哪个EXCEL文件中
  • 笔记本电脑安装win11哪个版本好_笔记本电脑安装win11专业版图文教程
  • Spring中观察者模式的应用
  • 【论文解读】AgentThink:让VLM在自动驾驶中学会思考与使用工具
  • sql列中数据通过逗号分割的集合,对其中的值进行全表查重
  • NAS 资源帖
  • STM32项目---汽车氛围灯
  • flowable工作流的学习demo
  • 【本地虚拟机】xshell连接虚拟机linux服务器
  • 云平台|Linux部分指令
  • 【Erdas实验教程】021:遥感图像辐射增强( 查找表拉伸)
  • NLP学习路线图(四十七):隐私保护
  • YOLOv8新突破:FASFFHead多尺度检测的极致探索
  • 【模板】埃拉托色尼筛法(埃氏筛)
  • Spring-rabbit重试消费源码分析
  • OCCT基础类库介绍:Modeling Data - 2D Geometry 3D Geometry Topology
  • Javascript和NodeJS异常捕获对比
  • C++基础算法————二分查找
  • 深度学习——基于卷积神经网络实现食物图像分类【1】(datalodar处理方法)
  • VMware虚拟机集群上部署HDFS集群
  • 达梦的三权分立安全机制
  • 【机器学习与数据挖掘实战 | 医疗】案例16:基于K-Means聚类的医疗保险的欺诈发现
  • 使用 Azure LLM Functions 与 Elasticsearch 构建更智能的查询体验
  • 【论文解读】OmegaPRM:MCTS驱动的自动化过程监督,赋能LLM数学推理新高度
  • C++包管理器vcpkg的使用
  • RK全志平台LCD设备调试思路
  • JDBC基础(1)
  • python使用milvus教程
  • 使用 Git 将本地仓库上传到 GitHub 仓库的完整指南