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

JDK21 基于 Spring-AI 集成大模型实现聊天机器人

基于 Spring-AI 集成大模型实现聊天机器人

  • 1. Ollama 本地化大模型服务平台
  • 2.Spring 项目搭建
    • 2.1启动类
    • 2.2 聊天接口
    • 2.4 大模型客户端配置类
    • 2.5 配置文件
  • 3.测试
    • 3.1 Html 测试页面
    • 3.2 聊天测试

1. Ollama 本地化大模型服务平台

使用 Ollama 搭建本地的 Deepseek

在这里插入图片描述

2.Spring 项目搭建

项目结构

在这里插入图片描述

依赖包

<?xml version="1.0" encoding="UTF-8"?>
<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.example</groupId><artifactId>spring-ai</artifactId><version>1.0-SNAPSHOT</version><properties><maven.compiler.source>21</maven.compiler.source><maven.compiler.target>21</maven.compiler.target><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><spring-boot.version>3.5.3</spring-boot.version><spring-ai.version>1.0.0</spring-ai.version></properties><dependencyManagement><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-dependencies</artifactId><version>${spring-boot.version}</version><type>pom</type><scope>import</scope></dependency><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><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-webflux</artifactId></dependency><dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-starter-model-ollama</artifactId></dependency></dependencies></project>

2.1启动类

package org.example;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplication
@AutoConfiguration
public class SpringAIApp {public static void main(String[] args) {SpringApplication.run(SpringAIApp.class, args);}
}

2.2 聊天接口

定义聊天接口,可以在基于配置信息,实现不同模型调用;并基于 Flux 实现流式响应

package org.example.controller;import jakarta.annotation.Resource;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.http.MediaType;
import org.springframework.http.codec.ServerSentEvent;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;import java.time.Duration;/*** @Author zhx && moon* @Since 21* @Date 2025-06-09 PM 5:30*/
@RestController
@RequestMapping("/api/ai")
@CrossOrigin(origins = "*", allowedHeaders = "*")
public class AIController {@Resource(name = "ollamaChatClient")private ChatClient client; // 自动注入OpenAI客户端@GetMapping(value = "/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE)public Flux<ServerSentEvent<String>> generateText(@RequestParam("message") String message) {Flux<String> aiResponse = client.prompt(message).stream().content().filter(x -> {// 过滤掉思考信号和空字符串return !"<think>".equals(x) && !"</think>".equals(x) ;});// 2. 转换为ServerSentEvent流return aiResponse.map(data -> ServerSentEvent.<String>builder().event("message").data(data).build()).concatWithValues(ServerSentEvent.<String>builder().event("complete") // 添加自定义的结束事件.data("STREAM_COMPLETED").build());}@GetMapping(value = "/test", produces = MediaType.TEXT_EVENT_STREAM_VALUE)public Flux<String> test(@RequestParam("message") String message) {String[] messages = {"启动服务", "加载配置", "连接数据库", "初始化完成"};return Flux.interval(Duration.ofSeconds(3)).take(messages.length).map(i -> "[LOG] " + messages[i.intValue()]);}
}

2.4 大模型客户端配置类

大模型客户端,定义聊天客户端,用于与大模型交互,使 Springboot 可以高效集成不同模型。用于配置模型基础信息和参数,如果用其他模型,则可以创建对应类。可以统一管理,用于动态指定聊天模型。

package org.example.config;import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.ollama.OllamaChatModel;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;/*** @Author zhx && moon* @Since 21* @Date 2025-06-10 PM 1:55*/
@Component
public class OllAmaConfig {@Bean("ollamaChatClient")public ChatClient ollamaChatClient(OllamaChatModel model) {return ChatClient.create(model);}}

2.5 配置文件

spring:ai:chat:client:enabled: false## ollamaollama:base-url: http://127.0.0.1:11434api-key: test-none #${OPENAI_API_KEY:your-default-key}chat:completions-path: /api/chatoptions:model: deepseek-r1:1.5btemperature: 0.7max-tokens: 2000top-p: 0.9

