ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Spring Boot + Vue 3 多模型AI聊天应用全栈实战

Spring Boot + Vue 3 多模型AI聊天应用全栈实战 简介面向毕业设计选题与Spring AI技术实践这份基于Spring Boot 3.2.0和Vue 3的AI聊天应用源码集成了DeepSeek、SiliconFlow、Gemini等多种大模型接口适合需要快速搭建AI对话产品原型的高校学生与Java开发者。项目以前后端分离方式组织后端采用Spring Boot与WebFlux响应式编程实现聊天接口的流式输出前端基于Vue 3完成实时聊天界面、响应式布局、Markdown渲染等交互能力代码结构清晰便于二次扩展。压缩包共20个文件以Java源码为主11个java辅以Vue/JavaScript逻辑3个js、项目配置yml、前端样式css、页面结构html及说明文档md等包体仅48KB轻量易读适合直接导入IDE学习与运行。已有182人学习下载对于想掌握Spring AI多模型接入、WebFlux流式响应或完成相关毕设课题的读者这份源码提供了完整的落地参考。 这段时间我一直在搞一个基于 Spring Boot 和 Vue 3 的 AI 聊天应用源码从头到尾完整落地了一版接入的模型包括 DeepSeek、SiliconFlow 和 Gemini。这个项目拿来当毕业设计定位非常讨巧它不碰自训练模型、不需要 GPU核心是把三家大模型 API 通过统一适配层接进自己的业务系统里同时把全栈里最常考的几块技术——Spring Boot、Vue 3、流式传输、异步消息队列——全部串起来。说白了这是一个既能在答辩时讲清楚又能在简历上写实的项目。这篇文章我打算把整个项目从选题思路、技术选型、后端适配层设计到前端流式渲染、Redis Stream 消息队列落地再到部署时踩过的坑一次说透。适合正在发愁毕设选题的同学也适合想快速走一遍 Java Vue 全栈 AI 应用的朋友参考。1. 选题思路与技术栈选型1.1 为什么是 Spring Boot Vue 3 AI 这个组合作为毕业设计项目的技术选型不能太保守也不能冒进到把自己卡死。Spring Boot Vue 3 是一个经历过大量生产环境验证的组合资料多、答疑方便、生态成熟这个底座几乎不会出大问题。真正让这个项目“跳出来”的点是 AI 接入现在主流大模型都提供 HTTP API不需要本地部署模型、不需要显卡一台普通笔记本就能跑完整套开发调试流程。选这个组合还有一层现实的考虑——就业导向。后端高频面试题里的 REST API 设计、接口鉴权、SSE 长连接、Redis 使用、事务处理前端高频的响应式状态管理、组件通信、异步请求处理在这个项目里全部都能找到对应的落地点。比起单纯做一个增删改查管理系统这个项目的技术覆盖面明显更有竞争力。另外从答辩演示角度考虑AI 聊天应用的效果非常直观。现场输入一句话模型逐字返回配合前端打字机效果评委一眼就能明白项目在做什么。这种“看得见”的成果比花大力气做后台管理、报表统计要省事得多也更容易留下好印象。1.2 多模型接入DeepSeek、SiliconFlow 与 Gemini 的差异一开始我也纠结过一个毕设项目接一个模型不就够了吗后来实际做下来发现接多个模型并不是“炫技”而是有非常实际的价值。第一容灾与成本。不同厂商的 API 时有波动有的模型在高峰期响应慢有的模型在特定类型的问题上效果差。多个模型可以互相备份也可以按场景切换——日常闲聊用便宜快速的模型复杂推理用更聪明的模型。第二这是项目里一个天然的亮点模块。在系统架构层面多模型接入意味着必须设计一个“适配层”把不同厂商的协议差异挡在业务外面。这一点在简历和答辩里都可以作为核心设计点来讲。这三家的接入方式差异相当明显DeepSeek 走的是 OpenAI 兼容协议API 路径是/chat/completions请求体里带messages数组流式响应里内容在choices[0].delta.content这个位置。SiliconFlow 作为一个模型聚合平台它提供的也是 OpenAI 兼容格式但 base URL 不一样模型名非常多相当于“一个 API 换几十种开源模型”。Gemini 的协议完全不同走的是generateContent请求体结构是contents数组流式返回里的文本在candidates[0].content.parts中。这也就意味着如果直接把 HTTP 调用逻辑写在业务代码里每换一个模型就要改一遍 Service。而通过定义一个统一的ChatProvider接口把每个模型封装成实现类业务层只依赖接口这就是适配层模式的核心价值。2. 后端架构设计与核心实现2.1 统一模型接口层与协议适配后端这块我先搭了一个非常薄的模型层核心是一个接口public interface ChatProvider { // 服务提供方名称如 deepseek / siliconflow / gemini String providerName(); // 非流式调用返回完整文本 String chat(ChatRequest request); // 流式调用通过 sink 逐段往外推 void streamChat(ChatRequest request, StreamSink sink); }ChatRequest里封装了用户消息、历史消息列表、模型名、temperature、max_tokens 这些公共参数。每个模型一个实现类比如DeepSeekChatProvider、SiliconFlowChatProvider、GeminiChatProvider各自负责把公共参数翻译成对应厂商的请求结构再解析返回结果。这里有一个非常关键的差异点解析流式响应。OpenAI 兼容接口的流式 chunk 长这样data: {choices:[{delta:{content:你}}]} data: {choices:[{delta:{content:好}}]} data: [DONE]Gemini 则长这样data: {candidates:[{content:{parts:[{text:你}]}}]}如果不做适配层这些解析逻辑会散落到业务代码各处分叉非常难看。做了统一接口之后前端根本不关心后端到底接的哪家模型它只收到一种统一格式的流式返回业务代码也完全不需要感知模型差异。这个设计在答辩时可以专门画一张图请求进来 - 路由到指定 Provider - 协议转换 - 流式返回逻辑非常清晰。在实现时有一个细节要注意调用外部 API 一定要设置连接超时和读取超时。我用的是RestClient连接超时给 5 秒读取超时给 60 秒。如果不设读取超时一旦上游模型迟迟不返回请求线程会一直挂着连接池很快就会被耗尽。2.2 流式对话SSE 推送与异步线程池AI 聊天的核心体验就是“打字机效果”这要求后端把模型返回的内容边收边推给前端。Spring Boot 里做这件事最直接的方式是 SSEServer-Sent Events用SseEmitter。需要注意的是SseEmitter默认会占用一个 Tomcat 工作线程。如果直接在 Controller 方法里同步调用上游 API等到模型全部返回完才结束那么每个对话请求会长时间占用一个线程。并发一高 Tomcat 线程池很快就会打满。所以正确的做法是把耗时操作丢给业务线程池执行Controller 方法直接返回SseEmitter让 Tomcat 线程立刻释放。大致结构是这样的PostMapping(/chat/stream) public SseEmitter stream(RequestBody ChatRequest request) { SseEmitter emitter new SseEmitter(120_000L); chatService.streamChat(request, emitter); return emitter; }emitter.send()可以发送文本片段结束的时候调用emitter.complete()异常时调用emitter.completeWithError(e)。这里有一个我踩过坑的点前端断开了连接时SseEmitter会抛出IOException这个异常一定要捕获并且确保调用emitter.complete()释放资源否则这个 emitter 会一直挂在内存里直到超时。还有超时时间不能随便设。太短则长文本还没推完通道就关了太长则异常情况下资源释放慢。我设置的是 120 秒基本能覆盖大多数模型生成长回答的时间。2.3 Redis Stream 异步落库与消费聊天记录必须落库这是毕设的基本要求。但这里有一个问题如果每聊一句话就同步去写一次数据库会拖慢对话响应如果把消息处理和模型调用串在一起用户要等数据库写完才能看到首字返回。解决思路是把“对话”和“记录”解耦通过 Redis Stream 做异步队列。这也是搜索热词里“spring boot redis stream 如何拉取队列消息”对应的部分。我在项目里实践下来核心逻辑分三段首先是写入端。每次会话结束后把消息记录封装成ChatRecord通过redisTemplate.opsForStream().add()写入指定 keyObjectRecordString, ChatRecord record StreamRecords.newRecord() .ofObject(chatRecord) .withStreamKey(chat:record:stream); redisTemplate.opsForStream().add(record);然后是消费端。Redis Stream 的消费比 List 的brpop先进之处在于支持消费者组可以多个消费者并行消费且每条消息只会被组内一个消费者拿到。配置一个StreamMessageListenerContainer监听同一个 keyStreamMessageListenerContainerString, ObjectRecordString, ChatRecord container StreamMessageListenerContainer.create( redisConnectionFactory, StreamMessageListenerContainerOptions.builder() .pollTimeout(Duration.ofSeconds(1)) .targetType(ChatRecord.class) .build()); container.receive( Consumer.from(chat-record-group, consumer-1), message - { ChatRecord record message.getData(); chatRecordMapper.insert(record); // 处理完成后确认消息 redisTemplate.opsForStream().acknowledge(chat:record:stream, chat-record-group, message.getId()); }); container.start();这里最关键的坑是acknowledge。Redis Stream 不会自动删除已投递消息如果消费者处理失败后没有确认消息会一直停留在 Pending 列表里再次被投递。如果不ack消息会累积最终变成“幽灵消息”。我在最开始没写ack导致启动一段时间后消息堆积数据库里重复数据一大片。所以完整流程一定是投递 - 处理 - ack三步缺一不可。另外要注意消费者组的创建需要提前执行否则receive会直接报NOGROUP错误。可以在启动时加一个初始化方法提前执行XGROUP CREATE chat:record:stream chat-record-group 0 MKSTREAM。3. 前端 Vue 3 交互层实战3.1 消息列表与流式解析前端是 Vue 3 Vite核心交互就是一个多会话聊天页面。这里最麻烦的部分是流式响应的解析。后端走的是 SSE但前端我没有用EventSource原因是EventSource只支持 GET 请求而我们的对话接口需要 POST body 传参数。所以采用了fetchReadableStream的方式手动解析。核心代码大概是这样const response await fetch(/api/chat/stream, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify(payload), }); const reader response.body.getReader(); const decoder new TextDecoder(utf-8); while (true) { const { done, value } await reader.read(); if (done) break; const chunk decoder.decode(value, { stream: true }); // 手动按行切分 SSE 格式 const lines chunk.split(\n).filter(line line.startsWith(data:)); for (const line of lines) { const data line.replace(data:, ).trim(); if (data [DONE]) continue; const json JSON.parse(data); currentMessage.value json.content; } }这段代码有一个经典的大坑chunk的分隔不一定是按行对齐的。TCP 分片、Nginx 转发、后端写入缓冲都有可能导致一次reader.read()返回的内容横跨多条 SSE 事件或者一条事件被拆成两半。直接按\n切分然后解析 JSON大概率会在切到一半的时候报JSON.parse错误。我的处理方案是维护一个buffer变量每次拿到新 chunk 先拼到 buffer再按换行符切分最后一段如果凑不齐一整行留在 buffer 里等下一次。这样解析就稳定了。这个细节虽然小但没经验的人真的要调很久。3.2 会话、模型切换与参数控制Vue 3 的逻辑组织我用了 Composition API整个聊天状态收敛到一个useChatStore里维护会话列表、当前会话消息、当前模型和参数设置。一个比较难处理的设计点是切换模型时上下文怎么处理。模型之间不能共享多轮上下文因为不同模型的提示词格式和上下文长度都不一样。我的策略是每个会话绑定一个模型新建会话时可以选择模型创建之后不允许切换如果用户想用另一个模型就新建会话。这样实现简单也避免混用导致的上下文错乱。参数面板我做了三个控制项temperature、top_p、max_tokens。这里有一个不同模型的兼容性问题——Gemini 对 temperature 的取值范围有自己的一套约束而 OpenAI 兼容接口的模型对 max_tokens 的命名是max_tokensGemini 则是maxOutputTokens。同样是前端传一个参数后端适配层要做一次映射。这也是为什么前端只需要一个统一结构后端去处理差异。前端还有一个小功能值得做历史会话本地持久化。我用 localStorage 保存会话列表和最近 50 条消息刷新页面不丢。这个功能实现成本极低但演示的时候很加分——起码证明你考虑到了用户体验。4. 部署、运行与常见问题排查4.1 开发环境从命令行把项目跑起来这个项目在本地跑起来的流程很简单但我见过太多同学卡在第一步。后端是标准的 Maven 项目命令行直接执行mvn spring-boot:run -Dspring-boot.run.profilesdevdev profile 里配置本地 Redis 连接、各模型 API Key、密钥等信息。注意这些配置不要写死在application.yml里用环境变量覆盖避免源码上传时把 API Key 泄露。前端是 Vite 工程跑起来也简单npm install npm run dev但有个关键配置必须写对Vite 的 dev server 代理。前端页面跑在 5173后端接口在 8080如果不配代理前端每个请求都会跨域。在vite.config.ts里加一段server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true, }, }, },这样前端请求/api/chat/stream时会自动转发到后端不会触发浏览器 CORS。把代理配好整个开发过程就顺畅很多。4.2 生产环境部署与 Tomcat 注意事项如果毕设要求部署到服务器常见做法有两种。一种是前后端分开部署后端打成 jar 包java -jar app.jar运行前端把 dist 目录扔给 Nginx 托管Nginx 配置/api反向代理到后端。这里有一个大坑Nginx 默认会缓冲 SSE 响应导致前端收到的一大坨数据在缓冲区里攒着流式效果完全丢失。必须关闭缓冲location /api/ { proxy_pass http://localhost:8080; proxy_buffering off; proxy_cache off; proxy_read_timeout 300s; }proxy_read_timeout也要调大默认 60 秒内如果后端没有输出数据Nginx 会断开连接。模型生成长文时可能超过这个时间。另一种是把前端打包后放到 Spring Boot 的static目录下一个 jar 包全搞定适合演示环境。但这种做法要注意 Vue Router 的 history 模式刷新会 404需要加一个 controller 把未知路径转发到index.html。方便起见演示环境我通常直接改用 hash 模式路由省掉这个麻烦。4.3 高频报错与排查方向项目跑完我把高频问题整理成了一张表基本覆盖了从联调到部署的各个阶段现象原因解决办法前端调接口报 401请求未携带登录 token检查拦截器放行/api/chat/**或补齐 Authorization 头200 但内容为空模型请求格式不对远端返回了空 choices用 Postman 直接打上游 API对比请求体差异流式内容一直不输出最后一次性出现Nginx 缓冲了 SSE关闭proxy_bufferingJSON parse error on chunkSSE 事件被拆包用 buffer 拼接按完整行解析Redis 连接拒绝本地 Redis 未启动先redis-cli ping再查配置路径Gemini 返回 400 Bad Requestcontents 结构或 participant role 不对仔细看 Gemini API 文档system 指令要用systemInstruction字段模型返回超长截断max_tokens 设置太小调大或在前端提示用户还有一个实用技巧开发阶段不要每个问题都调真实模型接口既烧钱又慢。我把每个模型的响应 JSON 保存成固定的测试文件后端提供一个 mock provider开发联调时切换到 mock 模式。前端不管后面接的是真模型还是假模型都能正常开发。这个思路在团队协作时特别实用其他人不申请 API Key 也能把前端页面完全调通。写在最后我个人在源码落地过程中最深的体会是这个项目真正的难点并不是“调用一个模型”而是把多个不同协议的模型统一进一套业务体系再通过流式和异步队列把交互体验做好。先跑通一个模型的最小闭环再逐步加多模型、加历史会话、加 Redis Stream 落库这个节奏会让整个开发过程清晰可控。最后再分享一个小的扩展方向如果想让这个项目再往上走一个档次可以尝试接入 Spring AI——它本身就是 Spring 官方对 AI 应用编程模型的抽象能够更规范地管理模型调用、提示词模板和结构化输出。把 Spring Boot Vue 3 的底座留着把适配层替换成 Spring AI或者在这个基础上加一个 RAG 知识库都可以让项目从“毕设完成度”直接升级到“求职作品级别”。结构搭好了后面想怎么长完全取决于你自己的方向。本文还有配套的精品资源点击获取
返回列表