资讯动态

Spring AI RAG实战:构建私有知识库问答系统的完整指南

发布时间:2026/10/17 7:56:55 来源:尧图企业网站定制
直接开始讲今天的重点Spring AI 里的 RAG 检索实战。如果你一直在关注这类人工智能应用开发应该对“私有知识库”这个场景不陌生——企业内部的规章制度、产品文档、客服话术、历史故障记录这些东西大模型没学过但业务又必须基于它们来回答。RAGRetrieval-Augmented Generation检索增强生成就是解决这个问题的标准套路。这篇文章我会完全围绕一个可运行的实战项目来拆解告诉你知识库是怎么构建的、检索和回答是怎么串起来的以及我在实际调试中踩过的那些坑。适合的人群是已经跑通过 Spring AI 基础调用、想进一步做企业级问答应用的开发者。我会把整个实现分成几个层次来讲先讲 RAG 这条链路里每个环节存在的意义再讲项目用了哪些组件和依赖接着是核心代码怎么落地最后是在真实环境里会遇到哪些奇奇怪怪的问题。如果你之前只是零散地看过概念这篇应该能帮你把整个流程串成一条完整的线。1. 先搞清楚 RAG 到底解决的是什么问题1.1 大模型的“记忆边界”和私有知识的鸿沟任何一个用过对话模型的人都会发现一个现象你问它公开的常识、知名的技术框架它能回答得头头是道但你问它“我们部门的请假流程是什么”“上个月那起线上故障的处理记录里有哪些结论”它就傻眼了。根本原因不是模型笨而是模型的参数里压根没有这些私有数据。训练一个模型需要海量语料和极高的算力成本没有任何一家企业会把内部知识拿去做预训练——既不划算也不安全。既然模型不知道那最常见的粗暴方案是“把文档拼进提示词里一起发给模型”。这个方案在数据量小的时候确实能凑合用比如你有一份 500 字的制度说明直接拼进去就完事了。可一旦知识库里躺着几百份文档、几十万字提示词根本装不下即便强行塞进去模型也会被大量无关信息干扰回答质量反而急剧下降。我们需要一种机制能够“按需”从知识库里找到和用户问题相关的那一小部分内容只把这一部分交给模型去理解。这就是 RAG 的核心逻辑。1.2 RAG 和微调的本质区别很多刚接触的人会纠结同样是让大模型“学会”私有知识为什么不选择微调我的理解是微调是在改变模型的权重让模型从“参数层面”记住某些知识成本高、周期长而且每次知识更新都要重新训练RAG 则完全不改变模型它只是动态地为每次回答“检索”出相关资料然后把这些资料作为上下文提供给模型。知识更新的时候只需要重新索引文档几分钟就能生效。用生活里的场景来类比微调像是让一个员工把公司制度全文背下来背错了还得重新背RAG 像是给这个员工配了一个随时能查的制度手册遇到问题先翻手册再回答。对于绝大多数企业场景“翻手册”显然比“背书”更轻量、更可控。知乎上有人问“RAG 是不是大模型应用的银弹”我的看法是它解决的是“知识缺失”和“知识更新”这两大核心命题但并不意味着可以完全替代微调。如果某个领域需要模型具备某种推理能力或表达风格微调仍然是必要的。不过对于私有知识库问答这个场景RAG 是绝对的优先方案。2. 项目基础准备技术选型和整体架构2.1 为什么选择 Spring AI 这套技术栈既然标题是 Spring AI 实战技术栈的主干自然围绕 Spring AI 展开。Spring AI 是面向人工智能应用的一个集成框架它做了一件很有价值的事把调用模型、向量化、向量存储、提示词组装这些零散的环节抽象成统一的接口让开发者不用关心各个模型厂商的差异。在这套体系里有两个角色非常关键。第一个是 EmbeddingModel专门负责把文本转换成向量——说白了就是把文字变成一长串浮点数让语义相近的文本在向量空间里位置靠近。第二个是 VectorStore负责向量的存储和相似度检索。项目中会选择哪种向量数据库、哪种嵌入模型直接决定了检索效果的“下限”。我做这个项目时选了三样东西本地 Ollama 服务用来跑嵌入模型和对话模型解决“数据不出内网”的合规问题PGVector 作为向量存储因为它是 PostgreSQL 的扩展不需要额外引入一套新数据库运维成本低Tika 负责解析 PDF、HTML、Word 等格式的文档因为企业内部资料很少是干净的纯文本。2.2 Maven 依赖与版本选择细节我开发时使用的 Spring Boot 版本是 3.2.xSpring AI 版本是 0.8.1。为什么强调版本因为 Spring AI 迭代速度极快接口变化也频繁网上很多旧教程里的代码在新版本里已经编译不过了。你如果自己新建项目务必以你实际引入的版本 API 为准而不是照着老文章抄。需要引入的核心依赖集中在三块。对话与嵌入模型相关的 Ollama 依赖负责解析各类文档的 Tika 依赖以及向量存储用的 PGVector 依赖。如果你不是用 Ollama 而是用云厂商的模型服务则换成对应的依赖即可接口层面是统一的。依赖配置大致长这样dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-ollama/artifactId version0.8.1/version /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-tika/artifactId version0.8.1/version /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-pgvector/artifactId version0.8.1/version /dependency如果你用的是 Spring AI 的 BOM 来管理版本那依赖里就不需要显式写版本号但我个人建议在早期阶段还是写清楚版本排查问题的时候更直观。另外Ollama 部分记得要在 application.yml 里配置一下指定 embedding 模型和 chat 模型。2.3 知识数据从哪里来项目里我用了一个很“复古”的数据源把一套 HTML 格式的产品帮助文档直接放到了项目的 resources/docs 目录下。为什么选 HTML因为企业内部文档最常见的形式就是网页和 PDFHTML 比纯文本更能模拟真实场景同时它在解析时会带上标题、段落结构对后续切分策略的验证也更有说服力。你完全可以用 PDF、Word 甚至 Markdown 文件来替代。需要注意的一点是不管用什么格式最终都要落到“能提取成文本”这一步。Tika 的作用就是把 PDF、HTML、Word 这些二进制或半结构化文件“榨”成纯文本。如果文档本身是扫描件图片那 Tika 也无能为力需要先走 OCR 识别流程这就是另一个话题了。项目结构上我推荐把文档放在固定目录下并保证资源目录被打进 classpath。Spring AI 的 FileSystemResource 可以直接读取文件系统路径所以文档放在项目内或外部磁盘都行只要代码里的路径写对。3. 核心实现从文档到向量再到问答3.1 文档读取把杂乱格式变成统一文本整个流程的第一步是读取文档。我最初的版本里用了一个最简陋的方式直接 File.listFiles 遍历目录把每个文件都转成 FileSystemResource然后交给 Spring AI 的 TikaDocumentReader 去解析。这里有个细节值得展开说一下TikaDocumentReader 一次只能读一个文档所以当你面对的是一个文档目录时必须自己写循环来遍历。我见过很多初学者把多个文件一次性传给 reader结果要么只读到了第一个文件要么直接报错。正确的方式是这样ListDocument documentList new ArrayList(); File[] files resourceDirectory.listFiles(); if (files ! null) { for (File file : files) { if (file.isFile() file.getName().endsWith(.html)) { TikaDocumentReader reader new TikaDocumentReader(new FileSystemResource(file)); documentList.addAll(reader.get()); } } }读取的结果是一个 Document 对象的列表。你可以把 Document 理解成 Spring AI 对“文本文档”的统一抽象它除了包含 content正文内容还有一个 metadata元数据属性用来记录文件名、文件路径等辅助信息。这些元数据在后续检索、过滤时非常有用比如你可以只检索某个部门或某类文档的内容。3.2 文本切分为什么切分策略直接影响回答质量读进来的文档如果是纯文本长度通常在几千到几万字之间。如果直接对整个文档做向量化后果是灾难性的向量维度是固定的但文档越长语义越模糊一个长文本的向量根本无法精准表达其中某一段的具体含义。检索时你会经常匹配到“好像相关但实际无关”的段落。所以必须做切分把长文档拆成若干短文本块。Spring AI 里负责这个动作的是 DocumentTransformer 接口常见实现是 TextSplitter 的子类。我用的是 TokenTextSplitter它按照 token 数来控制切分粒度。切分的参数需要根据你的实际场景调没有绝对的标准。我把默认的 chunkSize 设为 800chunkOverlap 设为 350。chunkSize 控制每个文本块的大致长度chunkOverlap 控制相邻两个块之间重叠的部分。为什么要重叠因为一个完整的意思可能恰好被切在边界上如果没有任何重叠语义就会被“拦腰斩断”检索时容易漏掉关键信息。有重叠之后同一个句子可能同时出现在前后两个块里检索命中率会高很多。文档切分还有一个隐藏的坑盲目按固定长度切会把一个完整段落拦腰截断。有些库支持按段落切分比如 detectingParagraphs 参数让切分器优先在段落边界处分割。我在项目里开了这个能力切出来的块更符合人类的阅读单元检索效果明显更好。对于一套十几页的 HTML 文档切分后的块数量通常是几十到上百不等。每块大约控制在几百 token向量化之后存储和检索的压力都不大。3.3 向量化与写入向量库PGVector 落库全流程文本块准备好之后就进入最核心的环节向量化并写入向量数据库。Spring AI 里完成这件事非常简洁核心代码如下VectorStore vectorStore new PgVectorStore(embeddingModel, jdbcTemplate); vectorStore.add(textSplittedDocuments);这里有一个很有意思的设计第一行代码在初始化 VectorStore第二行的 add 方法内部会自动调用嵌入模型为每个文本块生成向量然后把向量连同文本原始内容一起写入数据库的向量表。也就是说你不需要手动写“对每段文本调 embedding 接口、再 insert 数据库”这类胶水代码框架帮你封装好了。EmbeddingModel 用的什么模型呢我在配置里指定的是 Ollama 上的 bge-m3。这是一个开源的中英文向量模型体积适中效果在中文场景下表现不错。选它的理由很简单本地部署、免费、不需要把数据传到外部。如果你手里的机器配置更高也可以换用更大的嵌入模型如果用的是云服务商提供的 embedding 模型只要替换 Spring AI 的依赖和配置即可。PGVector 在数据库侧需要先安装扩展。如果你是第一次使用登进 PostgreSQL 后执行一行 SQL 就够CREATE EXTENSION vector;Spring AI 的 PgVectorStore 在初始化时会自动创建所需的表结构。你没有看错不需要手工建表框架帮你把表结构、索引都处理好了。我一般会建一个独立的数据库来隔离向量数据避免和业务表混在一起。有一个细节一定要提醒PgVectorStore 的初始化依赖一个 JdbcTemplate 实例如果你的项目里配置了多数据源务必确认传入的是目标数据源对应的 JdbcTemplate。我曾经在这里栽过跟头——向量数据写到了默认数据源查询的时候却连到了另一个库结果检索结果永远是空的。数据落库之后你可以顺手执行一条 SQL 看看效果向量表的每一行就是一个文本块包含原文内容、向量、元数据这几类字段。看到数据真的写进去了心里才踏实。3.4 检索与问答组装让模型学会“查阅”资料数据准备好了剩下的事就是“检索 回答”。当用户输入一个问题时整个流程是这样的先把用户的问题也做一次向量化得到一个查询向量。然后去向量表里做相似度检索找出最相近的若干个文本块。把检索出来的文本块和用户问题一起组装成一个带上下文的提示词交给对话模型去生成最终回答。这个流程的代码在 Spring AI 里也相当简洁SearchRequest request SearchRequest.builder() .query(userQuestion) .topK(4) .build(); ListDocument documents vectorStore.similaritySearch(request);topK 这个参数控制了检索多少个文本块。我建议设成 3 到 5 之间太少可能漏信息太多会引入噪声且增加 token 消耗。检索结果出来之后需要把它们拼进提示词。这里有一个我自己调整过的提示词模板模板中先用一段话给模型设定角色和规则再把检索出来的文档内容整体放进去最后附上用户的问题StringBuilder contextBuilder new StringBuilder(); for (Document doc : documents) { contextBuilder.append(doc.getContent()).append(\n\n); } String promptText 你是一个专业的问答助手请基于以下文档内容回答问题。\n 如果文档中没有相关信息请如实说明“文档中没有找到相关内容”不要编造答案。\n \n 文档内容\n %s \n 用户问题\n %s .formatted(contextBuilder.toString(), userQuestion);为什么提示词里要强调“不要编造答案”因为对话模型天生有“幻觉”倾向你给了它一堆资料它可能为了显得专业而强行“脑补”出一些文档里没有的结论。在知识库问答场景下这非常危险。所以我会在角色设定里明确边界宁可不回答也不能瞎编。最后一步就是把组装好的提示词交给对话模型让模型生成回答ChatClient chatClient ChatClient.builder(chatModel).build(); String answer chatClient.prompt().user(promptText).call().content();这里的 ChatClient 是 Spring AI 提供的同步调用客户端简洁直观。用起来的感觉就像是普通 HTTP 接口调用内部帮你处理了会话状态、参数封装等细节。4. 实际运行效果与性能调优4.1 一次完整问答的实测记录项目跑通之后我做了一次完整的实测。我挑了一个典型的私有知识问题“如何重置我们产品中的登录密码”这个问题在帮助文档里有专门的章节但对话模型本身完全不知道这个产品的存在。如果不用 RAG直接问模型它只会给出通用的“点击忘记密码”之类的泛泛回答走了 RAG 之后模型从知识库里检索到了产品特有的重置流程回答的内容完全匹配文档中的操作步骤。从响应时间上看整个链路包含一次嵌入计算、一次向量检索、一次对话生成。在本地 GPU 上嵌入计算的耗时可以忽略不计向量检索更是毫秒级大头基本都在对话生成上。整个回答时间在几秒到十几秒不等取决于选用的对话模型大小和硬件条件。我之前在开发环境用纯 CPU 跑对话模型一个 7B 参数的模型生成一段 200 字的回答需要十几秒体验确实一般。如果项目对响应速度有要求建议考虑把对话模型部署到 GPU或者换成参数量更小的模型。嵌入模型则完全不需要担心bge-m3 这类模型在 CPU 上跑也是毫秒级。4.2 检索效果不理想的常见原因在实际项目中最让人头疼的不是代码跑不通而是“代码明明能跑回答质量却很糟糕”。这类问题的根源几乎都集中在检索环节。第一个常见问题是切分粒度不合适。如果文本块太大一个块里包含太多主题检索时的问题是“好像相关”但模型真正需要的细节被淹没了如果文本块太小又可能一个完整的意思被拆散检索不到全部上下文。我的经验是先按 500 到 1000 的 chunkSize 去试然后人工检查几个典型问题的检索结果看看返回的文本块是不是真的有效再做调整。第二个常见问题是嵌入模型和实际语言场景不匹配。如果你的知识库以中文为主却用了以英文语料为主的嵌入模型检索效果会很差。这也是我选择 bge-m3 这类中英双语模型的原因。某些场景下可以考虑用同义词扩充查询或者对检索结果做二次重排但那是进阶优化入门阶段先把基础链路调好。第三个问题非常隐蔽元数据过滤失效。当你的向量库里既有 A 部门文档又有 B 部门文档如果检索时没有按部门过滤返回的结果可能会张冠李戴。Spring AI 的 SearchRequest 支持过滤器建议在数据量大、分类明确时一定要用起来。4.3 项目中值得注意的工程细节再分享几个实操层面的经验。日志是一个非常容易被忽略的调试利器。向量化过程、检索命中了哪些块、这些块的内容是什么都应该能被日志追踪到。我在实际开发时发现很多人排查询不到答案就是因为根本没看检索命中的原始内容一直在猜提示词写得对不对。还有一个建议是把文档切分、向量化做成独立的一次性任务而不是每次启动应用都重新执行。你可以写一个 CommandLineRunner 或者在应用启动时加一个开关判断向量表为空才执行导入。否则每重启一次应用就要重新解析全部文档既浪费时间又可能在向量库里产生重复数据。关于向量库中的重复数据多说一句如果执行了多次文档导入同样的内容可能会被写入多份检索时相似的片段会重复命中白白浪费 token 和上下文空间。我后来在导入逻辑里加了一个按文档路径的查重逻辑数据干净了很多。5. 常见问题排查与避坑指南5.1 依赖与版本带来的编译问题Spring AI 版本更新快网上示例代码的版本参差不齐我把踩过的版本问题整理成了速查表方便对照症状原因解决方案编译找不到 ChatClient 类Spring AI 版本过旧或过新API 变更统一升级到 0.8.1 以上并核对所有 spring-ai 依赖版本一致提示 PgVectorStore 不存在缺少 spring-ai-pgvector 依赖引入对应依赖并确认版本与 spring-ai 核心一致运行时报 Ollama 连接失败本地没有启动 Ollama 服务或模型未下载启动 Ollama执行 ollama pull 下载所需模型中文文档解析出来乱码HTML 或 PDF 编码问题检查 Tika 解析配置HTML 需确保声明了 charset 编码这个表格是排查的第一道关卡解决不了再看具体的异常堆栈。Spring AI 的异常信息通常写得比较直白照着提示去改基本都能处理。5.2 向量数据库连接与初始化问题PGSQL 的连接问题也是高频报错点。一个典型场景是你本机已经跑着一个 PostgreSQL端口用了 5432但里面没有创建 vector 扩展。Spring AI 初始化时会报类似“type vector does not exist”的错误。解决方法是提前在目标库执行 CREATE EXTENSION vector。另一个高频问题是在 Cloud 环境下面向公网的数据库连接串配错导致连接超时。我建议开发阶段先用本机 PostgreSQL 跑通全链路再迁移到服务器环境这样能有效隔离问题。如果你用的是内存型的向量存储比如 SimpleVectorStore排查起来会更简单因为不需要管数据库连接。但生产环境不太建议用 SimpleVectorStore毕竟数据持久化和并发能力有限。5.3 关于嵌入模型与对话模型的配置技巧最后说一个模型配置上的建议。有些同学把 embedding 模型和 chat 模型当成一回事在配置里只指定了一个模型名结果出现“模型不支持嵌入”之类的报错。实际上嵌入模型和对话模型是两类不同的模型Ollama 里需要分别下载配置里也要分别指定。Ollama 拉取模型的操作非常简单终端执行两条命令即可。如果你发现某个模型下载特别慢可以配置一下镜像源但不建议在公网环境使用不明来源的镜像安全第一。模型文件较大动辄几个 GB需要注意磁盘空间。我差点因为磁盘满了导致 Ollama 写入失败报错却毫无提示排查了很久才发现是磁盘问题。建议预留足够的磁盘空间并定期清理不用的模型。写在最后RAG 这套方案我从概念到落地走了一遍最大的体会是Spring AI 把复杂度隐藏得很好让你能用很少的代码搭出完整链路但真正的难点从来不在 API 调用而在于你对检索链路的理解和调优。同一个框架切分参数不同、嵌入模型不同、提示词不同结果天差地别。如果你正在做企业内部的智能问答、文档助手、客服知识库之类的项目建议先按本文的思路跑通一个最小闭环然后在真实数据上反复调整切分参数和检索策略再逐步叠加元数据过滤、重排、多轮对话这些进阶能力。等到你亲手把一个“代码能跑但回答很蠢”的系统调到“真正像那么回事”RAG 的原理自然就刻在脑子里了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