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

Spring AI 开发本地deepseek对话快速上手笔记

Spring AI

Spring AI是一个旨在推进生成式人工智能应用程序发展的项目,Spring AI的核心目标是提供高度抽象化的组件,作为开发AI应用程序的基础,使得开发者能够以最少的代码改动便捷地交换和优化功能模块‌

在开发之前先得引入大模型,这里选择deepseek

至于导入deepseek,咱这里选用ollama 大模型工具来进行本地化部署和管理

ollama下载与启动

进入ollama官网:Ollama

下载对应版本

直接install

deepseek模型下载

下载deepseek模型,这里选择的是r1:8b版本的,各位可以根据自己的电脑配置进行选择

执行ollama run deepseek-r1:8b

看到显示了success即运行成功

spring依赖引入

         <!-- WebFlux 响应式支持 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-webflux</artifactId></dependency><!--ollama spring ai依赖--><dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-ollama-spring-boot-starter</artifactId><version>1.0.0-M6</version></dependency><!-- Swagger3-knife4j依赖 --><dependency><groupId>com.github.xiaoymin</groupId><artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId><version>4.5.0</version></dependency>

ChatbotController

简单对话直接返回
@Slf4j
@RestController
public class ChatBotController {//注入模型,配置文件中的模型,或者可以在方法中指定模型@Resourceprivate OllamaChatModel model;
@GetMapping("/chat")public String chat(@RequestParam("message") String message){String call = model.call(message);System.out.println(call);return call;}}

启动

postman请求

返回

流式对话响应
@Slf4j
@RestController
public class ChatBotController {
//注入模型,配置文件中的模型,或者可以在方法中指定模型@Resourceprivate OllamaChatModel model;
@Resourceprivate StringRedisTemplate stringRedisTemplate;//引入存储消息服务@Resourceprivate ChatService chatService;@GetMapping(value = "/streamChat", produces = "text/event-stream;charset=UTF-8")public Flux<String> streamChat(@RequestParam("message")String message,@RequestParam String sessionId){Long userId = UserHolder.getUser().getId();return Flux.concat(processAnswering(message, sessionId, userId));}/*** 处理回答阶段*/private Flux<String> processAnswering(String message, String sessionId, Long userId) {return buildPromptWithContext(sessionId, message).flatMapMany(prompt ->model.stream(prompt).index().map(tuple -> {// 第一个消息添加标识if (tuple.getT1() == 0L) {return "[ANSWER] " + tuple.getT2();}return tuple.getT2();})).doOnNext(content -> saveMessage(sessionId, userId, message, content)).delayElements(Duration.ofMillis(30));}/*** 保存消息到Redis和数据库(带事务)*/@Transactionalprotected void saveMessage(String sessionId, Long userId, String question, String answer) {
//        // 保存用户问题
//        ChatContent userMsg = new ChatContent();
//        userMsg.setSessionId(sessionId);
//        userMsg.setMessage(question);
//        chatService.save(userMsg);// 保存AI回答ChatContent aiMsg = new ChatContent();
//        aiMsg.setSessionId(sessionId);aiMsg.setReceiveUserId(userId);aiMsg.setSendUserId(Long.valueOf(sessionId));aiMsg.setMessage(answer);
//        aiMsg.setType("ASSISTANT");chatService.save(aiMsg);// 更新Redis上下文stringRedisTemplate.executePipelined((RedisCallback<Object>) connection -> {connection.lPush((CONTEXT_PREFIX + sessionId).getBytes(),question.getBytes(),answer.getBytes());connection.lTrim((CONTEXT_PREFIX + sessionId).getBytes(), 0, MAX_CONTEXT_LENGTH * 2 - 1);return null;});}
}

请求postman

返回

可以看到返回的数据为流式的

前端引入

