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 版本最低17 17、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:11434
      chat:
        model: deepseek-r1:7b

3、应用开发

  • 创建ChatClient Bean
  • 指定模型
  • 指定系统角色 defaultSystem
  • 初始化
@Bean
    public ChatClient chatClient(OllamaChatModel model) {
        return ChatClient
                .builder(model)
                .defaultSystem("你是一只功夫熊猫,请以功夫熊猫的身份回答问题")
                .build();
    }
  • 模型调用-写个controller接口调用ChatClient
  • 入参prompt就是提示词
  • 引入chatclient
  • 指定输出内容编码 utf-8
  • stream 流式输出
    @Resource
    private 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();
                
                // 调用流式API
                callStreamingAPI(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请求URL
                    const 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仍然有问题,使用普通API
            async 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、发起会话

好了 开始玩吧
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值