基于RAG的智能知识库问答系统:SpringBoot+Vue.js毕业设计实战指南
发布时间:2026/10/1 13:39:28来源:尧图网络
1. 项目缘起与整体设计思路1.1 这个系统到底解决什么问题先说说我为什么盯上这个题目。过去一年我帮不下五个学弟学妹看过毕业设计其中三个都选了“智能知识库问答”这个方向。原因很简单大模型火了但企业里真正落地的痛点不是“模型不够聪明”而是“模型不知道我们公司内部那点事”。你问ChatGPT“我们公司报销流程是什么”它只能瞎编你问“这份设备维修手册里第三章讲了什么”它也没法直接给你翻出来。这就是RAGRetrieval-Augmented Generation检索增强生成要解决的核心问题——让大模型在回答之前先去指定的知识库里“查资料”然后基于查到的内容组织答案。这个毕业设计题目“基于RAG的智能知识库问答系统的设计与实现”本质上就是做一个私有化的、能上传文档、能问答、能溯源的知识管理工具。技术栈选的是SpringBoot Vue.js MySQL这是国内高校毕设最稳妥的组合没有之一。前端用Vue.js配ElementUI后端SpringBoot扛住接口MySQL存用户、知识库元数据和问答记录向量检索部分可以接本地模型或者调用API。整套东西做下来工作量饱满技术点覆盖全面答辩的时候也好讲——既有传统CRUD又有RAG这种前沿概念老师想挑刺都得先掂量掂量自己懂不懂向量检索。适合谁来参考这篇内容如果你是计算机相关专业的应届生正在找毕设题目或者已经选了RAG方向但不知道从哪下手那这篇东西就是给你写的。我会把架构怎么定、数据库怎么设计、RAG链路怎么串、前后端怎么对接、文档怎么处理这些事全部分享出来。哪怕你之前没接触过向量数据库跟着思路走也能把系统跑起来。1.2 为什么选SpringBoot Vue.js MySQL这套组合先解释一下技术选型的逻辑因为答辩的时候老师必问“你为什么用这个不用那个”。SpringBoot的优势在于生态成熟、资料多、上手快。你遇到任何问题搜索引擎里一搜八成已经有现成答案。而且SpringBoot的自动配置机制让整合MyBatis、Redis、MinIO这些组件变得非常轻松几个注解加配置文件就能跑起来。对于毕设这种周期短、要求功能全的项目来说SpringBoot是最不容易翻车的选择。我试过用Quarkus或者Micronaut性能确实好但资料少踩坑成本太高不值得。Vue.js配合ElementUI前端开发效率极高。ElementUI的组件库覆盖了表格、表单、弹窗、上传、分页这些毕设常用元素你不需要自己写CSS就能搭出一个看起来挺专业的后台界面。而且Vue的响应式数据绑定让前后端联调变得很舒服后端改个接口字段前端改个绑定就行不用手动操作DOM。MySQL作为关系型数据库存用户信息、知识库列表、文档元数据、问答历史这些结构化数据绰绰有余。向量数据虽然MySQL 8.0也支持向量类型但实际做RAG的时候我建议还是用专门的向量库或者内存索引MySQL只负责存业务数据。这样分工明确也方便答辩时解释“为什么不用MySQL存向量”——因为向量检索需要近似最近邻算法关系型数据库在这方面性能不行。注意如果你的学校要求必须用MySQL存所有东西那也可以把向量序列化成BLOB存进去检索的时候全量加载到内存算余弦相似度。但知识库文档一多这种方案就会卡得没法用。所以最好还是把向量检索单独拆出来。1.3 系统整体架构长什么样整个系统我把它拆成四层前端展示层、后端服务层、数据存储层、RAG引擎层。前端展示层就是Vue.js那套东西负责登录注册、知识库管理、文档上传、问答对话、历史记录查看这些页面。用户的所有操作都通过HTTP请求打到后端。后端服务层用SpringBoot对外提供RESTful接口。核心模块包括用户认证模块JWT、知识库管理模块、文档处理模块、问答服务模块、系统管理模块。每个模块对应一个Controller业务逻辑放在Service层数据访问用MyBatis-Plus。数据存储层分三块MySQL存业务数据MinIO或者本地文件系统存原始文档向量索引存文档切片的向量表示。如果图省事向量索引可以直接用内存里的一个List启动时从MySQL加载如果想做得像样一点可以接Chroma或者Milvus的轻量版。RAG引擎层是整个系统的灵魂。它负责把用户的问题转成向量去向量索引里找最相似的文档片段然后把问题和检索到的上下文拼成一个Prompt发给大模型生成答案。这一层可以完全本地化用Ollama跑一个小模型也可以调云端API。毕设答辩的时候本地化方案更稳妥因为不依赖网络演示不会翻车。2. 核心细节解析与实操要点2.1 数据库表设计别等到写代码才想这件事很多同学做毕设的通病是先写代码数据库表边写边改最后发现字段不够用或者关联关系乱了。我建议你先花半天时间把ER图想清楚后面能省至少三天的返工时间。这个系统最少需要这几张表表名用途关键字段user用户信息id, username, password, role, create_timeknowledge_base知识库id, name, description, user_id, create_timedocument文档元数据id, kb_id, file_name, file_path, file_size, status, chunk_countdocument_chunk文档切片id, doc_id, content, vector_id, chunk_indexqa_record问答记录id, user_id, kb_id, question, answer, sources, create_timedocument表的status字段很关键用来标记文档的处理状态0-待处理1-处理中2-已完成3-处理失败。因为文档上传后需要解析、切片、向量化这个过程可能耗时几秒到几十秒不能让前端一直等着所以用异步处理加状态轮询的方式。document_chunk表的vector_id字段存的是向量在向量库里的唯一标识。如果你用内存索引这个字段可以存数组下标如果用Chroma就存Chroma返回的ID。qa_record表的sources字段存的是本次回答引用了哪些文档片段用JSON格式存一个数组包含文档名和片段内容摘要。前端展示的时候可以做成“引用来源”的折叠面板答辩的时候这个功能很加分。实操心得MySQL建表的时候字符集统一用utf8mb4排序规则用utf8mb4_general_ci。别用utf8那个是假的utf8存emoji会报错。虽然毕设不一定用到emoji但养成好习惯没坏处。2.2 文档处理流水线从上传到可检索文档处理是RAG系统里最容易被低估的环节。很多同学以为上传完就完事了结果问答的时候发现检索出来的内容驴唇不对马嘴。问题就出在文档处理上。完整的文档处理流程是这样的文件上传前端用ElementUI的Upload组件后端用MultipartFile接收。文件存到MinIO或者本地磁盘数据库里只存路径。格式解析根据文件扩展名选择解析器。PDF用PDFBox或者Apache TikaWord用POITXT直接读Markdown按行读。解析出来的是一整段纯文本。文本清洗去掉多余的空格、换行、页眉页脚、特殊字符。这一步不做的话切片里会混入大量噪声影响检索质量。文本切片这是核心步骤。切片太短语义不完整切片太长检索精度下降。我一般用固定长度重叠的策略每片500个字符相邻片之间重叠50个字符。重叠是为了防止关键信息刚好被切在边界上。向量化把每个切片送进Embedding模型得到向量。可以用OpenAI的text-embedding-ada-002也可以用本地的BGE-small-zh。毕设推荐用本地模型免费且不依赖网络。存入向量索引把向量和对应的切片ID存进向量库。同时把切片内容存进MySQL的document_chunk表。// 切片核心逻辑示例 public ListString splitText(String text, int chunkSize, int overlap) { ListString chunks new ArrayList(); int start 0; while (start text.length()) { int end Math.min(start chunkSize, text.length()); chunks.add(text.substring(start, end)); start end - overlap; if (start text.length() - overlap) break; } return chunks; }注意切片大小不是固定的要根据文档类型调整。技术文档可以小一点300-500字因为概念密集叙述性文档可以大一点800-1000字因为需要上下文。毕设为了简单统一用500字加50字重叠就行。2.3 向量检索RAG的“查资料”环节向量检索的原理说起来不复杂把用户的问题也转成向量然后计算它和知识库里所有切片向量的相似度取最相似的Top-K个。相似度计算用余弦相似度公式是similarity (A · B) / (||A|| * ||B||)其中A是问题向量B是切片向量。余弦相似度的值在-1到1之间越接近1越相似。实际用的时候因为向量都已经归一化了所以直接算点积就行。Top-K的K值怎么定我试过K3、K5、K10。K太小可能漏掉关键信息K太大会引入无关内容干扰大模型。K5是个比较稳妥的默认值。如果知识库文档很多可以先用K20粗筛再用重排序模型精排取前5。// 余弦相似度计算向量已归一化 public double cosineSimilarity(float[] a, float[] b) { double dot 0.0; for (int i 0; i a.length; i) { dot a[i] * b[i]; } return dot; }检索的时候还有一个关键参数相似度阈值。低于这个阈值的切片直接丢弃不送给大模型。阈值设0.7比较合适太低会引入噪声太高可能什么都检索不到。这个值需要根据你的Embedding模型和文档特点微调。实操心得如果检索效果不好先别急着换模型。检查一下切片质量看看检索出来的片段是不是本身就答非所问。很多时候问题出在切片上不是模型上。3. 实操过程与核心环节实现3.1 后端项目搭建与依赖配置先创建一个SpringBoot项目用Spring Initializr生成骨架。注意SpringBoot版本别选太新的3.0以上对JDK版本要求高而且有些依赖还没适配。推荐用2.7.x版本稳定且资料多。pom.xml里需要加这些核心依赖!-- Web -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- MyBatis-Plus -- dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3/version /dependency !-- MySQL驱动 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency !-- JWT -- dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt/artifactId version0.9.1/version /dependency !-- 文档解析 -- dependency groupIdorg.apache.pdfbox/groupId artifactIdpdfbox/artifactId version2.0.29/version /dependency dependency groupIdorg.apache.poi/groupId artifactIdpoi-ooxml/artifactId version5.2.3/version /dependency !-- MinIO -- dependency groupIdio.minio/groupId artifactIdminio/artifactId version8.5.2/version /dependencyapplication.yml的配置server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/rag_kb?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true minio: endpoint: http://localhost:9000 access-key: minioadmin secret-key: minioadmin bucket-name: rag-docs注意MySQL 8.0的连接URL必须加serverTimezone参数否则会报时区错误。这个坑我踩过折腾了半小时才发现。3.2 文档上传与异步处理实现文档上传接口不能同步处理因为解析和向量化可能很慢。我的做法是上传接口只负责存文件、写数据库记录然后发一个异步任务去处理。PostMapping(/upload) public Result upload(RequestParam(file) MultipartFile file, RequestParam(kbId) Long kbId) { // 1. 存文件到MinIO String filePath minioService.upload(file); // 2. 写数据库记录 Document doc new Document(); doc.setKbId(kbId); doc.setFileName(file.getOriginalFilename()); doc.setFilePath(filePath); doc.setStatus(0); // 待处理 documentMapper.insert(doc); // 3. 发异步任务 documentProcessService.processAsync(doc.getId()); return Result.success(doc); }异步处理用Async注解记得在启动类上加EnableAsync。Async public void processAsync(Long docId) { Document doc documentMapper.selectById(docId); doc.setStatus(1); // 处理中 documentMapper.updateById(doc); try { // 解析文本 String text parseFile(doc.getFilePath()); // 清洗 text cleanText(text); // 切片 ListString chunks splitText(text, 500, 50); // 向量化并存储 for (int i 0; i chunks.size(); i) { float[] vector embeddingService.embed(chunks.get(i)); String vectorId vectorStore.add(vector, docId, i); DocumentChunk chunk new DocumentChunk(); chunk.setDocId(docId); chunk.setContent(chunks.get(i)); chunk.setVectorId(vectorId); chunk.setChunkIndex(i); chunkMapper.insert(chunk); } doc.setStatus(2); // 已完成 doc.setChunkCount(chunks.size()); } catch (Exception e) { doc.setStatus(3); // 失败 log.error(文档处理失败, e); } documentMapper.updateById(doc); }前端上传后轮询文档状态接口直到status变成2或3。3.3 问答接口的完整链路问答接口是整个系统最核心的部分。用户在前端输入问题后端要完成问题向量化 → 检索 → 拼Prompt → 调大模型 → 返回答案和来源。PostMapping(/ask) public Result ask(RequestBody AskRequest request) { // 1. 问题向量化 float[] queryVector embeddingService.embed(request.getQuestion()); // 2. 检索Top-K ListSearchResult results vectorStore.search(queryVector, request.getKbId(), 5); // 3. 过滤低相似度结果 results results.stream() .filter(r - r.getScore() 0.7) .collect(Collectors.toList()); // 4. 拼Prompt StringBuilder context new StringBuilder(); for (SearchResult r : results) { context.append(r.getContent()).append(\n\n); } String prompt 基于以下资料回答问题。如果资料中没有相关信息请说不知道。\n\n 资料\n context \n 问题 request.getQuestion() \n 回答; // 5. 调大模型 String answer llmService.chat(prompt); // 6. 存记录 QaRecord record new QaRecord(); record.setUserId(request.getUserId()); record.setKbId(request.getKbId()); record.setQuestion(request.getQuestion()); record.setAnswer(answer); record.setSources(JSON.toJSONString(results)); qaRecordMapper.insert(record); return Result.success(new AskResponse(answer, results)); }Prompt的写法很关键。我试过几种模板最后发现**明确告诉模型“资料里没有就说不知道”**能有效减少幻觉。另外把资料放在问题前面模型更容易找到依据。实操心得如果大模型返回的答案里出现了“根据资料”这种话可以在Prompt里加一句“回答时不要提及资料二字直接给出答案”。这样用户体验更好。3.4 前端关键页面实现前端用Vue CLI创建项目装ElementUI和axios。vue create rag-frontend cd rag-frontend npm install element-ui axios在main.js里引入ElementUIimport Vue from vue import ElementUI from element-ui import element-ui/lib/theme-chalk/index.css import App from ./App.vue import router from ./router import axios from axios Vue.use(ElementUI) Vue.prototype.$axios axios axios.defaults.baseURL http://localhost:8080 new Vue({ router, render: h h(App) }).$mount(#app)问答页面是核心用ElementUI的卡片和输入框搭template div classqa-container el-card div slotheader智能问答/div el-select v-modelkbId placeholder选择知识库 el-option v-forkb in kbList :keykb.id :labelkb.name :valuekb.id / /el-select el-input v-modelquestion typetextarea :rows3 placeholder输入你的问题 / el-button typeprimary clickask :loadingloading提问/el-button /el-card el-card v-ifanswer div slotheader回答/div p{{ answer }}/p el-collapse el-collapse-item title引用来源 div v-for(src, idx) in sources :keyidx p{{ src.fileName }} - 片段{{ src.chunkIndex }}/p p{{ src.content }}/p /div /el-collapse-item /el-collapse /el-card /div /template文档上传页面用el-upload组件设置:http-request自定义上传逻辑上传成功后轮询状态。4. 常见问题与排查技巧实录4.1 检索效果差怎么办这是RAG系统最常见的问题。用户问“报销流程”检索出来的却是“考勤制度”。排查思路按这个顺序来问题现象可能原因排查方法解决方案检索结果完全不相关Embedding模型不适合中文用几个已知相似的句子测试换BGE-small-zh或text2vec-base-chinese检索结果部分相关切片太大或太小查看切片内容调整chunkSize到300-800之间关键信息检索不到切片边界切断了语义检查关键信息所在切片增加overlap到100相似度分数普遍偏低向量未归一化检查向量模长归一化后再存检索结果重复文档有重复内容查看原始文档去重或增加多样性惩罚我遇到过一个典型案例用户问“设备故障怎么报修”检索出来的全是“设备采购流程”。后来发现是切片的时候把“报修”和“采购”切到了同一个片里导致向量语义混杂。把切片大小从1000降到500后问题解决。4.2 大模型回答不准确或胡编RAG系统最怕的就是模型“一本正经地胡说八道”。明明检索到了正确资料模型却给出了错误答案。原因通常是Prompt没写好。我的经验是Prompt里必须包含这三句话“仅基于以下资料回答问题”“如果资料中没有相关信息请直接说‘根据现有资料无法回答’”“不要编造资料中不存在的信息”另外把temperature参数调低0.1到0.3之间比较合适。temperature越高模型越有创造力但也越容易胡编。毕设演示的时候稳定比创意重要。注意如果用的是本地小模型比如Qwen2-1.5B它的指令遵循能力有限Prompt要写得更简单直接。别用太复杂的格式要求它理解不了。4.3 文档处理速度慢一个10页的PDF切片后可能有50-100个片段每个片段都要调Embedding模型。如果用云端API受网络影响可能要好几分钟。优化方案批量向量化把多个切片拼成一个batch一次请求返回多个向量。大多数Embedding API都支持batch输入。本地模型用ONNX Runtime跑BGE-smallCPU上单条推理只要几十毫秒比调API快得多。异步进度反馈前端显示处理进度用户知道系统在工作不会以为卡死了。// 批量向量化示例 public Listfloat[] embedBatch(ListString texts) { // 每批最多16条 Listfloat[] vectors new ArrayList(); for (int i 0; i texts.size(); i 16) { int end Math.min(i 16, texts.size()); ListString batch texts.subList(i, end); vectors.addAll(embeddingClient.embed(batch)); } return vectors; }4.4 前端跨域和文件上传大小限制前后端分离开发时跨域是必踩的坑。后端加一个配置类Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE) .allowCredentials(true) .maxAge(3600); } }文件上传大小限制在application.yml里改spring: servlet: multipart: max-file-size: 50MB max-request-size: 50MB实操心得MinIO的bucket权限要设成public read否则前端拿到的文件URL访问不了。但生产环境别这么干毕设演示无所谓。4.5 数据库连接池配置MySQL默认的连接数很少并发一高就报“Too many connections”。在application.yml里配一下HikariCPspring: datasource: hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000 idle-timeout: 600000 max-lifetime: 1800000这个配置对毕设来说绰绰有余。如果答辩演示时有多人同时访问也不会崩。5. 论文写作与答辩准备建议5.1 论文结构怎么安排毕设论文一般要求1.5万到2万字章节安排我建议这样第一章绪论讲研究背景和意义重点说清楚“为什么需要RAG”和“现有知识库系统有什么不足”。第二章相关技术介绍把SpringBoot、Vue.js、RAG、向量检索这些概念解释清楚别抄百度百科用自己的话讲。第三章需求分析画用例图、功能模块图。第四章系统设计画架构图、ER图、流程图。第五章系统实现贴核心代码和界面截图。第六章测试写功能测试和性能测试。第七章总结与展望。注意论文里的图别用Mermaid画用Visio或者Draw.io画好导出PNG。Mermaid渲染出来的图在Word里容易乱码。5.2 答辩时老师可能问的问题根据我帮人模拟答辩的经验这几个问题出现频率最高“你的RAG和直接问ChatGPT有什么区别”——答RAG能基于私有知识库回答且能溯源ChatGPT不知道你的私有数据。“向量检索的相似度怎么算的”——答余弦相似度公式是……“如果知识库很大检索会不会很慢”——答可以用近似最近邻算法如HNSW建索引把复杂度从O(n)降到O(log n)。“你的系统有什么创新点”——答把RAG落地到毕设场景实现了文档自动处理和引用溯源工程完整度高。“为什么不用LangChain”——答LangChain封装太厚出问题不好排查。自己实现链路更可控也更能体现工作量。5.3 源码和文档整理毕设提交一般要求源码论文答辩PPT。源码目录结构要清晰rag-kb-system/ ├── backend/ # SpringBoot后端 │ ├── src/main/java/ │ │ ├── controller/ │ │ ├── service/ │ │ ├── mapper/ │ │ ├── entity/ │ │ └── config/ │ └── pom.xml ├── frontend/ # Vue前端 │ ├── src/ │ │ ├── views/ │ │ ├── components/ │ │ └── router/ │ └── package.json ├── sql/ # 建表语句 │ └── init.sql └── README.md # 部署说明README里写清楚环境要求、启动步骤、默认账号密码。答辩老师如果让你现场演示你直接按README操作就行不会手忙脚乱。实操心得提前把演示数据准备好上传几份文档问几个典型问题把问答记录也预置几条。答辩现场网络可能不稳定如果调云端API卡住了就切到本地模型。我建议至少准备一套完全离线的方案作为保底。5.4 后续扩展方向这个系统做完之后如果想继续深挖有几个方向可以扩展一是加入多轮对话能力让用户能追问二是加入权限管理不同用户看到不同知识库三是加入知识图谱把实体关系也纳入检索范围四是加入反馈机制用户可以对回答点赞点踩用来优化检索排序。这些扩展点写在论文的“展望”章节里能体现你对领域的理解深度。最后再分享一个小技巧答辩PPT别放太多字每页一个核心点配一张图。老师看的是你的思路和演示效果不是你的PPT排版。把系统跑通、把问题答上来比什么都强。
网站建设高端定制企业官网