UI

      <div class="bot-chat-container"><!-- 聊天消息区域 --><div class="bot-chat-messages" ref="messagesContainer"><div v-for="message in bot_messages" :key="message.id":class="['message', message.sender]"><div class="avatar"><img :src="message.sender === 'user' ? userAvatar : botAvatar" alt="avatar"></div><div class="bubble"><div class="content"  v-html="renderMarkdown(message.content)"></div><!--<div class="content" v-else>{{ message.content }}</div>--><div class="status"><span class="time">{{ message.timestamp }}</span><span v-if="message.loading" class="typing-indicator"><span class="dot"></span><span class="dot"></span><span class="dot"></span></span></div></div></div></div><!-- 输入区域 --><div class="bot-input-area"><textarea v-model="inputMessage"@keydown.enter.exact.prevent="sendMessage"placeholder="输入你的消息..."></textarea><button @click="sendMessage" :disabled="isSending"><span v-if="!isSending">发送</span><span v-else class="sending-indicator"></span></button></div></div>

函数typescript

const sendMessage = async () => {if (!inputMessage.value.trim() || isSending.value) return// 用户消息const userMsg: ChatMessage = {id: Date.now().toString(),content: inputMessage.value.trim(),sender: 'user',timestamp: new Date().toLocaleTimeString()}bot_messages.push(userMsg)// 机器人响应占位const botMsg: ChatMessage = {id: `bot-${Date.now()}`,content: '',sender: 'bot',timestamp: new Date().toLocaleTimeString(),loading: true}bot_messages.push(botMsg)inputMessage.value = ''isSending.value = true// scrollToBottom()try {const sessionId = crypto.randomUUID()// const eventSource = new EventSource(`api/bot/streamChat?message=${encodeURIComponent(userMsg.content)}`)// 发起带有 Authorization 头的流式请求await fetchEventSource(`api/streamChat?message=${encodeURIComponent(userMsg.content)}&sessionId=333`, {method: 'GET',   // 或 POST(需服务端支持)headers: {'Authorization': sessionStorage.getItem("token"),  // 注入认证头 :ml-citation{ref="8" data="citationList"}},onopen(response) {if (response.ok) return;  // 连接成功throw new Error('连接失败');},onmessage(event) {// 处理流式数据(与原 EventSource 逻辑相同)const index = bot_messages.findIndex(m => m.id === botMsg.id)if (index !== -1) {bot_messages[index].content += event.databot_messages[index].loading = falsebot_messages[index].parsedContent = renderMarkdown(bot_messages[index].content)// scrollToBottom()}},onerror(err) {console.error('流式请求异常:', err);}});eventSource.onmessage = (event) => {const index = bot_messages.findIndex(m => m.id === botMsg.id)if (index !== -1) {bot_messages[index].content += event.databot_messages[index].loading = falsebot_messages[index].parsedContent = renderMarkdown(bot_messages[index].content)// scrollToBottom()}}eventSource.onerror = () => {eventSource.close()isSending.value = false}} catch (error) {console.error('Error:', error)isSending.value = false}
}

css