3.测试

3.1 Html 测试页面

<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>AI聊天对话框</title><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"><style>* {margin: 0;padding: 0;box-sizing: border-box;font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;}body {background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);min-height: 100vh;display: flex;justify-content: center;align-items: center;padding: 20px;}.chat-container {width: 100%;max-width: 800px;height: 80vh;background: #0f3460;border-radius: 20px;box-shadow: 0 15px 25px rgba(0, 0, 0, 0.6);display: flex;flex-direction: column;overflow: hidden;position: relative;}.chat-header {background: linear-gradient(135deg, #e94560 0%, #b01e68 100%);color: white;padding: 15px 20px;display: flex;align-items: center;border-bottom: 1px solid rgba(255, 255, 255, 0.1);}.chat-header h2 {font-size: 1.5rem;font-weight: 600;margin-left: 10px;}.chat-body {flex: 1;padding: 20px;overflow-y: auto;background: rgba(0, 0, 0, 0.1);}.message {margin-bottom: 20px;max-width: 80%;animation: fadeIn 0.3s ease-out;}.user-message {margin-left: auto;}.bot-message {margin-right: auto;}.message-content {padding: 12px 18px;border-radius: 18px;line-height: 1.5;position: relative;word-wrap: break-word;}.user-message .message-content {background: linear-gradient(to right, #0075ff, #3d86fe);color: white;border-top-right-radius: 5px;}.bot-message .message-content {background: rgba(255, 255, 255, 0.1);color: white;border-top-left-radius: 5px;border: 1px solid rgba(255, 255, 255, 0.1);position: relative;}.bot-typing {opacity: 0.7;font-style: italic;}.message-sender {font-size: 0.8rem;color: rgba(255, 255, 255, 0.7);padding: 5px 15px;}.chat-footer {padding: 15px 20px;background: rgba(0, 0, 0, 0.2);border-top: 1px solid rgba(255, 255, 255, 0.1);display: flex;}.message-input {flex: 1;padding: 12px 20px;border-radius: 30px;border: none;background: rgba(255, 255, 255, 0.08);color: white;font-size: 1rem;outline: none;transition: all 0.3s ease;}.message-input:focus {background: rgba(255, 255, 255, 0.15);}.message-input::placeholder {color: rgba(255, 255, 255, 0.4);}.send-button {background: linear-gradient(to right, #e94560, #ee6e8e);color: white;border: none;border-radius: 50%;width: 50px;height: 50px;margin-left: 10px;cursor: pointer;display: flex;justify-content: center;align-items: center;transition: all 0.3s ease;}.send-button:hover {transform: scale(1.05);box-shadow: 0 5px 15px rgba(233, 69, 96, 0.4);}.send-button:active {transform: scale(0.95);}.connection-status {position: absolute;top: 15px;right: 20px;display: flex;align-items: center;color: rgba(255, 255, 255, 0.7);font-size: 0.8rem;}.status-dot {width: 8px;height: 8px;border-radius: 50%;margin-right: 5px;}.connected .status-dot {background: #00ff80;box-shadow: 0 0 8px #00ff80;}.disconnected .status-dot {background: #ff5c5c;}.status-text {margin-left: 5px;}@keyframes fadeIn {from {opacity: 0;transform: translateY(10px);}to {opacity: 1;transform: translateY(0);}}@keyframes pulse {0% { opacity: 0.5; }50% { opacity: 1; }100% { opacity: 0.5; }}.type-indicator {display: inline-flex;}.dot {width: 8px;height: 8px;background: rgba(255, 255, 255, 0.7);border-radius: 50%;margin: 0 2px;animation: pulse 1.5s infinite;}.dot:nth-child(2) {animation-delay: 0.2s;}.dot:nth-child(3) {animation-delay: 0.4s;}.timestamp {font-size: 0.7rem;color: rgba(255, 255, 255, 0.4);text-align: right;padding: 0 5px;}@media (max-width: 600px) {.chat-container {height: 90vh;border-radius: 15px;}.message {max-width: 90%;}.send-button {width: 45px;height: 45px;}}</style>
</head>
<body><div class="chat-container"><div class="chat-header"><i class="fas fa-robot fa-2x"></i><h2>AI 助手</h2><div class="connection-status disconnected"><div class="status-dot"></div><span class="status-text">离线</span></div></div><div class="chat-body" id="chatBody"><div class="message bot-message"><div class="message-sender">AI 助手</div><div class="message-content">您好!我是AI助手,您可以问我任何问题,我会尽力帮您解答!</div><div class="timestamp">现在</div></div></div><div class="chat-footer"><input type="text" class="message-input" id="messageInput" placeholder="输入您的消息..."><button class="send-button" id="sendButton"><i class="fas fa-paper-plane"></i></button></div></div><script>const chatBody = document.getElementById('chatBody');const messageInput = document.getElementById('messageInput');const sendButton = document.getElementById('sendButton');const connectionStatus = document.querySelector('.connection-status');let eventSource = null;let currentBotMessage = null;let lastActivity = new Date();// 更新连接状态function updateConnectionStatus(isConnected) {if (isConnected) {connectionStatus.classList.remove('disconnected');connectionStatus.classList.add('connected');connectionStatus.querySelector('.status-text').textContent = '在线';} else {connectionStatus.classList.remove('connected');connectionStatus.classList.add('disconnected');connectionStatus.querySelector('.status-text').textContent = '离线';}}// 创建新消息元素function createMessageElement(content, isUser = false) {const messageDiv = document.createElement('div');messageDiv.className = `message ${isUser ? 'user-message' : 'bot-message'}`;const senderDiv = document.createElement('div');senderDiv.className = 'message-sender';senderDiv.textContent = isUser ? '您' : 'AI 助手';const contentDiv = document.createElement('div');contentDiv.className = 'message-content';const timestamp = document.createElement('div');timestamp.className = 'timestamp';timestamp.textContent = formatTime(new Date());if (isUser) {contentDiv.textContent = content;} else {contentDiv.textContent = content || '';}messageDiv.appendChild(senderDiv);messageDiv.appendChild(contentDiv);messageDiv.appendChild(timestamp);return messageDiv;}// 创建打字中状态function createTypingIndicator() {const typingDiv = document.createElement('div');typingDiv.className = 'type-indicator';typingDiv.innerHTML = `<div class="dot"></div><div class="dot"></div><div class="dot"></div>`;return typingDiv;}// 发送用户消息function sendMessage() {const message = messageInput.value.trim();if (!message) return;// 清空输入框messageInput.value = '';// 创建用户消息并显示const userMessageElement = createMessageElement(message, true);chatBody.appendChild(userMessageElement);chatBody.scrollTop = chatBody.scrollHeight;// 创建AI消息占位符并显示const botMessageElement = createMessageElement('', false);botMessageElement.querySelector('.message-content').classList.add('bot-typing');const typingIndicator = createTypingIndicator();botMessageElement.querySelector('.message-content').appendChild(typingIndicator);chatBody.appendChild(botMessageElement);chatBody.scrollTop = chatBody.scrollHeight;currentBotMessage = botMessageElement.querySelector('.message-content');// 连接到SSE端点connectToSSE(message);}// 连接到SSE接口function connectToSSE(message) {// 关闭现有连接(如果有)if (eventSource) {eventSource.close();}// 更新最后活动时间lastActivity = new Date();// 创建新的SSE连接eventSource = new EventSource(`http://127.0.0.1:8080/api/ai/chat?message=${encodeURIComponent(message)}`);updateConnectionStatus(true);// 接收消息流eventSource.onmessage = function(event) {lastActivity = new Date();// 移除打字状态if (currentBotMessage.querySelector('.type-indicator')) {currentBotMessage.querySelector('.type-indicator').remove();currentBotMessage.classList.remove('bot-typing');}// 添加新内容currentBotMessage.textContent += event.data;chatBody.scrollTop = chatBody.scrollHeight;};// 连接错误处理eventSource.onerror = function() {// 状态2=主动关闭,状态0=正常结束console.info('eventSource.readyState:' + eventSource.readyState);eventSource.close();// updateConnectionStatus(false);// 移除打字状态并显示错误信息if (currentBotMessage.querySelector('.type-indicator')) {currentBotMessage.querySelector('.type-indicator').remove();currentBotMessage.textContent = "抱歉,连接服务器时出现错误,请重试!";currentBotMessage.classList.add('bot-typing');}};}// 格式化时间 (HH:MM)function formatTime(date) {const hours = date.getHours().toString().padStart(2, '0');const minutes = date.getMinutes().toString().padStart(2, '0');return `${hours}:${minutes}`;}// 初始化连接// function initSSEConnection() {//     // 创建一个欢迎消息//     eventSource = new EventSource('http://127.0.0.1:8080/api/ai/chat?message=你好');//     eventSource.onopen = () => {//         updateConnectionStatus(true);//         console.info('只测试连接,然后关闭')//         eventSource.close(); //     };//     eventSource.onerror = () => {//         updateConnectionStatus(false);//     };// }// 事件监听sendButton.addEventListener('click', sendMessage);messageInput.addEventListener('keypress', (e) => {if (e.key === 'Enter') {sendMessage();}});// 检查连接状态setInterval(() => {const now = new Date();const timeDiff = (now - lastActivity) / 1000; // 秒// 超过60秒没有活动,关闭连接if (eventSource && timeDiff > 60) {eventSource.close();eventSource = null;updateConnectionStatus(false);}}, 5000);// 页面加载时初始化window.addEventListener('load', () => {initSSEConnection();});</script>
</body>
</html>

