DeepSeek+SpringAI实战AI家庭医生应用-从开发到部署,打造智能医疗助手 Evergreen 2025年10月25日 ai大模型, aigc 预计阅读 4 分钟 ##### 学到 - Ollama 本地模型部署工具 - DeepSeek-R1 推理大模型 - SpringBoot3 pring AI - **PageAssist 谷歌浏览器UI插件** - ChatboxAI可视化UI软件 - CherryStudio本地Api调用工具 ##### 核心技术业务功能 - 本地部署大模型DeepSeek - Ollama 脚本Api使用、角色定制 - SpringBoot + Spring AI - 代码调用本地模型编程 - 模型会话数据记录持久化 ##### DeepSeek家庭医生 1. Ollama部署:部署、常用命令、DeepSeek提示词(角色、适用场景) 2. 可视化UI:PageAssist、ChatboxAI、Cherry Studio(硅基流动API) 3. 构建项目环境: JDK21、Maven、 MySQL8、Navicat、内网互通 4. 搭建项目架构:SpringBoot3 构建、Maven项目构建、初始化Controller控制器、配置文件 yaml、多环境配置 5. Spring AI:Spring AI 说明、Ollama调用DeepSeek模型、PostMan调用Ollama接口、Spring AI 集成 Ollama、同步调用 DeepSeek 大模型、流式调用 DeepSeek 大模型、**AOP切面统计模型调用时间**、**StopWatch 优化统计时长**、 6. SSE服务端推送事件: **定制 DeepSeek 角色扮演**、**SSE事件原理机制**、**构建SSE服务器**、**持续推送消息给客户端**、**前端SSE事件集成** 7. 数据持久化:数据库表设计、集成 MyBatis-Plus、配置数据库实体与映射对象、用户与大模型会话记录保存与展 8. 线上部署:采购算力云服务器、SSH客户端命令、Ollama部署大模型 DeepSeek、Docker部署MySQL与数据迁移、Maven打包与构建、项目发布 ##### 项目架构及技术选型 ##### 项目重点关键流程 依赖包 ```xml 21 21 UTF-8 org.springframework.boot spring-boot-starter-parent 3.3.8 org.springframework.boot spring-boot-starter org.springframework.boot spring-boot-starter-aop org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-configuration-processor org.springframework.boot spring-boot-starter-test org.projectlombok lombok io.springboot.ai spring-ai-ollama 1.0.3 io.springboot.ai spring-ai-ollama-spring-boot-starter 1.0.3 com.mysql mysql-connector-j 8.0.33 com.baomidou mybatis-plus-boot-starter 3.5.10.1 org.mybatis mybatis-spring org.mybatis mybatis-spring 3.0.4 deepseek-doctor org.springframework.boot spring-boot-maven-plugin ``` ##### AOP切面统计模型调用时间 ```java @Slf4j @Component @Aspect public class ServiceLogAspect { /** * @Description: 切面表达式 * * 返回任意类型,比如 void,object,list 等 * com.xxx.service.impl 指定包名,要去具体切入切面的位置(某个java class所在的包位置) * .. 可以匹配到当前包以及它的子包 * * 可以匹配当前包和子包下的java class * . 无意义 * * 代表任意的方法名 * (..) 代表方法名的参数,这个参数是可以被传入的,也可以无参数 */ @Around("execution(* com.xx.service.impl..*.*(..))") public Object recordTimeLog(ProceedingJoinPoint joinPoint) throws Throwable { StopWatch stopWatch = new StopWatch(); stopWatch.start(); Object proceed = joinPoint.proceed(); String point = joinPoint.getTarget().getClass().getName() + "." + joinPoint.getSignature().getName(); stopWatch.stop(); // 获得时间差 // 打印任务的耗时统计 log.info(stopWatch.prettyPrint()); log.info(stopWatch.shortSummary()); // 任务信息总览 log.info("所有任务的总耗时:" + stopWatch.getTotalTimeMillis()); log.info("任务总数:" + stopWatch.getTaskCount()); // log.info("直接方法:{} 执行的时间为 {} 毫秒", point, takeTime); return proceed; } } ``` ##### SSE服务端推送事件 - SSEServer服务 ```java import lombok.extern.slf4j.Slf4j; import org.springframework.util.CollectionUtils; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import java.io.IOException; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; @Slf4j public class SSEServer { /** * 使用map对象,关联用户id和sse的服务连接 * 进阶提问1:SseEmitter 能不能放在Redis中和userId进行关联? * 进阶提问2:SseEmitter 如何在集群SpringBoot中存在 */ private static Map sseClients = new ConcurrentHashMap<>(); /** * 用于统计当前总在线人数 */ private static AtomicInteger onlineCounts = new AtomicInteger(0); public static SseEmitter connect(String userId) { // 设置超时时间,0代表永不过期;默认30秒,超时未完成任务则会抛出异常 SseEmitter sseEmitter = new SseEmitter(0L); // 注册SSE的回调方法 sseEmitter.onCompletion(completionCallback(userId)); sseEmitter.onError(errorCallback(userId)); sseEmitter.onTimeout(timeoutCallback(userId)); sseClients.put(userId, sseEmitter); log.info("当前创建新的SSE连接,用户ID为: {}", userId); onlineCounts.getAndIncrement(); return sseEmitter; } /** * @Description: 发送单条消息 */ public static void sendMessage(String userId, String message, SSEMsgType msgType) { if (CollectionUtils.isEmpty(sseClients)) { return; } if (sseClients.containsKey(userId)) { SseEmitter sseEmitter = sseClients.get(userId); sendEmitterMessage(sseEmitter, userId, message, msgType); } } /** * @Description: 发送消息给所有人 */ public static void sendMessageToAllUsers(String message) { if (CollectionUtils.isEmpty(sseClients)) { return; } sseClients.forEach((userId, sseEmitter) -> { sendEmitterMessage(sseEmitter, userId, message, SSEMsgType.MESSAGE); } ); } /** * @Description: 使用SseEmitter推送消息 */ public static void sendEmitterMessage(SseEmitter sseEmitter, String userId, String message, SSEMsgType msgType) { try { SseEmitter.SseEventBuilder msg = SseEmitter.event().id(userId).name(msgType.type).data(message); sseEmitter.send(msg); } catch (IOException e) { log.error("用户[{}]的消息推送发生异常!", userId); removeConnection(userId); } } /** * @Description: 主动切断,停止sse服务和客户端的连接 */ public static void stopServer(String userId) { if (CollectionUtils.isEmpty(sseClients)) { return; } SseEmitter sseEmitter = sseClients.get(userId); if (sseEmitter != null) { // complete 表示执行完毕,断开连接 sseEmitter.complete(); removeConnection(userId); log.info("连接关闭成功,被关闭的用户为 {}", userId); } else { log.warn("当前连接无需关闭,请不要重复操作"); } } /** * @Description: SSE连接完成后的回调方法(关闭连接的时候调用) */ private static Runnable completionCallback(String userId) { return () -> { log.info("SSE连接完成并结束,用户ID为: {}", userId); removeConnection(userId); }; } /** * @Description: SSE连接超时的时候进行调用 */ private static Runnable timeoutCallback(String userId) { return () -> { log.info("SSE连接超时,用户ID为: {}", userId); removeConnection(userId); }; } /** * @Description: SSE连接发生错误的时候进行调用 */ private static Consumer errorCallback(String userId) { return Throwable -> { log.info("SSE连接发生错误,用户ID为: {}", userId); removeConnection(userId); }; } /** * @Description: 从整个SSE服务中移除用户连接 */ public static void removeConnection(String userId) { sseClients.remove(userId); log.info("SSE连接被移除,移除的用户ID为: {}", userId); onlineCounts.getAndDecrement(); } /** * @Description: 获得当前所有的会话总连接数(在线人数) */ public static int getOnlineCounts() { return onlineCounts.intValue(); } } ``` SSEMsgType ```java public enum SSEMsgType { MESSAGE("message", "单次发送的普通消息"), ADD("add", "消息追加,用于流式stream推送"), FINISH("finish", "消息完成"), CUSTOM_EVENT("customEvent", "自定义消息的类型"), DONE("done", "消息完成"); public final String type; public final String value; SSEMsgType(String type, String value) { this.type = type; this.value = value; } } ``` SSEController ```java @Slf4j @RestController @RequestMapping("sse") public class SSEController { /** * @Description: 连接sse服务的接口 */ @GetMapping(path = "connect", produces = {MediaType.TEXT_EVENT_STREAM_VALUE}) public SseEmitter connect(@RequestParam String userId) { return SSEServer.connect(userId); } /** * @Description: 发送单条消息给SSE的客户端 */ @GetMapping("sendMessage") public Object sendMessage(@RequestParam String userId, @RequestParam String message) { SSEServer.sendMessage(userId, message, SSEMsgType.MESSAGE); return "OK"; } /** * @Description: 发送消息给所有客户端用户 */ @GetMapping("sendMessageAll") public Object sendMessageAll(@RequestParam String message) { SSEServer.sendMessageToAllUsers(message); return "OK"; } /** * @Description: add事件流式输出 */ @GetMapping("sendMessageAdd") public Object sendMessageAdd(@RequestParam String userId, @RequestParam String message) throws Exception { for (int i = 0 ; i < 10 ; i ++) { Thread.sleep(200); SSEServer.sendMessage(userId, message + "-" + i, SSEMsgType.ADD); } return "OK"; } /** * @Description: 停止sse */ @GetMapping("stop") public Object stopServer(@RequestParam String userId) { SSEServer.stopServer(userId); return "OK"; } /** * @Description: 获得当前所有的会话总连接数(在线人数) */ @GetMapping("getOnlineCounts") public Object getOnlineCounts() { return SSEServer.getOnlineCounts(); } } ``` 流式的方法OllamaService ```java @Slf4j @Service public class OllamaServiceImpl implements OllamaService { @Resource private OllamaChatClient ollamaChatClient; @Resource private ChatRecordService chatRecordService; @Override public Object aiOllamaChat(String msg) { return ollamaChatClient.call(msg); } @Override public Flux aiOllamaStream1(String msg) { // 代码执行到此处的时间 22:00:00 - 开始时间 Prompt prompt = new Prompt(new UserMessage(msg)); Flux streamResponse = ollamaChatClient.stream(prompt); // 代码执行到此处的时间 22:01:30 - 结束时间 // 两个时间的时间差为1分30秒,则总计90秒 return streamResponse; } @Override public List aiOllamaStream2(String msg) { Prompt prompt = new Prompt(new UserMessage(msg)); Flux streamResponse = ollamaChatClient.stream(prompt); List list = streamResponse.toStream().map(chatResponse -> { String content = chatResponse.getResult().getOutput().getContent(); // System.out.println(content); log.info(content); return content; }).collect(Collectors.toList()); return list; } @Override public void doDoctorStreamV3(String userName, String message) { // 保存用户发送的记录到数据库 chatRecordService.saveChatRecord(userName, message, ChatTypeEnum.USER); Prompt prompt = new Prompt(new UserMessage(message)); Flux streamResponse = ollamaChatClient.stream(prompt); List list = streamResponse.toStream().map(chatResponse -> { String content = chatResponse.getResult().getOutput().getContent(); SSEServer.sendMessage(userName, content, SSEMsgType.ADD); log.info(content); return content; }).collect(Collectors.toList()); SSEServer.sendMessage(userName, "GG", SSEMsgType.FINISH); // 保存AI回复的记录到数据库 String htmlResult = ""; for (String s : list) { htmlResult += s; } chatRecordService.saveChatRecord(userName, htmlResult, ChatTypeEnum.BOT); } } ```
评论区