ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Spring AI RAG入门:打造私有知识库问答系统

Spring AI RAG入门:打造私有知识库问答系统 1. 一条提示词问不倒的知识库这个项目到底在做什么先从最实际的问题说起。你手里有一堆文档——可能是产品说明书、内部培训资料、几十页的行业报告或者就是自己攒了半年的技术笔记。你想让AI直接回答“根据这些资料某某参数应该怎么配”但把文档整本丢给大模型它要么说“我的知识截止到某年某月”要么开始一本正经地编答案。这就是典型的“模型不知道你手里有什么”。这个项目解决的就是这件事。它叫RAG全称Retrieval-Augmented Generation检索增强生成。核心思路就一句话让AI在回答你的问题之前先到指定的文档库里“查资料”再把查到的内容连同问题一起交给大模型生成答案。Spring AI是Spring官方推出的AI应用开发框架它把对接大模型、向量数据库、文档解析这些脏活累活都封装成了Spring风格的标准接口。两者结合在一起就得到了“第4集让AI学会看文档Spring AI RAG入门实战”这个项目。这套方案适合谁第一类是Java后端工程师尤其是已经在用Spring Boot做业务系统、想给系统加一个“私有知识库问答”功能的人第二类是团队里负责内部工具开发的同学比如做一个能问答公司规章制度的机器人第三类是想快速验证RAG效果、但不想折腾Python技术栈的开发者。它最大的价值不是“AI很酷”而是让你用最熟悉的Java方式在一天之内把一套可用的文档问答能力跑起来。我当初做这个项目的直接动因很朴素团队里积累了一堆接口文档和运维手册每次新同学入职都要翻半天文档。与其让大家继续在文件夹里翻找不如把这些资料喂给AI做成一个统一的问答入口。但真上手之后发现RAG的坑不在“调用大模型”而在“文档怎么拆”“检索怎么准”这些看似不起眼的环节。这篇文章就把我从零到一跑通全流程的经验拆开来讲包括架构怎么搭、文档怎么处理、检索参数怎么调、踩过哪些坑。注意本文所有实操基于Spring AI 1.0.0版本如果你看到的是更新版本API命名可能略有差异但核心思路完全一致。2. 先想清楚架构为什么Spring AI适合做RAG2.1 从“文档”到“答案”的四步路径RAG不是一个新概念但很多人第一次接触时会被各种术语绕晕。其实它做的事情非常线性一共四步。第一步是“载入”把PDF、Word、TXT等格式的文档读取成纯文本。第二步是“切分”因为大模型有上下文长度限制而且整篇文档检索效果极差所以需要把长文本切成一块一块的片段每个片段叫做一个“文档块”。第三步是“向量化”用嵌入模型Embedding Model把每个文档块变成一个向量——你可以把向量理解成一组表示“这段话语义”的数字语义相近的段落向量距离也近。第四步是“检索与生成”用户提问时先把问题也转成向量到向量数据库里找出最相近的几个文档块然后把这些文档块作为“参考资料”连同问题一起发给大模型让它基于这些资料作答。这就是RAG的全部秘密。它不玄乎关键在于每一步都做到位。而Spring AI做的事情就是把上面四步封装成一套连贯的Java API让你不用分别去对接“读取PDF的库”“向量数据库的客户端”“大模型的SDK”而是像写Spring Boot业务代码一样把流程串起来。2.2 架构选型背后的三点考量当初我在技术选型上挣扎过要不要用LangChain要不要用Python最后选了Spring AI理由有三个。第一团队的语言栈是Java。RAG方案如果落到Python后续维护就变成“两个团队的语言”——业务系统是JavaAI模块是Python沟通和运维成本都高。Spring AI让AI功能变成整个Spring Boot项目的一个模块和现有代码无缝共存。第二Spring AI不是从零发明轮子。它的设计思路是用“统一接口 可插拔实现”的方式屏蔽底层差异。你今天用OpenAI的模型明天想换成通义千问或者本地跑的Ollama模型只需要改配置文件业务代码几乎不动。这一点在生产环境下非常重要因为模型厂商的API调整、价格变化、合规要求都是不可控的。第三它有完整的RAG链路抽象。Spring AI官方把文档载入、切分、向量化、存储、检索封装成了“VectorStore”和“DocumentReader”等接口虽然早期版本API有点粗糙但在1.0版本已经收敛得比较清晰。对Java工程师来说站在Spring生态里用这些接口远比自己去拼装底层的Elasticsearch客户端、OpenAI SDK顺畅得多。当然Spring AI也有让人纠结的地方。最明显的是它迭代速度快从0.8到1.0一些类名和包名变过几次网上教程经常对不上号。另一个问题是中文资料相对少遇到报错往往要在GitHub的issue里翻半天。但这些问题属于“成长的烦恼”不构成放弃的理由。2.3 整体模块划分一张图看懂所有组成部分在实际写代码之前先把项目要拆成几个模块理清楚。文档接入层负责读文档Spring AI内置了TXT、PDF、Markdown等常见格式的解析器Word文档需要借助Apache POI或先转成PDF/TXT。文档切分层负责把长文本切成语义完整的片段Spring AI提供了多种切分策略后面会详细对比。向量化与存储层负责把文档块转成向量并存储我选的是开源的Chroma向量数据库因为它轻量、嵌入式、零配置适合入门生产环境可以换Milvus或云上的向量库。检索层负责根据用户问题找出最相关的文档块Spring AI的VectorStore接口内置了“相似度搜索”方法。生成层负责把“文档块 用户问题”组装成Prompt调用大模型生成最终回答。按照这种方式拆好模块后面每一步调试都能单独推进出问题也容易定位。3. 动手前的准备依赖、模型和数据库选型3.1 最小可用版本组合先明确依赖版本。我用的是Spring Boot 3.3.x Spring AI 1.0.0。注意Spring Boot 3.x要求JDK 17及以上如果你的项目还在JDK 8这部分就需要单独权衡了。在pom.xml里需要添加Spring AI的BOMBill of Materials统一管理各个模块版本dependencyManagement dependencies dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-bom/artifactId version1.0.0/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement然后添加你需要的模块我这里用到的有spring-ai-openai-spring-boot-starter对接OpenAI兼容接口包括对话模型和嵌入模型。spring-ai-chroma-store-spring-boot-starter对接Chroma向量数据库。spring-ai-pdf-document-reader解析PDF文档。spring-ai-tika-document-readerApache Tika集成可以解析各种格式文档通用性很强。如果模型走的是OpenAI官方API需要注意网络连通性。我身边很多团队实际上用的是国内云厂商提供的OpenAI兼容接口或者本地部署的模型服务那就在配置文件里把base-url改成对应的地址即可。Spring AI的配置兼容做得不错只要接口协议兼容几乎不用改代码。3.2 嵌入模型和对话模型怎么选这里有个新手最容易踩的坑把“对话模型”和“嵌入模型”混为一谈。在RAG流程里对话模型负责“生成回答”嵌入模型负责“把文字变成向量”。两者可以来自同一个厂商也可以是不同厂商。比如对话模型用gpt-4o-mini嵌入模型用text-embedding-3-small这完全没问题。生产实践中很常见。选嵌入模型时要考虑三点。一是向量维度它直接影响向量数据库的存储和检索性能二是支持的语言对中文场景来说要选对中文支持好的模型三是调用成本嵌入模型通常按token计费虽然比对话模型便宜很多但文档量大时也是一笔开销。在Spring AI中配置两个模型的方式如下spring.ai.openai.api-key${OPENAI_API_KEY} spring.ai.openai.chat.base-url${OPENAI_API_BASE} spring.ai.openai.chat.options.modelgpt-4o-mini spring.ai.openai.embedding.options.modeltext-embedding-3-small如果你的环境里不方便调用在线API也可以本地部署Ollama然后spring.ai.ollama.base-urlhttp://localhost:11434 spring.ai.ollama.chat.options.modelqwen2.5:7b spring.ai.ollama.embedding.options.modelnomic-embed-text本地方案的好处是数据不出内网响应速度也可控虽然模型能力比大厂API弱一些但做内部知识库问答完全够用。我实际调过Linux服务器上Ollama的配置效果比较稳。3.3 为什么选择Chroma作为向量数据库向量数据库是RAG的存储底座。市面上可选的有Chroma、Milvus、Weaviate、Qdrant还有云上的Pinecone以及国内云厂商的向量检索服务。我没有选择重型的Milvus是因为入门阶段追求“快速跑通”。Chroma是嵌入式向量数据库它以本地文件方式运行不需要独立部署服务器启动Spring Boot项目时会自动初始化。对个人项目、中小团队内部工具来说这个特性太友好了。等整个链路跑通、文档量上升到百万级之后再迁移到Milvus或云服务也不迟。因为Spring AI的VectorStore接口是统一的迁移时主要改配置和依赖业务代码改动影响比较小。这就是框架抽象带来的好处。4. 核心编码环节从载入文档到检索问答全流程4.1 读取并切分文档为什么切片大小直接影响回答质量载入文档这件事Spring AI做了一层很好的封装。接口是DocumentReader不同的实现类读不同的格式。比如读PDFvar reader new PdfDocumentReader( new FileSystemResource(/data/docs/myfile.pdf), new ParagraphPdfDocumentTextSplitter() );这段代码表示读取指定路径下的PDF文件然后按段落切分文本。ParagraphPdfDocumentTextSplitter会尽量保持段落完整性避免在段落中间硬切。如果你的文档版式复杂可能还是需要用Apache PDFBox直接抽取文本但Spring AI内置的解析器对大多数场景够用。对于通用格式文档TikaDocumentReader是更好的选择。Apache Tika能识别上百种格式Word、PPT、Excel都能抽取文本。使用方式和PDF Reader类似。真正考验功夫的环节是“切分策略”。我实测下来切分粒度直接决定了检索质量比模型选型的影响还要大。Spring AI提供了几种切分器TokenTextSplitter按token数量硬切比如每500个token切一块不管语义是否完整。SentenceTextSplitter按句子边界切。ParagraphTextSplitter按段落切。直接按token硬切最常见的问题是“把一句话从中间切断”检索到这样的片段后丢给大模型它只能看到半句话回答自然容易跑偏。我踩过一次这样的坑把一份售后手册按512个token切块结果很多块的第一句都是“异常情况的处理方法如下”后面内容却是上一条的残留AI答非所问。后来我换成了“先按段落粗切超长段落再按句子细切”的两级策略。具体做法是在代码里定制切分逻辑var splitter new TokenTextSplitter.Builder() .withChunkSize(600) .withChunkOverlap(100) .build();withChunkOverlap是重叠大小。为什么要重叠因为语义有连续性。如果第3块结尾和第4块开头本来是同一句话检索第3块时缺少第4块的信息就可能差一个关键转折。设置100个token的重叠相当于相邻块之间共享一段内容减少信息断裂。这个参数是实战调优最值得花时间的点之一。4.2 文档向量化并写入向量库文档切好之后下一步是向量化并存储。在Spring AI中这段代码非常简洁Configuration public class RAGVectorStoreConfig { Bean public VectorStore vectorStore(EmbeddingModel embeddingModel) { return new ChromaVectorStore(embeddingModel, collection-name); } }创建好VectorStore后把切好的文档块写入ListDocument documents splitter.apply(reader.get()); vectorStore.add(documents);这里注意一点documentSplitter的apply方法接收的是Document列表返回的也是Document列表但返回的每个Document内部已经带有切分后的文本。最后一行vectorStore.add(documents)会逐条调用你配置的嵌入模型把文本转为向量然后存入Chroma。这个环节非常容易在细节上踩坑我这里列出几个我实际遇到过的重复导入问题如果你连续运行两次写入逻辑向量库里会出现两份相同内容的文档。检索时两份都可能被召回虽然大模型最终生成的答案变化不大但会浪费token。解决办法是在写入前按文档ID清理旧数据或者维护一个“已导入文档ID集合”。嵌入维度不一致如果你中途换了嵌入模型新模型生成的向量维度和库里已有的向量维度不一致查询时会直接报错。解决方法是换模型后把向量库集合删掉重建。Spring AI中可以通过ChromaVectorStore的deleteCollection()方法重置。中文支持有些嵌入模型对中文支持不好导致语义检索效果极差。测试方法是随意拿两条语义相近但字面不同的句子去查看检索结果是否准确。我推荐先测试text-embedding-3-small和bge-m3这类对中文友好的模型。4.3 检索与生成一条完整问答链路的实现整个RAG模块的核心服务类大概长这样Service public class RAGService { private final ChatModel chatModel; private final VectorStore vectorStore; public RAGService(ChatModel chatModel, VectorStore vectorStore) { this.chatModel chatModel; this.vectorStore vectorStore; } public String ask(String question) { // 1. 把问题向量化并在向量库中检索 ListDocument relevantDocuments vectorStore.similaritySearch(question); // 2. 组装上下文 String context relevantDocuments.stream() .map(Document::getText) .collect(Collectors.joining(\n\n)); String prompt 你是一名企业内部知识库助手。请严格根据以下资料回答用户问题。 如果资料中找不到答案请直接说“根据现有文档无法回答”不要编造。 资料 %s 用户问题 %s .formatted(context, question); // 3. 调用大模型生成回答 return chatModel.call(prompt); } }这里最容易被忽略的是“严格根据资料”这句提示。如果Prompt里不加这个约束大模型会倾向于“自由发挥”。原因很简单大模型的训练语料里有大量通用知识当你问的问题和文档内容相关但又不完全一致时它很可能把文档答案和它的“记忆”混在一起输出。在知识库场景里这通常不是我们想要的。我习惯在Prompt里把“资料不足就承认”写进去这是从客服场景里学到的经验——宁可答不上来也不要答错。similaritySearch默认返回Top K条结果K默认是4可以通过withTopK调整vectorStore.similaritySearch( SearchRequest.builder() .query(question) .topK(5) .similarityThreshold(0.3) .build() );topK代表召回条数similarityThreshold代表相似度阈值。这两个参数是检索质量的关键调优点。阈值设太高可能什么都召不回来设太低会召回一堆不相关内容。我的经验是从0.3开始调结合真实问题测试看召回结果是否匹配。4.4 配置完整的Spring Boot启动流程把以上代码串起来后还需要写一个启动入口并配置好相关Bean。我的做法是提供一个初始化接口在服务启动后的第一次使用时如果向量库为空自动加载并向量化/data/docs目录下的所有文档Component public class DocumentInitializer implements ApplicationRunner { private final VectorStore vectorStore; public DocumentInitializer(VectorStore vectorStore) { this.vectorStore vectorStore; } Override public void run(ApplicationArguments args) { // 只有库为空时才导入 if (vectorStore.count() 0) { ListDocument documents loadAllDocuments(data/docs); vectorStore.add(documents); System.out.println(文档导入完成共 documents.size() 块); } } }这里再提醒一点ApplicationRunner的run方法在Spring Boot启动后自动执行非常适合做一次性初始化。但如果你文档量很大比如几百MB启动流程会被拖慢。生产上建议改成后台线程执行或者做成手动触发的管理接口。5. 检索质量调优从“答非所问”到“精准命中”5.1 先跑通再谈优化我见过很多人一上来就研究各种高级RAG技巧——重排、查询改写、混合检索、Agent路由。这些技术都有用但都有一个前提基础链路已经跑通数据已经正确入库。在你还没有完整跑通一次问答的前提下讨论这些就是空中楼阁。所以我的建议是先搭最小闭环用一份干净的测试文档比如产品说明书前五页跑通“提问→检索→生成→答案”全链路。再逐步加真数据。这样出了问题能快速定位到底在哪一环。5.2 三段式调优策略第一段是“切分调优”。观察召回内容是否完整。如果召回片段开头结尾都是残句优先调整切分器策略增加重叠或者改为段落切分。第二段是“召回数量调优”。topK越大召回的片段越全但噪音也越多topK越小答案越精准但可能漏掉关键信息。我一般先用5根据结果微调。第三段是“Prompt调优”。模型能不能“正确使用资料”和Prompt关系极大。如果你发现资料明明有答案模型却说得含糊说明Prompt没有约束好它。可以加一句“请首先参考资料中的信息并注明信息来源”。5.3 实测数据一个典型的调优前后对比我拿公司的售后FAQ文档做过一次完整测试。初始配置是TokenTextSplitter512 token一块无重叠topK4无相似度阈值过滤。结果是用户问“宕机后如何恢复”召回的4块里有2块完全不相关有1块只是泛泛提到“系统启动”但没细节模型最终给出了一个含糊的“建议重启服务”的回答。调整之后换成段落优先切分块大小600 token重叠100topK5相似度阈值0.3。同一问题召回的5块里有3块直接对应宕机处理流程包含具体命令和解救步骤模型给出的回答直接引用了文档中的操作步骤质量明显提升。这份对比用一句话总结检索决定了答案的下限生成决定了答案的上限。如果你的检索结果不对后面模型再强也没用。6. 常见问题与排查技巧实录6.1 问题速查表问题现象可能原因解决方案启动时报NoSuchBeanDefinitionException缺少starter依赖或没有开启自动配置检查pom.xml是否添加spring-ai-*-spring-boot-starter调用模型时超时网络不通或模型服务未启动先用curl测试模型API地址可访问性中文回答效果差嵌入模型对中文支持弱更换text-embedding-3-small或bge-m3等中文友好模型答案总是“编造”Prompt缺少约束或资料未命中在Prompt中加入“严格根据资料回答”调整切分和检索参数向量库导入两次相同内容代码重复执行add导入前检查vectorStore.count()或先清空集合检索结果和问题完全无关切分太碎、向量库有脏数据重建向量库调整切分策略检查文档解析是否正常6.2 三个我印象最深的排查经历第一个是“模型返回空字符串”。排查了一圈发现是similaritySearch返回了空列表导致Prompt的“资料”部分是空的大模型面对空资料不知如何作答就直接输出了空白。根源是文档内容全部是图片型PDF解析之后文本为空。换了Tika并补充OCR工具才解决。第二个是“同样的知识库同事跑出来效果不一样”。后来发现是文档切分参数不同。他用了默认的TokenTextSplitter而我改成了段落优先。切分策略对RAG的影响是全局性的一旦修改参数需要重新向量化全部文档否则检索结果混乱。第三个是“向量库里有数据但查不到”。原因是相似度阈值设高了所有候选的相似度都在阈值以下。排查方法很简单把阈值调低或置空看看能不能召回。如果召回了一大堆不相关内容说明不是阈值问题而是嵌入模型或文档解析的问题。6.3 必须避开的几个“新手坑”不要跨版本升级Spring AI版本之间API差异较大升级前一定看官方Migration Guide别直接替换依赖版本。不要把PDF原样整本入库可以基于Tika做预处理去掉页眉页脚图表干扰。不要忽略文档清洗很多文档里有大量换行、空格、乱码直接影响切分和向量化效果。我在实践中会先用正则做一轮清洗再交给切分器。不要用生产地址测试向量库测试时用一个独立collection避免脏数据影响线上问答。7. 再往下走从Demo到生产力的几条扩展路径跑通基础RAG之后你已经具备了一个可用的知识库问答雏形。如果要在真实业务里落地有几个方向值得投入精力。第一个是“多轮对话”目前实现的是单轮问答。可以在提问前把历史对话摘要也拼接进Prompt让AI能理解“你刚才问过什么”这在售后场景里几乎是刚需。第二个是“多知识库隔离”比如给销售团队和研发团队各自独立的向量库集合按权限访问。做法很简单在写入文档时为每条Document加metadata字段标明知识库ID检索时通过SearchRequest的filter表达式过滤。第三个是“混合检索”用关键词匹配和向量检索混合召回然后通过Rerank模型排序。这种方法能同时照顾“精确术语匹配”和“语义模糊匹配”在生产环境中效果提升明显。第四个是引入Agent智能体思路。Spring AI Alibaba社区已经有一些基于Spring AI构建Agent的实践本质上是把“检索”变成一个Agent可以调用的工具让大模型自主决定何时检索、检索什么、以及综合多轮检索结果。这是从“问答机器人”走向“自主执行任务”的关键一步。这几个方向都有公开案例和源码可参考但无论选哪个核心还是先把基础检索质量做扎实。数据都还没切好、检索都还不准谈再多“Agent”都是空架子。以我个人的经验来说Spring AI这套组合拳最舒服的地方在于它把Java工程师从“什么都要自己做”的AI泥潭里拉了出来让你用写Spring Boot接口的方式写完一个RAG服务。虽然它还远不够完美社区生态还在快速变化但作为把AI能力融入现有系统的第一站它确实是Java后端团队一个非常务实的切入点。如果你正在评估“要不要在项目里引入文档问答”我的建议很直白先花半天搭出这个最小闭环再根据实际效果决定下一步。技术选型永远比不过亲手跑一遍。
返回列表