.bot-chat-container {display: flex;flex-direction: column;height: 100vh;background: #f5f5f5;
}.bot-chat-messages {flex: 1;overflow-y: auto;padding: 20px;background: linear-gradient(180deg, #f0f2f5 0%, #ffffff 100%);
}.message {display: flex;margin-bottom: 20px;gap: 12px;
}.message.user {flex-direction: row-reverse;
}.avatar img {width: 40px;height: 40px;border-radius: 50%;box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}.bubble {max-width: 70%;position: relative;
}.bubble .content {padding: 12px 16px;border-radius: 12px;line-height: 1.5;font-size: 14px;
}.message.bot .content {background: white;border: 1px solid #e5e7eb;border-radius: 12px 12px 12px 4px;
}.message.user .content {background: #3875f6;color: white;border-radius: 12px 12px 4px 12px;
}.status {display: flex;align-items: center;gap: 8px;margin-top: 4px;font-size: 12px;color: #666;
}.message.user .status {justify-content: flex-end;
}.typing-indicator {display: inline-flex;gap: 4px;
}.dot {width: 6px;height: 6px;background: #999;border-radius: 50%;animation: bounce 1.4s infinite ease-in-out;
}.dot:nth-child(2) {animation-delay: 0.2s;
}.dot:nth-child(3) {animation-delay: 0.4s;
}@keyframes bounce {0%, 80%, 100% { transform: translateY(0); }40% { transform: translateY(-4px); }
}.bot-input-area {display: flex;gap: 12px;padding: 20px;border-top: 1px solid #e5e7eb;background: white;
}textarea {flex: 1;padding: 12px;border: 1px solid #e5e7eb;border-radius: 8px;resize: none;min-height: 44px;max-height: 120px;font-family: inherit;
}button {padding: 0 20px;background: #3875f6;color: white;border: none;border-radius: 8px;cursor: pointer;transition: opacity 0.2s;
}button:disabled {background: #a0aec0;cursor: not-allowed;
}.sending-indicator {display: inline-block;width: 20px;height: 20px;border: 2px solid #fff;border-top-color: transparent;border-radius: 50%;animation: spin 0.8s linear infinite;
}@keyframes spin {to { transform: rotate(360deg); }
}
.bubble :deep() pre {background: #f8f8f8;padding: 12px;border-radius: 6px;overflow-x: auto;
}.bubble :deep() code {font-family: 'JetBrains Mono', monospace;font-size: 14px;
}.bubble :deep() ul,
.bubble :deep() ol {padding-left: 20px;margin: 8px 0;
}.bubble :deep() blockquote {border-left: 4px solid #ddd;margin: 8px 0;padding-left: 12px;color: #666;
}

发送消息

至此简单的AI对话完成了

源码地址

后端:https://github.com/enjoykanyu/chat_serve

前端:https://github.com/enjoykanyu/kChat_web

觉得不错得话,可以帮点个star呀,感谢

若在执行部署过程中有任何问题,欢迎githup提issue

http://www.xdnf.cn/news/5871.html

相关文章:

  • 07_Java中的锁
  • 系统平衡与企业挑战
  • Tomcat与纯 Java Socket 实现远程通信的区别
  • 中国人工智能智能体研究报告
  • Linux的文件查找与压缩
  • 关于cleanRL Q-learning
  • Java集合框架详解与使用场景示例
  • MySQL 5.7在CentOS 7.9系统下的安装(下)——给MySQL设置密码
  • Android NDK 高版本交叉编译:为何无需配置 FLAGS 和 INCLUDES
  • org.slf4j.MDC介绍-笔记
  • 集成DHTMLX 预订排期调度组件实践指南:如何实现后端数据格式转换
  • web 自动化之 yaml 数据/日志/截图
  • Boundary Attention Constrained Zero-Shot Layout-To-Image Generation
  • 配置hadoop集群-启动集群
  • apache2的默认html修改
  • 【前端三剑客】Ajax技术实现前端开发
  • ETL 数据集成平台与数据仓库的关系及 ETL 工具推荐
  • 前端流行框架Vue3教程:15. 组件事件
  • kafka----初步安装与配置
  • PROFIBUS DP转ModbusTCP网关模块于污水处理系统的成功应用案例解读​
  • C++中的各式类型转换
  • 序列化和反序列化(hadoop)
  • RabbitMQ发布订阅模式深度解析与实践指南
  • 解决 CentOS 7 镜像源无法访问的问题
  • 爬虫请求频率应控制在多少合适?
  • cocos creator 3.8 下的 2D 改动
  • Kubernetes Horizontal Pod Autosscaler(HPA)核心机制解析
  • 【android bluetooth 框架分析 02】【Module详解 6】【StorageModule 模块介绍】
  • C#进阶(1) ArrayList
  • TDengine编译成功后的bin目录下的文件的作用