SpringAI + DeepSeek本地大模型应用开发-智能会话
上篇我们已经完成了ollama+deepseek r1 本地部署,接下来进行spring ai + deepseek 实战吧;
目标:通过本地模型实现DeepSeek一样ai会话窗口
先看成果:

三步搞定:
1. 引入spring ai依赖
2. 完成模型yml配置
3. 应用开发
1、引入spring ai 相关依赖
- 我引的1.0.0-M6 稳定版
spring ai 要求jdk 版本最低1717、21都行,LTS版长期支持- spring ai 给ollama封装了starter,引就完事了
- spring ai相关内容bom 全搞定
<properties><java.version>17</java.version><spring-ai.version>1.0.0-M6</spring-ai.version>
</properties>
<!--ollama-->
<dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-ollama-spring-boot-starter</artifactId>
</dependency><dependencyManagement><dependencies><!--spring ai--><dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-bom</artifactId><version>${spring-ai.version}</version><type>pom</type><scope>import</scope></dependency></dependencies>
</dependencyManagement>
2、完成yml配置
- 配置ollama 地址,端口默认11434
- 配置聊天模型,我又装了deepseek-r1:7b
spring:ai:ollama:base-url: http://localhost:11434chat:model: deepseek-r1:7b
3、应用开发
- 创建ChatClient Bean
- 指定模型
- 指定系统角色 defaultSystem
- 初始化
@Beanpublic ChatClient chatClient(OllamaChatModel model) {return ChatClient.builder(model).defaultSystem("你是一只功夫熊猫,请以功夫熊猫的身份回答问题").build();}
- 模型调用-写个controller接口调用ChatClient
- 入参prompt就是提示词
- 引入chatclient
- 指定输出内容编码 utf-8
- stream 流式输出
@Resourceprivate ChatClient chatClient;@RequestMapping(value = "/chat",produces = "text/html;charset=utf-8")public Flux<String> chat(String prompt) {return chatClient.prompt().user(prompt).stream().content();}
- 发起会话,访问接口
- 这一步就说明本地模型已经访问成功了

