做一个毕业设计最怕的不是没人用而是做得像“课程作业”。我见过太多同学拿着Spring BootMyBatisVue2的图书管理系统去答辩被老师一句“这不就是个CRUD吗”直接问死。如果你也是计算机、软件工程专业想做一个既有业务、又有技术亮点的项目那“基于SpringAIVue3的墨韵书法鉴赏系统”这个方向值得好好研究。它把传统藏品展示网站升级成一个“能对话、会赏析”的智能平台集成了作品展示、作者与朝代管理、用户收藏、AI一键生成鉴赏文案、书法知识问答等核心功能还通过SpringAI的流式接口把大模型的回答像打字机一样吐到页面上。这篇文章就用实际开发过的经验从需求拆解到前后端实现再到答辩前踩过的坑完整讲一遍。适合想用Java做AI应用毕设、或者正在学SpringAI和Vue3的同学借鉴。1. 项目设计与思路拆解别把“鉴赏系统”做成“展示网站”1.1 需求到底在要什么先想清楚毕设题目里“鉴赏系统”三个字老师到底想看什么最基本的是书法作品能展示、能分类、能搜索没有AI也说得通但那样和十年前的艺术品网站没有区别。真正让“鉴赏”成立的是体验用户看到一幅作品不只是看图片还想知道它出自哪位名家、属于什么朝代、笔法特点是什么、历史背景是什么。传统做法是前端写死一段简介但内容有限、一经发布就不可变。用大模型来生成这些赏析与答疑就变成动态的、可扩展的。所以我的拆解是基本业务闭环作品管理、分类检索、用户收藏、统计。AI核心亮点自动生成作品鉴赏、实时书法知识问答、多轮对话、流式输出。技术支持SpringAI统一模型接入、Vue3实现交互、数据库存储对话记录。1.2 为什么是SpringAI而不是自己用HTTP调模型很多同学一听到“接入大模型”第一反应是自己封装一个HttpClient或者拿Python的FastAPI套一个AI接口。这样不是不行但和Java生态有点割裂。SpringAI的好处在于官方适配器做得足够好。我第一次用的时候最直观的感受是换模型商只需要改配置。用户问同一个问题我用通义、智谱、DeepSeek都能跑前端代码一行不动。这个能力在答辩时是个很好的加分项你可以当场演示改一个配置文件就切换模型。另一个原因是流式输出。SpringAI把SSE和Flux封装得特别顺手接口返回FluxString前端用fetch直接读流。如果自己用HttpClient接模型你得手动解析SSE格式、处理chunk拼装、还要考虑背压问题开发量不小。而且SpringAI的PromptTemplate、ChatMemory、Tool这些功能都是现成的等于官方把AI应用开发的常用零件都给你了你只需要组合业务逻辑。1.3 为什么前端选Vue3Vue3的Composition API确实比Vue2的Options API更适合这种交互复杂一点的页面。你要写AI对话窗口、收藏按钮、筛选条件联动、键盘快捷操作逻辑一旦多了用ref、computed、watch拆分开比什么都堆在data里清爽很多。加上Vite的秒编译和Element Plus组件库后台管理页面能少写很多样式。对大模型应用的页面来说流式读取响应天然适合用ReadableStream而Vue3的Composition API可以用一个自定义hook把Stream的读取、错误处理、生成中的状态全部封装起来。一个useAssistant hookhandleSend之后调用fetch读取reader一行行拼text在ref里更新。这体验比Options API的公共方法复用要舒服。当然选TypeScript不是必须的但如果时间允许我会建议开启。答辩时老师说“你这个系统怎么保证数据结构的稳定性”你就可以答用TS做了接口类型约束。这个优势不用白不用。1.4 技术选型对比与取舍做选择题时需要知道每一条路的坑。我列了一个对比表方案优点缺点结论SpringAI官方抽象、流式好、可切换模型版本迭代快功能还在完善选它题目要求LangChain4j工具调用成熟、RAG支持好学习成本略高可作为备选Python FastAPIAI库多毕设没有Java核心不推荐Vue2 JS老项目资料多Composition API缺交互代码难维护不推荐热搜词也是Vue3这个表在答辩时可以放PPT里效果很好。你不需要把所有方案实现一遍但能让老师看到你确实横向比较过而不是上来就拍脑袋。2. 数据库与AI链路设计从字段到提示词2.1 核心表结构数据模型是系统的地基也是AI回答准确性的保障。表结构我列几张核心的。第一张是书法作品表id bigint 主键title varchar 作品名称author_id bigint 作者IDdynasty_id bigint 朝代IDimage_url varchar 作品图片content_text text 原文/释文brief varchar 简介style_tag varchar 书体标签如楷书、行书、草书、隶书、篆书created_at、updated_at第二张是作者表id、name、alias、birth_year、death_year、intro。第三张是朝代表id、name、start_year、end_year。后面还有用户表、收藏表、浏览记录表、对话记录表其中对话记录表至少要包含session_id、user_id、question、answer、create_time。这里有一个设计细节作品表为什么要冗余一个style_tag字段因为你要在AI对话里回答“该作品属于什么书体”直接查字段即可不需要模型猜测。同理brief字段也可以作为Prompt的上下文减少幻觉。数据库里有的准确信息就要优先给模型而不是让它从旧记忆里抽取。2.2 对话记录的存储策略SpringAI内置了ChatMemory但那是内存级的重启就没了。毕业设计一般会要求看到用户的历史提问记录所以我建议自己做一张表存。每次对话完成之后把question和answer插入。考虑到同一个会话里的上下文需要提交给模型前端维护一个会话级别的消息数组存一下API往返。这个表在答辩时打开数据库就能展示数据是真实写入的比内存数据更有说服力。存储时需要注意answer字段最好存最终完整回答不要存流式过程中的半截数据。流式是给用户看的中间过程入库的应该是最终结果。否则表格里全是残句老师一看就觉得脏。2.3 Prompt模板设计大模型能不能给出专业答案一半看模型本身另一半看你的系统提示词。书法鉴赏系统我设置的系统角色是“一位深耕中国古代书法史与书法美学的专家”语气要有书卷气又不死板必须分3点回答回答必须结合作品背景引用碑帖名称如果用户问的是非书法问题要引导回书法主题回答控制在200字左右。关键一点不要在用户问题里盲目拼接所有作品信息。我试过把整篇释文都塞进Prompt不仅费Token还容易让模型跑偏生成一堆释文而没有赏析。后来只传核心字段作品名、作者、朝代、书体、两三句简介。把长文本放在用户选择“查看完整鉴赏”时才传给模型。2.4 让模型能查数据库Tool函数调用SpringAI支持把Java方法暴露成Tool让大模型在需要时自行调用。我在系统里实现了两个工具getWorkListByStyle(String style)按书体查作品列表。getWorkDetailByName(String name)按名称查作品详情。用户问“你帮我看看王羲之的兰亭序集字有哪些”模型发现需要查库会自动把这个Tool调用出来然后拿着结果组织回答。这个机制在答辩时非常唬人能给老师一种“AI不是凭空瞎聊而是真正系统联动”的感受。实现上就是在方法上标注Tool注解设置name属性然后把这个Bean注册给ToolCallback。SpringAI会根据模型返回的工具调用请求路由到对应方法。这个功能学起来不算难但很能体现你对AI应用的理解深度。3. 后端实操SpringAI接口从配置到流式全通3.1 工程初始化与依赖JDK17是基础SpringBoot建议3.3以上。SpringAI我用的是1.0.0-M系列虽然官方说部分版本不稳定但M4实际上还算稳。需要根据你选的模型服务添加依赖。比如你对接国内支持标准大模型接口的厂商可以用spring-ai-starter-model-openai这个Maven坐标注意这只是适配器名称实际请求到哪里由base-url控制同一个适配器可以对接不同厂商。pom依赖示例dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-openai/artifactId /dependency配套application配置spring: ai: openai: base-url: ${AI_BASE_URL} api-key: ${AI_API_KEY} chat: options: model: ${AI_MODEL_NAME} temperature: 0.7 max-tokens: 800注意api-key不要硬编码在源码里我通常用环境变量或启动参数注入。毕设源代码如果传到GitHub泄露了API Key就麻烦了。3.2 流式对话接口用ChatClient完成。核心代码RestController RequestMapping(/api/ai) public class ChatController { private final ChatClient chatClient; public ChatController(ChatClient.Builder builder) { this.chatClient builder.build(); } PostMapping(value /chat/stream, produces MediaType.TEXT_EVENT_STREAM_VALUE) public FluxString chatStream(RequestBody ChatRequest request) { String systemPrompt 你是书法鉴赏专家回答问题要结合书画史不要编造。; return chatClient.prompt() .system(systemPrompt) .user(request.getMessage()) .stream() .content(); } }几个细节返回类型必须是FluxString不要用Mono。一旦用了Mono流式就变成一次性返回。produces设置成text/event-stream。如果前端需要完整历史可以在user方法里拼接消息数但不要传递过长的历史容易超Token。3.3 智能赏析接口一个POST接口接收workId从数据库查出作品信息拼进Prompt模板调用chatClient.call()得到完整结果。也可以改成流式。PromptTemplate示例String promptText 请以书法鉴赏专家的口吻赏析以下作品 作品名{title} 作者{author} 朝代{dynasty} 书体{style} 简介{brief} 要求先概括整体艺术价值再从笔法、章法、墨法三个方面展开最后给出适合的观展建议。不超过300字。 ; PromptTemplate pt new PromptTemplate(promptText); Message message pt.create(Map.of(title, work.getTitle(), author, work.getAuthorName())); String result chatClient.prompt(message).call().content();业务层拿到result后可以顺便把这段赏析插入到作品的缓存字段中下次访问直接展示缓存省一次模型调用。很多大模型API是计费的能给系统省点成本。3.4 文件上传与图片识别扩展如果老师想看你接入图片可以在作品管理中上传图片后再调用支持图片的模型。但要注意模型API能否传图依赖厂商和版本。我这里不展开只说思路把图片转成Base64放在消息内容里同样用ChatClient将UserMessage的TextContent和MediaContent拼在一起。如果厂商不支持可以退回纯文本说明。别为了这个功能丢掉主流程。3.5 对话记忆实现SpringAI提供了ChatMemory接口简单实现是MessageWindowChatMemory。需要构建ChatClient时带上ChatMemoryChatMemory chatMemory MessageWindowChatMemory.builder() .windowSize(10) .build(); ChatClient chatClient ChatClient.builder() .defaultSystem(...) .defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory)) .build();这样每次请求会把过去10条消息自动带上。但默认的memory是全局的多用户串台。解决用Advisor的conversationId参数区分。前端可以维护一个sessionId每次请求传header或参数。后端要存这10条消息对应的会话ID不同的sessionId不要互相污染。4. 前端实操Vue3页面、流式渲染与状态管理4.1 Vite创建Vue3项目前端工程我用Vite创建模板直接带TypeScriptnpm create vitelatest calligraphy-web -- --template vue-ts cd calligraphy-web npm install npm install vue-router4 pinia element-plus axios目录结构建议views/home首页views/works作品列表views/work-detail作品详情views/chatAI对话页面views/admin后台管理components通用组件composables/useAssistant.ts流式对话逻辑stores/user.ts、stores/chat.tsPinia状态4.2 流式对话hooks实现核心代码export function useAssistant() { const message ref() const loading ref(false) const content ref() async function send(question: string, sessionId: string) { loading.value true content.value try { const res await fetch(/api/ai/chat/stream, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ message: question, sessionId }) }) const reader res.body!.getReader() const decoder new TextDecoder() while (true) { const { done, value } await reader.read() if (done) break const text decoder.decode(value) // 解析SSE data: 前缀 const lines text.split(\n) for (const line of lines) { if (line.startsWith(data:)) { content.value line.replace(data:, ).trim() } } } } finally { loading.value false } } return { message, loading, content, send } }注意如果你直接用EventSource与POST接口对接会失败因为EventSource只支持GET。所以这里用fetchReadableStream最方便。4.3 聊天消息UI与自动滚动聊天窗口要自动滚动到底部可以在接收chunk时执行nextTick然后设置scrollTop scrollHeight。发送按钮需要loading禁用防止重复请求。为了让打字机效果更好不要清除历史记录而是把当前生成内容append到最后一个assistant气泡中。代码层面我习惯用一个数组const messages refArray{ role: string, content: string }([]) async function handleSend() { const question message.value.trim() if (!question) return messages.value.push({ role: user, content: question }) message.value messages.value.push({ role: assistant, content: }) await send(question, 会话ID) messages.value[messages.value.length - 1].content content.value }这样在UI里只需要遍历messages数组最后一个assistant气泡会不断更新内容视觉上就是逐字输入。4.4 页面路由与权限游客可以看作品、可以提问但收藏和管理后台要登录。用路由守卫检查pinia里的用户状态未登录跳登录页。后台管理用meta:{roles:[admin]}来控制不满足角色直接403页面。这是一个很常规但必要的设计答辩时也会被问到“权限是怎么做的”。这里有一个坑pinia在路由守卫执行时可能还没有完全初始化需要先调用一个初始化store的方法比如useUserStore()之后访问store.user。如果直接读window.localStorage也行但不推荐因为和store数据同步容易出问题。4.5 移动端适配书法作品大多是大图页面可以用CSS columns实现瀑布流图片加载用懒加载。详情页在移动端调整字体和大图片宽度。AI对话页面也要注意软键盘弹出后滚动条位置。整体不用搞太复杂但至少别在手机模式下错位毕竟现在很多评委习惯用手机浏览器随便点两下。5. 部署演示与踩过的坑别让答辩卡在跨域和Token上5.1 本地开发环境跨域前后端分离开发前端要访问后端一种简单方式是在vite.config.ts里配置proxy开发时直接把/api代理到localhost:8080/api。另一种方式在后端配置CORS。我建议两个都配但要以proxy为主因为生产环境用Nginx反向代理同样可以避免跨域。Vite代理示例export default defineConfig({ server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })这样浏览器里没有任何跨域问题前端所有请求都走本地域名后端不需要暴露复杂头信息。5.2 生产部署前端打包后dist目录放到Nginx。Nginx配置location / { root /usr/share/nginx/html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://backend:8080/api/; proxy_buffering off; proxy_read_timeout 120s; }proxy_buffering off很重要否则SSE流会被Nginx缓冲前端迟迟收不到第一个token。后端服务要注意环境变量不要把API Key写在仓库里。Docker Compose可以起MySQL、前端、后端一条命令跑完整套环境。但毕业演示其实不用上K8s太招眼了Docker Compose刚刚好。5.3 踩坑实录流式输出不生效这个问题我排查了一整天。一开始后端返回Flux跑浏览器直接能出流但前端接上之后还是一次性拿到。查了半天发现两个原因一是前端用了axiosaxios默认只在网络层结束才resolve读不了流。二是Nginx没加proxy_buffering off。这两个都要改。如果本地vite代理也有buffering问题可以在proxy配置里加changeOrigin。真实环境建议直接用fetch。第二个隐藏坑是Vite的proxy默认会压缩吗不会但会缓冲。你需要确认dev环境的响应头里没有额外的Content-Encoding干扰。遇到问题时先打开浏览器Network面板看一下响应是不是一直pending直到最后才全部返回。如果是一下子返回就说明缓冲没关。5.4 踩坑实录Token超限书法作品的释文有时候很长用户连续问几轮后Token超限导致报错。解决方法是给对话加窗口大小比如MessageWindowChatMemory windowSize10同时超过4000字就压缩。还可以把对话历史按“最近N条规则压缩摘要”两层策略。毕业设计有10轮左右够用没必要上复杂RAG。如果用户问了一篇特别长的释文模型可能会截断或者报错。这种情况下应该设置max-tokens上限并在前端捕获异常显示一个“回答过长已截断”的提示。别让页面出现红色错误栈很难看。5.5 模型输出质量不稳定AI不是百分百靠谱书法史也有真伪争议问题。比如“天下第二行书是谁写的”模型有时候会答错朝代。我的处理是在作品详情数据表里已经写了准确信息调用Tool把准确信息取到后再让模型基于事实回答而不是让它从旧记忆里抽取。另外可以在SystemPrompt里加“如果你不确定就明确说不确定不要编造”。后台也可以加“人工纠错”功能管理员发现AI回答有误时可以编辑答案并缓存。答辩时如果老师故意问一个生僻问题你可以说系统支持人机协同校验AI不是唯一信息来源。这个思路比单纯吹AI效果更稳妥。5.6 答辩高频问题整理提前准备这些问题的答复为什么不用LangChain4j答SpringAI与Spring Boot原生集成切换模型方便适合Java技术栈LangChain4j也优秀但多一层依赖。AI回答错了怎么办答用数据库事实约束 允许AI说不知道。另外后台可以对AI回答做人工标记。换模型要改哪里答改application.yml即可前端不用动。系统性能如何答流式输出降低了首字延迟且并发不高用简单限流防止Key耗尽。有哪些扩展方向答接入向量库做RAG、接入图片识别、增加用户分享功能。6. 源码结构与数据初始化6.1 后端目录结构一个基础的后端分包是这样的controller接口层只做参数解析和返回。service业务逻辑包括AI调用。mapper数据访问。entity表映射对象。dto前端交互对象。configSpringAI、CORS、拦截器配置。AI相关代码可以单独放一个service/ai包里面放ChatService、ToolService等。这样答辩展示时能很清楚地说出哪个类是核心。6.2 前端目录结构前端除了views和components我专门放了一个api目录里面是axios定义的接口函数。不要在组件里直接写fetch统一走api模块。一个好处是面试官或老师看代码时能很快找到接口在哪里。另一个好处是如果后端路径变了只需要改一个文件。stores目录里的chat store负责维护会话列表和消息列表user store负责登录态。composables里的useAssistant只是流式工具真正的消息列表状态还是放在pinia这样切路由不会丢。6.3 数据初始化SQL最后准备一份干净的初始化数据方便一键跑起来。书法作品表插入几条经典作品《兰亭集序》 王羲之 东晋 行书《多宝塔碑》 颜真卿 唐 楷书《寒食帖》 苏轼 北宋 行书《自叙帖》 怀素 唐 草书朝代表也要完整宋、元、明、清都有对应时间段。如果在答辩现场要演示搜索功能数据量至少20条作品太少会显得系统空。做这个项目给我最大的感受是AI不是万能的外挂它只是把传统系统的体验带到了一个新维度。真正让作品被“看懂”的还是你数据库里那一条条准确的史料。所以建议后来者别急着接模型先把书法作品的字段、作者、朝代信息做扎实再让AI去讲。调试流式输出时也不要一上来就写完整页面先用Postman或者浏览器直接访问后端接口确认后端能出流再接前端。答辩前记得关闭无关浏览器把网络环境跑通并准备一个精简的演示脚本别让临时输入占用了宝贵的提问时间。