简介面向毕业设计与课程设计场景的智能问答系统源码基于Spring AI Alibaba技术栈实现RAG检索增强生成流程适合计算机、软件工程、电子信息等专业学生作为课设或毕设参考。压缩包共14个文件包含5个Java源文件、2个properties配置、pom.xml与mvnw等构建支持文件以及README.md说明文档整体仅17KB属于轻量级纯代码项目便于快速阅读和运行调试。目前已有143人学习下载。项目展示了Spring AI Alibaba技术栈的工程实践通过检索与生成模型协同完成知识库问答代码中涉及数据存储、文档解析、向量检索及提示词工程等关键环节。学习者可借此掌握RAG系统从配置到接口调用的完整链路理解模块拆分与参数设计同时获得一套可演示、可扩展的毕业设计选题原型对于后续从事AI应用开发也有实际帮助。1. 毕设与课设里的 Spring AI Alibaba RAG 智能问答系统到底难在哪如果你正在为毕业设计或课程设计挑题目大概率会被“基于Spring AI Alibaba的RAG智能问答系统”这个方向吸引。它听起来完整有后端框架、有AI模型、有知识库、有前端展示工作量好描述也方便答辩时讲故事。但真正动手后你会发现问答本身反而是最简单的一步难点全在“怎么让系统真的答对”——也就是RAG里的检索和内容组织。这篇文章我会按自己实际搭过这套方案的顺序帮你把Spring AI Alibaba里做RAG的最小可跑通路径拆开讲并重点说清楚参数怎么调、坑在哪儿、拿到什么效果才算能交差。这套方案适合两类人一类是要在几周内拿出可演示成果的学生另一类是想在公司内部快速验证私有知识问答可行性的后端开发者。你不需要先精通大模型原理但对Java、Spring Boot和基本的SQL要有底子。2. 为什么用 Spring AI Alibaba 来做 RAG一套接口省掉一半集成活2.1 先认清 Spring AI Alibaba 是什么不是黑匣子是对齐了“模型接入”的抽象层Spring AI Alibaba 是 Spring AI 生态里针对阿里云通义系列模型做的适配实现。它的价值不在于内置了多么神奇的RAG算法而在于帮你把“换模型”“换向量库”“换切分策略”这类脏活统一成了同一套接口。常见做法是你在代码里面向 ChatClient、EmbeddingModel、VectorStore 这三个抽象编程底层到底连的是通义千问还是别的模型对业务代码基本透明。这对毕设来说很有用。因为答辩时老师大概率会问“你用的是哪个模型、为什么选这个”你可以理直气壮地回答用的是 DashScope 上托管的 qwen 系列模型通过 Spring AI Alibaba 接入后续如果想换其他厂商模型改动集中在配置层而不是把业务代码重写一遍。这个回答既体现了你对抽象的理解又不用真的去证明它。2.2 RAG 骨架四个环节在框架里分别落在哪个组件上RAG 全称 Retrieval-Augmented Generation核心思想是不让大模型凭空回答而是先从你自己的知识库里检索出相关内容再把这些内容作为上下文拼进 Prompt让模型基于给定材料作答。Spring AI Alibaba 里这套流程的落点如下表RAG 环节做什么Spring AI Alibaba 里对应组件常用实现文档加载把 pdf/txt/md 读成文本DocumentReaderPagePdfDocumentReader、TextDocumentReader文本切分把长文档切成适合检索的块TextSplitterTokenTextSplitter向量化把文本块转成向量EmbeddingModelDashScope 的 text-embedding-v3向量存储存向量并支持相似度查询VectorStorePGVectorStore、RedisVectorStore生成回答把检索结果拼进 Prompt 让模型回答ChatClient通义千问 qwen-plus / qwen-turbo这里面最容易让新手翻车的点是VectorStore 和 EmbeddingModel 的向量维度必须一致。比如 text-embedding-v3 输出的是 1024 维向量那你在建 PGVector 表时如果指定了 1536 维写入时就会报维度不匹配。另外说一个常见的认知误区很多同学一听知识库就想到知识图谱或 ontology想着要不要先建实体关系图。这属于 RAG 知识库与结构知识库的选型问题。做毕设单文档问答向量库就够了只有当你要回答“多跳推理”类问题、且数据本身有强关系结构时才值得引入 kg。那个工作量不是一两周能补完的别给自己挖坑。2.3 最小工程结构先想清楚文件怎么摆再动手写代码我一般会建议按下面的目录结构来组织源码包直接拿过来改也行但建议你至少理解每个文件为什么存在spring-ai-rag/ ├── pom.xml ├── src/main/resources/ │ ├── application.yml │ └── prompt/ │ └── rag-system.st ├── src/main/java/com/example/rag/ │ ├── RagApplication.java │ ├── config/ │ │ └── VectorStoreConfig.java │ ├── controller/ │ │ └── ChatController.java │ ├── service/ │ │ ├── DocumentIngestionService.java │ │ └── RagChatService.java │ └── datasource/ │ └── DataInitializer.java最关键的只有三个类加一个配置文件。DocumentIngestionService 负责把文档灌进向量库RagChatService 负责问答时检索加生成VectorStoreConfig 负责声明向量库客户端。你先把这个骨架搭起来再往里面填代码比拿到一堆源码文件却不知道从哪看起要高效得多。3. 把本地文档做成知识库读取、切分、向量化的完整流程3.1 加载文档DocumentReader 读取 pdf 和 txt 的最小代码这一步的目标只有一个把磁盘上的文件变成框架里的 Document 对象列表。Document 在 Spring AI 里就是“一段文本 元数据”它本身不关心文件格式。下面给一个典型的读取 pdf 和 txt 的方法。Service public class DocumentIngestionService { private final VectorStore vectorStore; public DocumentIngestionService(VectorStore vectorStore) { this.vectorStore vectorStore; } public void ingestPdf(String path) { // 常见做法是用 Spring AI 提供的 PdfDocumentReader它会自动分页读取 PdfDocumentReader reader new PdfDocumentReader(new FileSystemResource(path)); ListDocument docs reader.get(); processDocuments(docs, pdf); } public void ingestText(String path) { // 普通文本不存在分页问题整篇读进来后面交给切分器 TextDocumentReader reader new TextDocumentReader(new FileSystemResource(path)); ListDocument docs reader.get(); processDocuments(docs, txt); } private void processDocuments(ListDocument docs, String sourceType) { // 随后统一做切分、向量化、写入向量库 ListDocument splitDocs new TokenTextSplitter().apply(docs); // 给文档打上来源标签后期排查检索结果时能知道命中来自哪个文件 splitDocs.forEach(doc - { doc.getMetadata().put(sourceType, sourceType); doc.getMetadata().put(filePath, (String) doc.getMetadata().get(file_name)); }); vectorStore.add(splitDocs); } }这个代码里有几个点要说明。FileSystemResource 的路径在 Windows 和 mac 上写法不一样Windows 用C:/data/xx.pdfmac/Linux 用/Users/xx/data/xx.pdf用相对路径最省事。PdfDocumentReader 在读取扫描版 PDF 时是拿不到文字的它只抽文本层扫描件需要先过 OCR这个我在避坑章节再细说。最后 vectorStore.add 是一次性批量写入数据量大时可以分批。3.2 切分文档TokenTextSplitter 是 RAG 效果的第一道分水岭很多线上 rag 问答效果差的根源不是模型不行而是切分得稀碎。TokenTextSplitter 的典型配置如下Bean public TextSplitter textSplitter() { return new TokenTextSplitter( 800, // 每个文本块的目标 token 数 200, // 块与块之间重叠的 token 数 5, // 最小块长度低于这个值会被丢弃 5000, // 单次调用模型的最大 token 数防止超限 true // 是否保留与来源文档的关联信息 ); }参数怎么理解第一个值 chunkSize 决定检索的“颗粒度”。太大比如 2000 token切出来的块包含太多无关信息相似度检索时噪音多太小比如 200 token又容易把一句话的上下文切断模型拿到的是残缺逻辑。我一般把默认值设在 600 到 1000 之间然后看具体文档类型再调。第二个值 overlap 是重叠量它存在的意义是防止“上一块的结尾”恰好是“下一块的开头句话”被切断。实践里 overlap 设为 chunkSize 的 15% 到 25% 比较常见。切分粒度直接决定后面的召回质量这块值得多花点时间拿真实文档做实验而不是照抄默认参数。如果你想在 mac 上搭建 rag 知识库做实验用小一点的 pdf 先跑通再上大文档。3.3 写入向量库PGVector 配置和维度对齐向量库选择上毕设场景最稳的是 PGVector因为你有现成的 PostgreSQL不用额外引入一套专用向量数据库。下面是一个在 Spring AI Alibaba 中配置 PGVectorStore 的常见做法Configuration public class VectorStoreConfig { Bean public VectorStore vectorStore(DataSource dataSource, EmbeddingModel embeddingModel) { // 建表语句会在首次初始化时自动执行前提是数据库里创建了对应 schema return new PgVectorStore( dataSource, embeddingModel, PgVectorStore.PgVectorIndexConfig.builder() .withIndexType(PgVectorStore.PgVectorIndexType.HNSW) .withDimensions(1024) .build() ); } }这段配置里withDimensions(1024) 必须与你用的 EmbeddingModel 输出维度严格一致。DashScope 的 text-embedding-v3 默认是 1024 维如果你的 EmbeddingModel 用的是 v2 版本那是 1536 维直接写 1024 会在首次写入时报错。HNSW 索引适合数据量在几十万条以内的场景查询快但构建时耗内存数据量很小也可以用 IVFFlat实验对比下来差异不大。对应的 application.yml 里数据源配置和平常的 Spring Boot 项目一样关键是别忘把向量库插件打开。PGVector 在 PostgreSQL 里是一个扩展首次使用前需要执行建扩展的语句常见做法是在 DataInitializer 里加一行。Component public class DataInitializer implements ApplicationRunner { private final JdbcTemplate jdbcTemplate; Override public void run(ApplicationArguments args) { // 仅对 PostgreSQL 生效MySQL 不支持这个语法 jdbcTemplate.execute(CREATE EXTENSION IF NOT EXISTS vector); } }这一步很多人会漏导致启动时报 “column embedding does not exist” 之类的错其实不是代码问题是扩展没装。4. 把检索和回答串起来ChatClient 是问答主流程的入口4.1 先检索再拼 PromptRAG 的核心调用顺序问答服务是 RAG 真正面向用户的部分。流程一定是“先向量检索后模型生成”顺序不能反。下面是最常见的实现方式我加上了检索结果的打印方便你调试时看到底命中了什么。Service public class RagChatService { private final VectorStore vectorStore; private final ChatClient chatClient; public RagChatService(VectorStore vectorStore, ChatClient chatClient) { this.vectorStore vectorStore; this.chatClient chatClient; } public String answer(String question) { // 第一步向量检索效果不好先看这里 ListDocument hits vectorStore.similaritySearch( SearchRequest.builder() .query(question) .topK(5) .similarityThreshold(0.5) .build() ); // 调试时把命中片段打出来看检索到了什么是调优的第一步 hits.forEach(doc - { System.out.println(命中片段: doc.getContent()); System.out.println(来源文件: doc.getMetadata().get(filePath)); }); // 第二步把命中结果拼成上下文 String context hits.stream() .map(Document::getText) .collect(Collectors.joining(\n\n---\n\n)); // 第三步让模型基于上下文回答 return chatClient.prompt() .system(spec - spec.text( 你是一个知识库问答助手。请只根据下面提供的资料回答问题。 如果资料中没有答案请直接回答“资料中未找到相关信息”不要编造。 资料 {context} 问题{question} ) .param(context, context) .param(question, question)) .call() .content(); } }这个代码里有三个参数需要认真理解。topK 是召回条数它决定有多少文本块被塞给模型回答。topK 太大会让 Prompt 很长既费 token 又容易让模型抓不住重点太小则可能漏掉答案。我从 5 开始调如果发现模型老是答“未找到”先加大 topK 到 8 试试如果答得东拉西扯再降回来。similarityThreshold 是相似度阈值低于这个分数的文本块会被过滤掉。它的作用是防止“明明没相关内容硬要拿一堆低质量片段凑数”。这个值非常敏感设 0.7 可能把有效答案过滤掉设 0.3 又会放进一堆垃圾。我在真实场景下一般先不设阈值只看返回的相似度分数分布再决定阈值。另外一个值得注意的点这个 Prompt 里明确写了“如果资料中没有答案就直说”。这是 RAG 系统防止幻觉的最重要一道闸门千万别省。答辩时老师会专门问这个问题你可以顺势讲出“模型只会基于提供的资料作答不依赖内部记忆”这句话。4.2 给问答加一个 HTTP 入口Controller 怎么接有了 Service 之后Controller 非常简单但这层往往是演示时最出效果的部分。代码如下RestController RequestMapping(/api/chat) public class ChatController { private final RagChatService ragChatService; public ChatController(RagChatService ragChatService) { this.ragChatService ragChatService; } PostMapping(/ask) public MapString, Object ask(RequestBody MapString, String payload) { String question payload.get(question); String answer ragChatService.answer(question); return Map.of( question, question, answer, answer, timestamp, System.currentTimeMillis() ); } GetMapping(/health) public String health() { return ok; } }建议你在演示时先用 Swagger 或直接 curl 调用这个接口再打开前端页面。这样如果前端有问题你不会误以为是后端挂了。我在本地验证时常用这条命令curl -X POST http://localhost:8080/api/chat/ask \ -H Content-Type: application/json \ -d {question: 这个项目的技术栈是什么}返回的 JSON 里如果 answer 字段引用了你资料里的原话说明整条链路通了。4.3 一次完整的问答链路里模型到底看到了什么这个问题我建议每个做毕设的同学都要能在答辩时讲清楚。用户提问后系统做三件事把问题转成向量在 PGVector 里搜索最接近的 5 个文本块把这 5 块文本拼进 system prompt再把用户的问题作为 user message 发给 qwen 模型。以“这个项目的技术栈是什么”为例模型实际看到的 Prompt 是这样的你是知识库问答助手。 资料 【块1】项目采用 Spring AI Alibaba 作为后端框架... 【块2】前端使用 Vue3...后端使用 Java 17... 问题这个项目的技术栈是什么模型不会直接访问你的数据库它只是“读”了你给的资料然后作答。这也解释了为什么 RAG 系统的效果天花板取决于检索质量——模型再强资料没检索到它也答不出来。这就是所谓 rag 瓶颈几乎每个真实系统都要面对。你把这个道理讲清楚答辩老师就知道你不是只会调 API。5. RAG 智能问答系统的避坑指南现象、原因、解决这一章是我最想写给后来者的部分。以下每一条都是实际操作中高概率遇到的问题按“现象→原因→解决”的格式来写方便你排查时对照。5.1 api-key 配置了但请求时报 InvalidApiKey现象项目能启动但第一次调用问答接口时抛异常日志里出现类似 “InvalidApiKey” 或 “InvalidParameter” 的字样。原因绝大多数情况是 application.yml 里的 key 写错了位置或者 key 前后有空格。Spring AI Alibaba 的配置前缀通常是spring.ai.alibaba.tongyi.api-key但如果你同时引入了 Spring AI 的 OpenAI 适配器两个配置前缀可能冲突。解决检查配置文件确认没有多余空格并确认你用的 key 是 DashScope 的 API Key 而不是阿里云账号的 AccessKey。这两个东西长得像但完全不一样。如果确认无误重启服务试试因为有些版本的配置读取是启动时加载一次的。5.2 向量写入时报维度不匹配现象执行 vectorStore.add 时控制台报 “expected 1024 dimensions, but got 1536” 或相反的错误。原因EmbeddingModel 输出的向量维度和你建表时指定的维度不一致。这通常是因为你换了 embedding 模型但 PGVector 表是用旧维度创建的。解决确认当前使用的 embedding 模型维度删除旧的向量表重建。常见做法是在 DataInitializer 里加一行删除旧表的逻辑或者直接到数据库里执行DROP TABLE IF EXISTS vector_store然后重启服务让它重新建表。千万别在生产环境这么干但毕设阶段无所谓。5.3 文档加载成功但检索结果永远答非所问现象问“系统支持哪些用户角色”模型回答一段完全不相关内容或者答“资料中未找到相关信息”但你确定文档里有这段描述。原因这是最常见的 rag 瓶颈。问题出在切分粒度或检索阈值上而不是模型。比如 chunkSize 设得太大答案藏在某个大块的中间相似度计算时被其他文字稀释了或者 similarityThreshold 设得过高真正的答案块被过滤掉了。解决先把 similarityThreshold 去掉或设成 0把 topK 调到 10打印所有命中的片段和分数人眼判断哪些是相关块。如果相关块分数很高但仍然答错问题在 Prompt 拼接如果相关块根本没被召回问题在切分。5.4 mac 上 PostgreSQL 启动失败服务一直起不来现象在 mac 本地开发启动 Spring Boot 时报连接数据库失败检查发现 PostgreSQL 服务没起来或者psql命令找不到。原因mac 上 PostgreSQL 需要单独安装常见的 Homebrew 安装方式不会自动启动服务。另外 Spring Boot 默认的用户名密码是postgres/postgres如果本地数据库没设置密码连接会失败。解决依次执行brew install postgresql、brew services start postgresql然后进入 psql 创建用户和数据库把连接串里的密码改成实际的。另外注意 PGVector 扩展在 mac 上也需要安装对应依赖否则CREATE EXTENSION会报找不到文件此时需要brew install pgvector。5.5 模型回答得很流畅但内容完全不在点上现象系统能正常回复语句通顺但答案和你知识库里的内容毫无关系像在自由发挥。原因大概率是 Prompt 模板里没有写“仅凭资料作答”的强约束模型把知识库内容当成了参考而不是唯一依据。另一种可能是你检索出来的片段本身就跑偏了但模型润色得很好掩盖了问题。解决把 system prompt 里的约束从“请参考资料回答”改成“请只根据资料回答资料中未提及的内容一律不要输出”。同时回到第 4.1 节的调试方法确认检索片段本身是正确的。这属于 RAG 里典型的“生成掩盖检索问题”你不看中间结果永远发现不了。6. 进阶先测检索、再测生成的调试习惯能让你的系统少走一半弯路最后一个想分享的技巧是我自己踩了不少坑换来的习惯任何 RAG 系统都要把“检索质量”和“生成质量”分开验证。很多同学在最终效果不好时第一反应是换模型、调 Prompt却忘了问题可能出在检索环节。你可以在项目里加一个专门的调试接口或者直接写个 main 方法先脱离 ChatClient 单独跑相似度检索把命中的片段和分数打印出来看。这里给一张我平时调参用的对照表按场景选择起点再微调症状先调哪个参数调整方向答非所问命中片段相关性差topK加大到 8若仍然不行检查切分能找到答案但夹带大量无关内容similarityThreshold从 0 逐步上调到 0.5 附近答案太碎缺少上下文chunkSize 与 overlap增大 chunkSize 或 overlap回答“未找到”但文档里明明有similarityThreshold先调低到 0.2若恢复再逐步回调我自己现在的做法是每次改完切分或向量库配置先跑 5 个固定问题人眼检查检索结果再把同样的 5 个问题走一遍完整问答对比两次输出差异。这个方法笨但极其可靠因为检索结果是参数调优最直接的反馈信号。最后说句实在话这个方向做完并不难但做好需要你对每一个环节都较真。我在最初调试时也走过不少弯路最大的教训就是“不要急着跑去调模型参数先把检索结果打印出来看一眼”。希望这些经验能帮你少踩一些坑把毕业设计的时间花在刀刃上而不是浪费在和数据库连接和维度报错死磕上。本文还有配套的精品资源点击获取