4、聊天窗口- html
- 源码扔这了
- 直接保存本地双击访问就行了
- 这是我让DeepSeek模仿DeepSeek会话页面写的
- 配置访问本地localhost接口
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>KonfuPanda AI Chat</title><style>* {margin: 0;padding: 0;box-sizing: border-box;font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;}body {background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);min-height: 100vh;display: flex;flex-direction: column;align-items: center;padding: 20px;}.header {width: 100%;max-width: 800px;text-align: center;margin-bottom: 20px;}.logo {display: flex;align-items: center;justify-content: center;gap: 12px;margin-bottom: 10px;}.logo-icon {width: 32px;height: 32px;background: linear-gradient(135deg, #10a37f 0%, #0d8c6c 100%);border-radius: 6px;display: flex;align-items: center;justify-content: center;color: white;font-weight: bold;}.logo-text {font-weight: 700;font-size: 24px;background: linear-gradient(135deg, #10a37f 0%, #0d8c6c 100%);-webkit-background-clip: text;-webkit-text-fill-color: transparent;}.subtitle {color: #666;font-size: 14px;margin-bottom: 20px;}.chat-container {width: 100%;max-width: 800px;height: 65vh;background-color: white;border-radius: 16px;box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);display: flex;flex-direction: column;overflow: hidden;margin-bottom: 20px;}.chat-messages {flex: 1;overflow-y: auto;padding: 20px;display: flex;flex-direction: column;gap: 20px;}.message {display: flex;gap: 12px;max-width: 85%;}.user-message {align-self: flex-end;flex-direction: row-reverse;}.avatar {width: 36px;height: 36px;border-radius: 8px;display: flex;align-items: center;justify-content: center;font-weight: bold;color: white;flex-shrink: 0;margin-top: 4px;}.user-avatar {background: linear-gradient(135deg, #10a37f 0%, #0d8c6c 100%);}.assistant-avatar {background: linear-gradient(135deg, #6c757d 0%, #5a6268 100%);}.message-content {padding: 14px 18px;border-radius: 18px;line-height: 1.5;font-size: 15px;box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);white-space: pre-wrap;word-wrap: break-word;}.user-message .message-content {background: linear-gradient(135deg, #10a37f 0%, #0d8c6c 100%);color: white;border-bottom-right-radius: 6px;}.assistant-message .message-content {background-color: #f8f9fa;color: #333;border-bottom-left-radius: 6px;border: 1px solid #f0f0f0;}.input-container {width: 100%;max-width: 800px;background-color: white;border-radius: 16px;box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);padding: 20px;}.input-wrapper {display: flex;gap: 12px;align-items: flex-end;}.input-box {flex: 1;border: 1px solid #e0e0e0;border-radius: 12px;padding: 14px 18px;resize: none;font-size: 15px;line-height: 1.5;max-height: 120px;overflow-y: auto;transition: all 0.3s;}.input-box:focus {outline: none;border-color: #10a37f;box-shadow: 0 0 0 2px rgba(16, 163, 127, 0.1);}.send-button {background: linear-gradient(135deg, #10a37f 0%, #0d8c6c 100%);color: white;border: none;border-radius: 12px;padding: 14px 24px;cursor: pointer;font-weight: 500;transition: all 0.3s;box-shadow: 0 4px 12px rgba(16, 163, 127, 0.3);}.send-button:hover {transform: translateY(-2px);box-shadow: 0 6px 16px rgba(16, 163, 127, 0.4);}.send-button:disabled {background: #a0a0a0;cursor: not-allowed;transform: none;box-shadow: none;}.typing-indicator {display: flex;gap: 6px;padding: 14px 18px;background-color: #f8f9fa;border-radius: 18px;border-bottom-left-radius: 6px;width: fit-content;border: 1px solid #f0f0f0;}.typing-dot {width: 10px;height: 10px;border-radius: 50%;background-color: #a0a0a0;animation: typing 1.4s infinite ease-in-out;}.typing-dot:nth-child(1) {animation-delay: -0.32s;}.typing-dot:nth-child(2) {animation-delay: -0.16s;}@keyframes typing {0%, 80%, 100% {transform: scale(0.8);opacity: 0.5;}40% {transform: scale(1);opacity: 1;}}.welcome-message {text-align: center;margin: 20px 0;color: #666;font-size: 14px;}/* 滚动条样式 */.chat-messages::-webkit-scrollbar {width: 6px;}.chat-messages::-webkit-scrollbar-track {background: #f1f1f1;border-radius: 10px;}.chat-messages::-webkit-scrollbar-thumb {background: #c1c1c1;border-radius: 10px;}.chat-messages::-webkit-scrollbar-thumb:hover {background: #a8a8a8;}/* 响应式设计 */@media (max-width: 768px) {body {padding: 10px;}.chat-container {height: 70vh;}.message {max-width: 90%;}.input-wrapper {flex-direction: column;}.send-button {width: 100%;}}/* 流式输出光标效果 */.streaming-cursor {display: inline-block;width: 8px;height: 20px;background-color: #10a37f;margin-left: 2px;animation: blink 1s infinite;vertical-align: middle;}@keyframes blink {0%, 50% { opacity: 1; }51%, 100% { opacity: 0; }}</style>
</head>
<body><div class="header"><div class="logo"><div class="logo-icon">AI</div><div class="logo-text">KonfuPanda Chat</div></div><div class="subtitle">与AI助手进行智能对话(流式输出)</div></div><div class="chat-container"><div class="chat-messages" id="chat-messages"><div class="message assistant-message"><div class="avatar assistant-avatar">AI</div><div class="message-content">你好!我是AI助手,基于DeepSeek技术构建。我可以帮助你解答问题、提供信息、协助思考等各种任务。有什么我可以帮助你的吗?</div></div></div></div><div class="welcome-message">输入消息开始对话,按Enter发送,Shift+Enter换行</div><div class="input-container"><div class="input-wrapper"><textarea id="message-input" class="input-box" placeholder="输入消息..." rows="1"></textarea><button id="send-button" class="send-button">发送</button></div></div><script>document.addEventListener('DOMContentLoaded', function() {const chatMessages = document.getElementById('chat-messages');const messageInput = document.getElementById('message-input');const sendButton = document.getElementById('send-button');// 固定配置const API_URL = "http://localhost:8080/ai/chat";const CHAT_ID = "chat-session-" + Date.now();// 自动调整输入框高度messageInput.addEventListener('input', function() {this.style.height = 'auto';this.style.height = (this.scrollHeight) + 'px';});// 发送消息函数function sendMessage() {const message = messageInput.value.trim();if (!message) return;// 添加用户消息到聊天窗口addMessage(message, 'user');// 清空输入框messageInput.value = '';messageInput.style.height = 'auto';// 禁用发送按钮sendButton.disabled = true;// 显示正在输入指示器showTypingIndicator();// 调用流式APIcallStreamingAPI(message);}// 添加消息到聊天窗口function addMessage(content, sender) {const messageDiv = document.createElement('div');messageDiv.className = `message ${sender}-message`;const avatarDiv = document.createElement('div');avatarDiv.className = `avatar ${sender}-avatar`;avatarDiv.textContent = sender === 'user' ? '你' : 'AI';const contentDiv = document.createElement('div');contentDiv.className = 'message-content';contentDiv.textContent = content;messageDiv.appendChild(avatarDiv);messageDiv.appendChild(contentDiv);chatMessages.appendChild(messageDiv);// 滚动到底部chatMessages.scrollTop = chatMessages.scrollHeight;return contentDiv; // 返回内容元素以便后续更新}// 显示正在输入指示器function showTypingIndicator() {const typingDiv = document.createElement('div');typingDiv.className = 'message assistant-message';typingDiv.id = 'typing-indicator';const avatarDiv = document.createElement('div');avatarDiv.className = 'avatar assistant-avatar';avatarDiv.textContent = 'AI';const contentDiv = document.createElement('div');contentDiv.className = 'typing-indicator';for (let i = 0; i < 3; i++) {const dot = document.createElement('div');dot.className = 'typing-dot';contentDiv.appendChild(dot);}typingDiv.appendChild(avatarDiv);typingDiv.appendChild(contentDiv);chatMessages.appendChild(typingDiv);// 滚动到底部chatMessages.scrollTop = chatMessages.scrollHeight;}// 移除正在输入指示器function removeTypingIndicator() {const typingIndicator = document.getElementById('typing-indicator');if (typingIndicator) {typingIndicator.remove();}}// 流式API调用函数 - 修复406错误async function callStreamingAPI(prompt) {try {// 构建GET请求URLconst url = `${API_URL}?prompt=${encodeURIComponent(prompt)}&chatId=${encodeURIComponent(CHAT_ID)}`;console.log('请求URL:', url); // 调试用const response = await fetch(url, {method: 'GET',headers: {// 根据您的接口produces设置,使用text/html;charset=utf-8'Accept': 'text/html;charset=utf-8',// 或者尝试其他可能的Accept类型// 'Accept': 'text/plain;charset=utf-8',// 'Accept': 'application/json',}});console.log('响应状态:', response.status); // 调试用if (!response.ok) {throw new Error(`HTTP错误: ${response.status} - ${response.statusText}`);}// 移除正在输入指示器removeTypingIndicator();// 创建流式消息容器const messageContainer = addMessage('', 'assistant');// 添加流式光标const cursor = document.createElement('span');cursor.className = 'streaming-cursor';messageContainer.appendChild(cursor);// 处理流式响应const reader = response.body.getReader();const decoder = new TextDecoder('utf-8');let accumulatedText = '';// 移除初始光标cursor.remove();while (true) {const { done, value } = await reader.read();if (done) {// 流式输出完成sendButton.disabled = false;break;}// 解码并处理数据块const chunk = decoder.decode(value, { stream: true });accumulatedText += chunk;// 更新消息内容messageContainer.textContent = accumulatedText;// 滚动到底部chatMessages.scrollTop = chatMessages.scrollHeight;}} catch (error) {console.error('API调用错误:', error); // 调试用removeTypingIndicator();addMessage(`调用API时出错: ${error.message}`, 'assistant');sendButton.disabled = false;}}// 备选方案:如果流式API仍然有问题,使用普通APIasync function callRegularAPI(prompt) {try {const url = `${API_URL}?prompt=${encodeURIComponent(prompt)}&chatId=${encodeURIComponent(CHAT_ID)}`;const response = await fetch(url, {method: 'GET',headers: {'Accept': 'text/html;charset=utf-8',}});if (!response.ok) {throw new Error(`HTTP错误: ${response.status}`);}const text = await response.text();removeTypingIndicator();addMessage(text, 'assistant');sendButton.disabled = false;} catch (error) {removeTypingIndicator();addMessage(`调用API时出错: ${error.message}`, 'assistant');sendButton.disabled = false;}}// 发送按钮点击事件sendButton.addEventListener('click', sendMessage);// 输入框回车事件messageInput.addEventListener('keydown', function(e) {if (e.key === 'Enter' && !e.shiftKey) {e.preventDefault();sendMessage();}});// 聚焦输入框messageInput.focus();});</script>
</body>
</html>
5、发起会话
好了 开始玩吧

