Spring AI Alibaba 构建 RAG 智能问答系统实操全解析
简介基于Spring AI Alibaba的RAG智能问答系统毕业设计/课程设计源码包面向计算机、电子信息等专业学生用于课程设计、期末大作业或毕设参考。项目将Spring框架与阿里云数据库产品结合实现检索增强生成问答流程涵盖后端接口、数据处理与检索、文本生成等关键模块。包体共14个文件以5个Java源文件为主辅以properties配置、XML工程配置、README说明及Maven包装脚本整体仅17KB结构紧凑便于直接阅读与二次改造。目前已有143人学习下载。通过研读源码可理解系统整体架构与RAG链路设计掌握Spring集成、数据库访问、检索与生成等落地技能也可借此拓展到客服、在线教育等智能问答应用场景。1. 毕设做 RAG 智能问答为什么我把 Spring AI Alibaba 排在方案第一位毕设或课设选「基于 Spring AI Alibaba 的 RAG 智能问答系统」这个题目说明你已经跳过了纯 Chatbot 的俗套——不带知识库的问答在答辩现场太容易被追问打穿而 RAG检索增强生成恰好是「大模型 工程能力」的组合点。Spring AI Alibaba 的价值在于它把通义千问的调用、向量化、向量检索、提示词组装都收敛到 Spring Boot 的编程模型里你不需要自己拼 HTTP 请求、管会话、写重试。对 Java 技术栈的学生来说这是当前在毕设周期内把 RAG 跑通并讲清楚的最短路径。这篇笔记按我实际做类似项目的顺序来写先弄清链路选型再落代码最后解决检索质量和答辩验证。2. 先别写代码把 RAG 链路在 Spring AI Alibaba 里跑对的组件选型2.1 从文档到回答的四个环节哪个环节决定毕设能不能过RAG 看着是「检索 生成」拆开其实是四个环节文档切片、向量化Embedding、向量检索、提示词组装与大模型生成。很多第一次做的人把精力全压在最后一个环节——调 prompt 让模型「好好回答」结果发现答案还是不对。真实情况是前三个环节里只要有一个出问题后面怎么调 prompt 都救不回来。Spring AI Alibaba 在这四个环节上都有对应组件DocumentReader负责读文档TokenTextSplitter负责切片EmbeddingModel负责向量化DashScope 的 embedding 模型VectorStore负责存储和检索。最后生成阶段它内置了一个QuestionAnswerAdvisor这个组件是 RAG 问答的胶水——它拿到用户问题后去向量库检索再把检索到的片段塞进 prompt让模型基于这些片段回答。我见过不少毕设翻车是因为没搞清楚这个顺序先写了 ChatClient 调模型发现回答得不错于是以为 RAG 已经做完了。实际上那只是「有知识库的普通对话」模型回答里没有一条是真正来自你指定资料的。判断标准很简单把知识库文件删掉如果回答内容不变说明你的 RAG 链路根本没生效。这个验证方法后面会反复用到。2.2 技术选型对照Spring AI Alibaba 与裸调 SDK、LangChain4j 的取舍在 Spring Boot 工程里做 RAG常见有三条路直接用 DashScope 的 HTTP SDK、用 LangChain4j、用 Spring AI Alibaba。我倾向于 Spring AI Alibaba理由不是它最强大而是它最适合毕设/课设这个周期。直接调 DashScope SDK 的问题是所有东西都要自己拼文档切分逻辑、提示词模板、召回历史、向量存储。这些加起来的工作量能占整个项目的 60% 以上而且大多是重复劳动。LangChain4j 功能全模型抽象丰富但它的 API 风格和 Spring 生态的融合度一般很多同学装完依赖后光版本冲突就折腾一周。Spring AI Alibaba 走的是 Spring 官方抽象ChatClient是链式调用风格VectorStore是统一接口换存储后端不用改业务代码。对比项裸调 DashScope SDKLangChain4jSpring AI Alibaba学习成本低但要重复造轮子中高概念多中低Spring 风格统一与 Spring Boot 集成手动管理需额外适配原生支持向量存储抽象无有但配置偏重有本地/远程可切换答辩可讲的技术深度偏浅偏散聚焦在 RAG 链路本身还有一个现实因素答辩老师问「为什么选这个方案」时Spring AI Alibaba 的答案很有说服力——它是阿里在 Spring AI 官方规范上的实现底层模型用通义千问国内可访问、有免费额度整个链路都是 Java 生态标准。这个选型本身就是一个能讲 5 分钟的答辩点。3. 落地一版可答辩的 RAG 问答依赖、配置、入库与 ChatClient 四段代码3.1 工程骨架与 Maven 依赖按最小集来配如果你拿到的压缩包里已经带了后端工程先别急着跑第一步是确认版本对齐。Spring AI Alibaba 对 Spring Boot 版本有要求当前主流是 3.2 以上的 Boot如果你本地是 Spring Boot 2.x直接换主题会更省事。我一般会新建一个干净的 Spring Boot 工程再引入依赖避免被包内自带的旧配置带偏。parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.3.5/version relativePath/ /parent dependencies !-- Spring AI Alibaba 官方 starter同时引入 DashScope Chat Embedding -- dependency groupIdcom.alibaba.cloud.ai/groupId artifactIdspring-ai-alibaba-starter/artifactId version${spring-ai-alibaba.version}/version /dependency !-- 本地向量库实现不依赖外部中间件适合毕设演示 -- dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-simple-vector-store/artifactId version${spring-ai.version}/version /dependency !-- 读取本地文件TextReader 依赖 Spring 的资源抽象 -- dependency groupIdorg.springframework/groupId artifactIdspring-core/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency /dependencies版本号用${spring-ai-alibaba.version}占位而不是写死是因为这个项目发布节奏比较快写死容易踩兼容坑。用 Spring Initializr 或阿里云开发者工具创建工程时选带 Spring AI 的版本即可。这段依赖里最重要的是前两个spring-ai-alibaba-starter是主入口它会把 DashScope 的 ChatModel、EmbeddingModel 自动装配成 Spring Beanspring-ai-simple-vector-store是本地向量库数据存在内存或本地文件里不需要额外装 Redis 或 Elasticsearch。这对毕设很关键——答辩现场的电脑不一定有 Docker外部中间件容易当场翻车。3.2 配置通义千问模型与向量库application.yml 三个必调参数拿到工程后大部分人的配置会卡在 api-key 和模型名上。DashScope 的 key 从阿里云百炼控制台申请长期有效免费额度够毕设用。application.yml 里最核心的配置如下spring: ai: dashscope: api-key: ${DASHSCOPE_API_KEY} # 500 万 token 额度的输入尺寸长文本场景比 qwen-turbo 更稳 chat: options: model: qwen-plus temperature: 0.3 embedding: options: model: text-embedding-v3 vectorstore: simple: # 本地向量库持久化目录不配置则纯内存重启丢数据 persist-path: ./data/vector-store.json三个必调参数分别是api-key用环境变量注入不要硬编码到代码里、chat.options.modelqwen-plus 比 qwen-turbo 的指令遵循能力好回答格式更可控、embedding.options.modeltext-embedding-v3 是当前 DashScope 主推的向量模型支持 1024/768/512 维输出默认 1024。temperature设到 0.3 以下知识问答场景不需要模型发挥想象力温度越低回答越贴近给定资料。persist-path这个配置是血泪教训——默认的 SimpleVectorStore 是纯内存存储重启应用后所有向量数据清空你得重新跑一遍入库脚本。加上持久化路径后数据落盘重启还在。3.3 文档切分与入库本地 TXT 转成可检索向量的最小链路知识库入库这一步常见做法是写一个CommandLineRunner应用启动时自动读取指定目录下的文档切分后向量化并写入 VectorStore。最小实现如下Component public class KnowledgeBaseInitializer implements CommandLineRunner { private final EmbeddingModel embeddingModel; private final VectorStore vectorStore; public KnowledgeBaseInitializer(EmbeddingModel embeddingModel, VectorStore vectorStore) { this.embeddingModel embeddingModel; this.vectorStore vectorStore; } Override public void run(String... args) throws Exception { // 读取 resources/docs 下的知识文档 Resource resource new FileSystemResource(data/knowledge-base.txt); TextReader textReader new TextReader(resource); textReader.setCharset(StandardCharsets.UTF_8); Document document textReader.read(); // 切片每片 500 tokenoverlap 50兼顾上下文连贯和检索精度 TokenTextSplitter splitter TokenTextSplitter.builder() .withMaxTokensPerChunk(500) .withOverlapTokens(50) .build(); ListDocument chunks splitter.apply(List.of(document)); // 为每个切片附加来源信息便于检索后追溯 chunks.forEach(chunk - chunk.getMetadata().put(source, knowledge-base.txt)); vectorStore.write(chunks); System.out.println(知识库入库完成 chunks.size() 个切片); } }TextReader是 Spring AI 内置的文本文件读取器会把整个文件读成一个Document对象。TokenTextSplitter按 token 数切分withMaxTokensPerChunk(500)表示每片最大 500 token——这个数值是我在毕设项目中调过多次的折中值太小100~200会切断完整语义太大1000会导致检索命中时返回过多不相关内容。withOverlapTokens(50)是相邻切片的重叠 token 数用来缓解切分把关键词从中间切断的问题。vectorStore.write(chunks)是自动完成的它会先调用EmbeddingModel给每个切片生成向量再写入向量库。你不需要关心向量化过程的细节这是 Spring AI 抽象层的功劳。mac 上跑这套链路基本零成本只要 JDK 17 以上、堆内存给到 1G 以上即可SimpleVectorStore 是内存式计算不依赖 Docker 和网络服务。3.4 问答接口检索、拼 Prompt、让模型只按本地资料说话问答接口是核心也是答辩时演示的主战场。我直接用ChatClient链式 API QuestionAnswerAdvisor完成整个「检索增强生成」流程RestController RequestMapping(/api/qa) public class QaController { private final ChatClient chatClient; public QaController(ChatClient.Builder builder, VectorStore vectorStore) { // QuestionAnswerAdvisor 会自动完成检索向量库 → 拼入 prompt → 调用模型 this.chatClient builder .defaultAdvisors(new QuestionAnswerAdvisor(vectorStore)) .build(); } PostMapping(/ask) public String ask(RequestBody QuestionRequest request) { String answer chatClient.prompt() .user(request.question()) .call() .content(); return answer; } }QuestionAnswerAdvisor是这个接口的「幕后功臣」。它内部做的事是接收用户问题 → 用EmbeddingModel把问题向量化 → 到VectorStore里做相似度检索 → 取出 topK 个文档片段 → 按固定模板组装成「参考资料 问题」的完整 prompt → 交给 ChatClient 调用模型。整个过程你只需要配置一个VectorStore实例。关键点是ChatClient.Builder是 Spring AI 自动装配的VectorStore是我们在 3.1 里通过spring-ai-simple-vector-store自动装配出来的两个 Bean 都能直接注入。如果你需要强制模型「只依据资料回答」可以在 prompt 上追加一句「如果参考资料中没有答案直接说『资料中未找到相关信息』不要编造」。这是应付幻觉问题的第一道防线。提示QuestionAnswerAdvisor的默认行为是「检索到多少就用多少」不会自动阻止模型补充外部知识。想在答辩中展示严谨性请在 system prompt 里显式声明只能依据给定资料回答。4. 让它答非所问的 5 个坑RAG 系统常见的翻车点与排查路径4.1 现象一模型总能答但答案完全来自幻觉最典型的翻车场景你问知识库里没有的问题模型照样答得头头是道。原因不是模型不行而是你的链路上QuestionAnswerAdvisor根本没生效——常见于手动构造 Prompt 而不是通过ChatClient的 advisors 机制或者VectorStore是空的入库代码没跑成功。解决路径很直接在接口里临时返回一次检索结果直接调用vectorStore.similaritySearch(你的问题)看有没有数据返回。如果返回为空检查入库逻辑的日志输出如果返回有数据但回答仍幻觉检查 prompt 里是否加入了「仅依据以下资料回答」的约束。4.2 现象二提示词里已经有知识了答案还是旧资料典型场景是修改了知识库文件重新入库后回答内容没变化。原因是向量库里新旧数据同时存在检索可能命中了旧切片。SimpleVectorStore 的write方法默认是追加不是覆盖。解决办法是入库前按条件删除旧数据先vectorStore.delete(List.of(knowledge-base.txt))按 3.3 中写入的 source 元数据删除再执行写入。更稳妥的做法是给每个切片生成唯一的 docId更新时先删后写形成幂等操作。4.3 现象三分片一大检索命中率暴跌有次我把每片切成 1500 token原因是「这样模型能看更多上下文」。结果检索到的片段和问题明显无关——因为嵌入向量是对整片内容做的平均化一个片段里包含 3 个主题时任何单个主题的语义都被稀释了。切片的本质是「让每个向量代表一个单一主题」500 token 是我在中文场景下的常用起点。如果你发现答非所问先把分片长度减半测试同时检查是否按段落边界切分而不是硬按字符数截断。4.4 现象四向量库本地跑通换台机器就查不到数据如果你配置了persist-path本地向量库会保存到文件但换电脑时这个文件不会跟着走。毕设答辩经常发生的情况是在宿舍电脑上入库成功到实验室电脑上拉代码后直接跑回答永远搜不到资料。解决路径是让入库逻辑「启动时自动执行」——3.3 里的CommandLineRunner就是为此设计的每次启动检查向量库是否为空为空则自动重建。另外注意.gitignore里把./data/目录排除掉不要把本地向量文件提交到 Git 仓库那是环境产物不是源代码。4.5 现象五eval 数据测着准真用户一问就崩自己拿 5 条「精心设计」的问题测回答都很好换同学随口问一句模型开始胡说。这不是模型问题是测评方法问题——测试问题全部来自你已经知道答案的知识库范围等于开卷考试。RAG 系统公认的瓶颈就在这里缺少一套「未知问题」的验证集。我后来养成的习惯是准备两类测试集一类是知识库内问题的标准答案对照一类是明确超出知识库范围的「无答案问题」专测模型会不会老实说不知道。5. 从「能答」到「答对」检索质量、切片参数与知识库形态的取舍5.1 命中率上不去的第一瓶颈embedding 模型与文本长度的关系当你发现答案「能答但不对」第一反应不是换大模型而是看检索回来的片段到底对不对。很多项目的实际情况是生成模型没问题检索环节把不相关的文本当成了依据。检索质量的瓶颈核心在 Embedding 模型对长文本的语义压缩能力。DashScope 的text-embedding-v3在长文本上表现还可以但它同样遵循一个规律文本越长向量里「平均语义」越明显越难精确匹配用户问题。我的调参顺序是先把单条检索到的片段打印出来人工看一遍再决定改什么。如果片段相关但不够精准说明切片粒度可能偏大如果片段完全不相关优先检查 embedding 模型是否选对再看问题本身的表述是否和知识库文档风格差别过大。还有一个容易被忽略的点embedding 模型对中文缩略词、俗称的匹配能力一般知识库文档里写「人工智能」用户问「AI」向量检索可能匹配不上。解决方案是在入库前对文档做一次术语归一化或检索时对问题做同义扩展。5.2 切片策略的调参顺序先定语义边界再调 overlap切片参数看似简单实际是 RAG 调优中最玄学的一块。我总结的顺序是第一优先按文档本身的语义边界切——比如 Markdown 的二级标题、TXT 的段落空行用这些做天然切片点第二再设置 maxTokens保证单片在 300~800 token 之间第三最后调 overlap 参数默认 50~100 token 即可。overlap 的意义不是「多存点内容」而是防止切片边界恰好切断一个完整的语义单元。比如一句话跨越了两个切片边界两边各存一半检索时都匹配不到完整含义。overlap 设置过大会造成冗余存储和检索噪声50 到 100 是充分的。另外注意overlap 只会缓解问题不会根治问题。真正要做的还是第一步——语义边界优先而不是孤立地调数字。5.3 向量库检索的 topK 与相似度阈值怎么判断召回结果真的相关检索参数里最影响回答质量的是topK和similarityThreshold。topK 默认是 4表示拿回 4 个片段拼进 prompt。topK 不是越大越好——片段越多prompt 越长模型反而容易被不相关信息干扰。我用QuestionAnswerAdvisor时通常会这样显式构造检索条件Bean public Advisor questionAnswerAdvisor(VectorStore vectorStore) { SearchRequest searchRequest SearchRequest.builder() .query() .topK(5) .similarityThreshold(0.5) .build(); return new QuestionAnswerAdvisor(vectorStore, searchRequest); }similarityThreshold是相似度下限低于这个值的片段会被过滤掉。0.5 是我在中文场景下的常用起点如果你的知识库文档风格统一可以调到 0.6 减少噪声如果文档风格多样0.4 更保险。调这个参数的最快方法是打印检索结果的分数和内容肉眼判断这条阈值下「混进多少无关文本」。在 3.4 的 QaController 里临时加一行日志输出检索结果就能看到每个候选片段的相似度分数这是最直接的排错手段。5.4 什么时候要上结构化的知识库用知识库形态区分当切入点做完整套 RAG 之后答辩老师大概率会问「你这套方案对于所有文档都适用吗如果知识库是表格、JSON、或带关系的数据怎么办」这个问题回答得好能拉开明显差距。文本向量库适合非结构化资料——PDF、TXT、Markdown 这类叙事文本但如果你要答的是「某个字段的取值」「两实体之间的关系」纯向量检索会力不从心。结构知识库比如数据库表、图数据库、本体 ontology处理的是确定性的查询字段值过滤、关系查询、聚合统计。RAG 知识库和结构知识库的区分点就在这里——前者处理「语义相近但表述不固定」的问题后者处理「完全确定」的问题。真实项目里两者常混用先用结构化查询缩小范围再用 RAG 做生成。毕设里不需要真正实现图谱但如果你在文档里做了「知识库形态区分与选型分析」这一小节答辩时就能展示你清楚方案的前提条件和边界而不是只会调接口。6. 答辩前做一个可重复的评估脚本召回率、无答案测试与引用溯源RAG 系统不能靠感觉验收。临答辩前一天我会固定跑一套自动化评估脚本用最朴素的方式量化回答质量。脚本逻辑很简单准备 20 条来自知识库的 QA 对和 10 条无答案问题循环调用/api/qa/ask接口把返回的 answer 和标准答案做关键词重叠度计算同时记录每次检索命中的片段来源。import requests import re qa_pairs [ (什么是 RAG, 检索增强生成, knowledge-base.txt), # ... 更多来自知识库的问答对 ] unanswerable [你们系统支持实时翻译吗, 其他产品的报价是多少, ...] def keyword_overlap(answer: str, reference: str) - float: ref_words set(re.findall(r[\u4e00-\u9fa5], reference)) ans_words set(re.findall(r[\u4e00-\u9fa5], answer)) return len(ref_words ans_words) / max(1, len(ref_words)) for question, reference, _ in qa_pairs: resp requests.post(http://localhost:8080/api/qa/ask, json{question: question}).json() overlap keyword_overlap(resp[answer], reference) print(f[{overlap:.2f}] Q: {question}) for question in unanswerable: resp requests.post(http://localhost:8080/api/qa/ask, json{question: question}).json() if 未找到 not in resp[answer]: print(f[FAIL] 无答案问题泄漏: {question})这套脚本能反映两个核心指标对库内问题关键词重叠度高于 0.6 视为及格对库外问题凡是输出了一本正经的答案都视为失败。无答案测试尤为重要——它直接验证 prompt 约束是否生效也是答辩时老师最爱追问的场景。跑完脚本后我会把「20 条库内问题通过、10 条无答案问题拒绝」这个数字写进答辩 PPT远比口头说「效果不错」有力。如果你想让答辩内容更出彩可以再加一步引用溯源——让返回结果带上知识来源。常见做法是在检索时把每个切片的source元数据3.3 中写入的 knowledge-base.txt提取出来拼到回答末尾的「参考资料」字段。这样老师问「你这条回答的依据是什么」你可以直接展示文档出处证明回答不是模型编造的。我有一个教训有次做演示前没测无答案问题现场老师问了一个 2024 年之后发生的事知识库停在 2023 年模型却默认自己知道当场答错场面一度很僵硬。从此我把「库外问题测试」列成必跑项。做 RAG 毕设最值得投入的不是把模型换得更大而是把检索质量和验证闭环做好——希望你这次少踩我踩过的坑。本文还有配套的精品资源点击获取