3.2 聊天测试

启动 JAVA 服务

在这里插入图片描述

打开测试页面,并输入测试问题

在这里插入图片描述

http://www.lryc.cn/news/576725.html

相关文章:

  • 【智能协同云图库】智能协同云图库第三弹:基于腾讯云 COS 对象存储—开发图片模块
  • Leetcode 3598. Longest Common Prefix Between Adjacent Strings After Removals
  • [database] Closure computation | e-r diagram | SQL
  • 【LeetCode 热题 100】560. 和为 K 的子数组——(解法二)前缀和+哈希表
  • swift-22-面向协议编程、响应式编程
  • SpringSecurity6-oauth2-三方gitee授权-授权码模式
  • 加密货币:USDC和比特币有什么区别?
  • web3区块链-ETH以太坊
  • 代理模式 - Flutter中的智能替身,掌控对象访问的每一道关卡!
  • aws(学习笔记第四十八课) appsync-graphql-dynamodb
  • Docker错误问题解决方法
  • Keil MDK 的 STM32 开发问题:重定向 printf 函数效果不生效(Keil MDK 中标准库未正确链接)
  • 基于springboot+vue的数字科技风险报告管理系统
  • 现代 JavaScript (ES6+) 入门到实战(一):告别 var!拥抱 let 与 const,彻底搞懂作用域
  • 领域驱动设计(DDD)【23】之泛化:从概念到实践
  • 网络缓冲区
  • DOP数据开放平台(真实线上项目)
  • 马斯克的 Neuralink:当意念突破肉体的边界,未来已来
  • Word之电子章制作——1
  • 【编译原理】期末
  • 华为云Flexus+DeepSeek征文|利用华为云一键部署的Dify平台构建高效智能电商客服系统实战
  • Youtube双塔模型
  • C++共享型智能指针std::shared_ptr使用介绍
  • cocos creator 3.8 - 精品源码 - 挪车超人(挪车消消乐)
  • Neo4j无法建立到 localhost:7474 服务器的连接出现404错误
  • Linux基本命令篇 —— less命令
  • springboot+Vue驾校管理系统
  • matplotlib 绘制水平柱状图
  • 基于LQR控制器的六自由度四旋翼无人机模型simulink建模与仿真
  • 使用deepseek制作“喝什么奶茶”随机抽签小网页