ARTICLE DETAIL

资讯详情

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

Spring AI实战:RAG知识库搭建全流程与避坑指南

Spring AI实战:RAG知识库搭建全流程与避坑指南 1. 为什么我的第一个RAG项目翻车了先搞懂知识库检索的真正价值先说个我的真实经历。去年年底我接了一个内部知识库项目需求听起来特别简单把公司几十份产品文档扔进去让业务同事用自然语言提问AI直接给答案。当时市面上已经很流行RAG了我想着这不就是调用大模型API 向量数据库 几个文件的事吗结果做出来的第一版demo问退货流程是什么系统从文档里捞了一段完全无关的售后政策说明拼给大模型大模型一本正经地胡编了个退货地址出来。后来我复盘才发现问题根本不在于大模型笨在于我压根没理解RAG的完整链路。很多Spring AI初学者都会踩同样的坑以为RAG 文档切块 存向量 相似度搜索三步走但每一步都有大量工程细节任何一个环节偷懒最终效果都会大打折扣。这篇教程我准备把我从入门到能独立搭建生产级知识库的全过程拆开来讲重点覆盖Spring AI框架在整个RAG链路里扮演什么角色、向量数据库怎么选型、文档处理和切块的正确姿势、元数据设计、以及如何和传统关系数据库配合。文中的代码都是Spring AI 1.0.0版本下实测可运行的版本差异带来的坑我也会单独说明。适合谁来读如果你只是听说过RAG、想快速跑通一个demo这篇能带你起步如果你已经在用LangChain或者其他语言做RAG、想迁到Java生态这篇能帮你少走弯路如果团队正打算用Spring AI做知识库这篇基本可以当成一份选型参考和落地清单。一句话概括RAG的价值它不改变大模型的生成能力它改变的是大模型的记忆范围。模型参数里没有你公司的内部数据和最新业务规则而RAG通过检索把相关知识实时塞给模型让编造变成了依据。这个思路本身不难难的是每一步都做得足够扎实。2. 动手前的关键判断Spring AI在RAG链路中的定位与选型逻辑2.1 先看清RAG的六个环节再决定用框架还是手写RAG不是某个单一组件它是一条完整的数据流水线。我习惯把它分成六个环节离线段数据准备数据采集 → 文档解析 → 切块Chunking→ 向量化Embedding→ 入库在线段问答服务查询语义化 → 向量检索 → 重排序 → Prompt组装 → 大模型生成Spring AI在这个链路里能覆盖的是向量化API的封装、向量数据库的抽象接口、以及在线段中的检索和Prompt组装。换句话说框架帮你把调用OpenAI接口连接Milvus相似度计算这些重复劳动做成了标准化API但文档解析、切块策略、查询改写这些真正影响效果的环节框架只给了默认实现需要你自己根据业务调优。我见过不少团队花了两周时间纠结要不要用Spring AI、还是自己写个Client调用大模型API其实这个决策没那么难。如果你已经确定用Java生态Spring AI几乎是唯一值得认真考虑的选项——它目前对主流向量库的支持非常完整Milvus、PGVector、Redis、Chroma、Qdrant、Elasticsearch都有官方实现而且提供了统一的VectorStore接口换数据库时业务代码几乎不用改。2.2 向量数据库选型不要只看排名要看你的数据量和部署环境很多教程推荐向量库时直接甩一个排行榜出来这其实很误导人。选型第一原则是你的知识库规模和数据更新频率决定了你该选什么。我拿自己的实际项目对比过几种方案给你一个参考方案数据量上限部署成本适合场景我的建议Redis RediSearch百万级向量以内极低已有Redis可直接用中小知识库、高频读取、数据更新频繁团队已有Redis基础设施时的首选PGVectorPostgreSQL插件千万级低复用现有PG需要向量检索和业务数据强关联的场景强推尤其适合要和数据库表做关联查询的场景Milvus亿级中高需要独立部署大规模知识库、高并发检索数据达到千万级再考虑否则是过度设计Elasticsearch千万级中已有ES集群可复用全文检索和向量检索混合场景团队已有ES时才有性价比注意我特意标记了PGVector。为什么因为这篇标题是数据库实战很多知识库项目根本不是纯文档——业务数据本身就是结构化的存在MySQL或者PostgreSQL里。这时候如果你单独搭一个向量库就会面临文档向量在一个库、业务数据在另一个库、两边还要同步关联的麻烦。用PGVector可以直接在业务数据库旁边建向量字段一条SQL里同时支持条件过滤和向量相似度排序工程复杂度立刻降一半。2.3 Embedding模型的选型决定知识库效果上限的往往是这一步你可能觉得RAG链路里大模型是最重要的但实际经验告诉我Embedding模型选错了后面怎么调都是白费。因为向量检索的质量完全取决于Embedding能否准确表达文本语义。这里有一个很反直觉的现象同一个知识库用OpenAI的text-embedding-3-small和用某个开源中文Embedding模型在中文文档上的召回效果可能相差非常大。因为很多开源模型是在英文语料上训练的中文语义表达天然弱一截。选型建议很直接预算充足、对数据安全不敏感直接用OpenAI的text-embedding-3-large效果稳定API简单中文业务为主、预算有限用智谱的embedding-2或者BAAI/bge-large-zh-v1.5中文效果实测接近OpenAI数据必须内网部署用Ollama跑bge-m3或者用Spring AI支持的OllamaEmbeddingModel本地推理我自己的项目因为对数据安全要求高最后选了Ollama bge-m3效果虽然比OpenAI稍弱但完全够用。关键提醒一定不要在生产环境混用不同的Embedding模型来向量化数据——离线入库时用了模型A在线查询时换成了模型B两者的向量空间不一致检索效果会直接崩溃。3. 实战代码Spring AI项目搭建与知识入库全流程3.1 Maven依赖配置版本对应关系是最大的坑Spring AI的版本历史比较特殊它在1.0.0正式版之前经历了很长的里程碑阶段不同版本的API差异巨大。网上大部分教程还是基于0.8.x的旧写法如果你直接用最新版会发现OpenAiEmbeddingModel的构造函数都变了。我这里用Spring Boot 3.3.x Spring AI 1.0.0为例直接给你可以跑通的版本组合parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.3.5/version relativePath/ /parent properties spring-ai.version1.0.0/spring-ai.version /properties dependencies !-- Spring AI 核心 -- dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter/artifactId version${spring-ai.version}/version /dependency !-- OpenAI 的 Embedding 和 Chat 模型 -- dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-openai/artifactId version${spring-ai.version}/version /dependency !-- PGVector 向量数据库支持 -- dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-pgvector-store/artifactId version${spring-ai.version}/version /dependency !-- 文档解析器 -- dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-pdf-document-reader/artifactId version${spring-ai.version}/version /dependency /dependencies !-- Spring AI 的依赖管理需要专门的 BOM -- dependencyManagement dependencies dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-bom/artifactId version${spring-ai.version}/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement注意最后那个BOMBill of Materials很多人漏了它结果就是依赖版本冲突报一堆莫名其妙的错。Spring AI并不是Spring官方的项目它有自己的发布节奏所以不能用Spring Boot的依赖管理直接管它的版本。再说一个很容易踩的坑Spring AI 1.0.0需要JDK 17及以上而且对Spring Boot 3.2以下版本不兼容。如果你还在用Spring Boot 2.7.x那基本可以放弃Spring AI先升级Spring Boot再说。3.2 配置文件的正确写法模型API地址、Key和向量库连接依赖加好之后最基础的就是application.yml配置。这里我把OpenAI和Ollama两种配置都列出来方便你对比spring: ai: # OpenAI 方式如果用 OpenAI 的接口 openai: base-url: https://api.openai.com api-key: ${OPENAI_API_KEY} chat: options: model: gpt-4o-mini temperature: 0.2 embedding: options: model: text-embedding-3-small # 如果用 Ollama 本地模型把上面的 openai 配置去掉换成下面的 # ollama: # base-url: http://localhost:11434 # chat: # options: # model: qwen2.5:7b # embedding: # options: # model: bge-m3 # 向量数据库用 PGVector vectorstore: pgvector: index-type: HNSW distance-type: COSINE_DISTANCE dimensions: 1024 datasource: url: jdbc:postgresql://localhost:5432/rag_db username: postgres password: postgres有个细节值得留意dimensions必须和你的Embedding模型输出维度完全一致。bge-m3输出是1024维text-embedding-3-small是1536维OpenAI的text-embedding-3-large是3072维。填错了启动时不一定报错但检索时相似度计算会全部乱套。我在项目里曾经因为改了模型忘记改dimensions排查了一下午才发现是这个低级错误。distance-type我推荐用COSINE_DISTANCE因为文本向量基本都用余弦相似度衡量语义接近程度。HNSW是PGVector支持的近似最近邻索引算法在数据量超过一万条时一定要开全表扫描的性能差得离谱。3.3 文档入库的核心代码从读文件到切块、向量化、持久化配置好了之后我们直接写知识库的入库逻辑。这一段是整个RAG项目里最机械但最容易出问题的部分Service public class KnowledgeIngestionService { private final VectorStore vectorStore; private final DocumentReader pdfReader; private final TokenTextSplitter textSplitter; public KnowledgeIngestionService(VectorStore vectorStore, DocumentReader pdfReader, TokenTextSplitter textSplitter) { this.vectorStore vectorStore; this.pdfReader pdfReader; this.textSplitter textSplitter; } public void ingestPdf(String pdfPath, String category) { // 1. 读取PDF文档 var document pdfReader.get(); // 2. 添加业务元数据这一步很多教程会忽略但检索时极其重要 document document.stream() .map(doc - { MapString, Object metadata new HashMap(doc.getMetadata()); metadata.put(category, category); metadata.put(source, pdfPath); metadata.put(ingested_time, LocalDateTime.now().toString()); return new Document(doc.getContent(), metadata); }) .collect(Collectors.toList()); // 3. 切块默认按Token数切分 ListDocument chunks textSplitter.apply(document); // 4. 向量化并存入PGVector vectorStore.add(chunks); log.info(成功入库 {} 个文档块来源{}, chunks.size(), pdfPath); } }这段代码只有二十多行但每个步骤都有讲究。第一步读取PDF用的是spring-ai-pdf-document-reader它底层调用了PagePdfDocumentReader会把PDF按页解析成多个Document对象。不要在这个阶段直接入库因为一页PDF的内容量差别可能非常大——有的页面只有两行字有的页面是密密麻麻的表格直接入库会导致检索粒度过粗或过细语义都不对。所以第二步里我给每个Document打上了category和source元数据这是很多入门教程不会提但生产环境必须做的事。为什么因为知识库通常不止一类内容用户在提问时可能限定只看退货政策如果没有元数据做过滤向量检索会从所有文档里捞相似内容无关内容就会混进Prompt。加了这个字段之后在线检索阶段可以直接用Spring AI的FilterExpressionBuilder做前置过滤。第三步切块是整个入库流程里对最终效果影响最大的环节。Spring AI默认的TokenTextSplitter按Token数量切分默认块大小是1000Token、重叠200Token。这不是万能的默认值我后面专门开了个章节讲切块调优。3.4 在线检索与问答让大模型基于检索结果作答文档入库只是RAG的一半另外一半是查询链路。Spring AI把在线检索封装得非常简洁核心代码如下Service public class RagChatService { private final ChatClient chatClient; private final VectorStore vectorStore; public RagChatService(ChatClient.Builder chatClientBuilder, VectorStore vectorStore) { this.chatClient chatClientBuilder .defaultSystem(你是一个知识库问答助手请严格基于给定的知识库内容回答问题。 如果知识库中没有相关信息请明确回答未找到相关答案不要编造。) .build(); this.vectorStore vectorStore; } public String ask(String question) { // 1. 查询向量库取Top K个最相关的文档块 ListDocument similarDocuments vectorStore.similaritySearch( SearchRequest.query(question) .withTopK(5) .withSimilarityThreshold(0.6) ); // 2. 组装Prompt把检索到的内容作为上下文塞给大模型 String context similarDocuments.stream() .map(doc - doc.getContent()) .collect(Collectors.joining(\n\n---\n\n)); // 3. 调用大模型生成回答 return chatClient.prompt() .system(请基于以下知识库内容回答问题。\n知识库内容\n context) .user(question) .call() .content(); } }这个查询链路有一个相当隐蔽的坑withSimilarityThreshold的阈值设置。这个值不是凭空拍的它完全取决于你的Embedding模型输出的相似度分布。同样一句退货流程在OpenAI的Embedding模型下相似度可能是0.78在bge-m3下可能只有0.55因为向量空间不一样cosine值绝对值差异很大。我建议你做一个检索调试模式——不直接调用大模型而是把相似度分数打出来人工检查。看看你的知识库数据里正确的文档块相似度通常落在什么区间再据此定阈值。不要照抄网上教程的0.7或者0.8那都是针对特定模型调出来的。4. 文档切块的艺术别再迷信默认的TokenSplitter4.1 三种切块方式对比固定长度、语义切分和按结构切分切块策略直接决定了检索的颗粒度。我见过太多人拿着默认的TokenTextSplitter跑一遍就上线然后抱怨检索结果怎么这么碎片化答案引用根本对不上。原因很简单你的文档结构不是按Token组织的按Token切块会把一个完整的知识点拦腰截断。我在实践中总结出三种切块方式各自适用场景完全不同切块方式原理优点缺点适合的文档类型固定长度Token/字符数按固定长度切分实现简单、块大小可控容易切断语义单元新闻、公告、无强结构文档按标题结构切分Markdown/HTML标题识别文档层级按章节切块保留语义完整性对无标题文档无效产品手册、技术文档、公众号文章语义切分嵌入聚类/意图识别用模型判断段落边界语义最完整计算开销大、实现复杂混合话题的长文档、会议纪要Spring AI的TokenTextSplitter本质是第一种。它对技术文档这种段落间逻辑完整的内容效果很差。我改造项目后用的是第二种思路在PDF解析之后先用正则识别出标题行然后按标题层级把内容组装成章节块再对超长章节按Token二次切分。4.2 块大小与重叠窗口80%的效果问题出在这里如果你实在不想搞复杂的按标题切块那就务必把固定长度切块的两个参数调好块大小Chunk Size我实测下来中文知识库的块大小控制在200~500 Token之间最合适。太小的块比如100 Token以下会丢失上下文大模型拿到的是一堆断章取义的单句太大的块比如1000 Token以上会混入过多无关信息稀释检索精度。重叠窗口Overlap默认200Token看似是为了防止切断语义实际上很多人忽略了它的副作用——相邻两个块的大量重叠文本会造成检索冗余同一个短语被重复塞给大模型。我的建议是重叠窗口设置为块大小的10%~15%就够了比如400Token的块重叠40~60Token。还有一个细节切块后务必做去重。我遇到过文档里同一段政策出现了三次正文一遍、附录一遍、FAQ再引用一遍切块后产生了大量近似重复的向量。检索时Top K结果全来自同一份重复内容其他知识全被挤掉了。最简单可靠的去重方式是取内容的哈希值入库前过滤掉哈希相同的块。4.3 元数据是检索的隐形调节器通过元数据过滤把问错文档问题提前解决回到我前面提的category元数据。生产级知识库一定不是一个库装一切常见的情况是售后知识、产品手册、内部规章、历史工单全部塞在一个向量库里。这时候如果你不做元数据过滤用户问出差怎么报销系统极有可能从规章制度和工单历史里同时捞内容大模型就分不清哪个才是当前有效的政策。Spring AI的PgVectorStore是原生支持元数据过滤的用法很简单FilterExpressionBuilder b new FilterExpressionBuilder(); // 查询时只检索 category faq 的文档块 SearchRequest query SearchRequest.query(退货流程) .withTopK(3) .withFilterExpression(b.eq(category, faq).build());这段代码会在SQL层面直接加一个WHERE category faq条件然后再做向量相似度排序。这样有两个好处一是检索精度显著提升二是性能也更快因为向量检索只需要在过滤后的数据集内扫描不需要全库遍历。元数据设计的原则也很简单凡是查询时会用到的业务属性都放进元数据包括但不限于文档来源、部门、时间、文档类型、权限等级。这里有个注意点PGVector的metadata字段存储的是JSON格式查询里如果要对数值类型做范围过滤比如只查最近30天的文档存储时记得用LocalDate或者Long不要直接存字符串否则范围查询会走错索引。5. 数据库在RAG项目中扮演的角色不只是向量存储5.1 业务数据入库用本地数据库同步或ETL更新机制保证知识新鲜度很多RAG项目的文档来源并非一个个PDF文件而是数据库里持续更新的业务数据——比如农产品知识库里的作物病虫害记录、电商平台里的商品描述和售后政策。这种情况下直接把关系数据库的表数据实时同步进向量库是最合理的手段。我参与过一个农业知识库项目数据源是MySQL里一张上千条记录的植保知识表。最开始的方案是每天晚上全量导出再重新入库但随着数据量涨到几万条全量重建的时间和成本都很难接受。后来我们改成增量同步在源表里增加一个updated_at字段标识记录的更新时间同步程序定期扫描WHERE updated_at 上次同步时间的记录把变更记录的ID、标题、正文组装成纯文本重新做切块和向量化替换掉向量库中对应的旧块这套流程用Spring中的定时任务就能实现核心逻辑是每次入库时用业务ID作为元数据下次同步时先按ID删除旧向量再插入新向量。Spring AI的VectorStore.delete方法是支持按ID的注意入库时把业务表的主键塞进Document的id字段// 入库时 Document doc new Document(String.valueOf(recordId), content, metadata); vectorStore.add(List.of(doc)); // 增量同步时 vectorStore.delete(List.of(String.valueOf(recordId))); vectorStore.add(List.of(newDoc));5.2 进一步让大模型直接查询数据库如果说把数据库内容喂给RAG还只是数据搬运那么更高级的做法是把SQL查询能力也变成RAG的一部分也就是现在讨论度很高的nl2sql方向。Spring AI为此提供了专门的SQL流程支持——把数据库表结构信息作为上下文让大模型根据用户提问生成SQL然后执行并把查询结果返回给用户。我的实际建议是不要在RAG问答的第一轮对话里直接让大模型生成SQL并执行非常危险。安全做法是先生成SQL再由程序解析展示给用户确认确认之后才执行。毕竟大模型生成的SQL也可能完全错误直接执行轻则返回错误结果重则触发未预期的DELETE或UPDATE。如果你要做这个方向一定要在数据库账号的权限层面严格限制为只读。5.3 向量库和业务库双写一致性是你早晚要面对的问题最后谈一个所有RAG项目进入生产期后都绕不开的话题数据一致性。业务数据更新后向量库里的旧向量什么时候更新如果同步失败用户查到的还是旧数据怎么办我的经验是不要在代码里强行追求双写事务一致那会让系统复杂到不可维护。更务实的是最终一致性方案业务数据变更先落到业务库同时把变更事件发送到消息队列消费者异步更新向量库给向量库的每条记录打上sync_time元数据定时任务扫描长期未更新的数据触发补偿同步这样即使某个时刻向量库和业务库短暂不一致也不会阻塞主流程而且最终数据会收敛到一致。在农业知识库的实际场景里作物病虫害的信息并非毫秒级变化小时级的一致性足够业务使用了但对于产品说明书这类可能随时修正的文档至少把同步周期压缩到分钟级。6. 调优与避坑从召回率到多轮对话的进阶实践6.1 检索效果不行先按这三个维度排查如果发现RAG的回答质量差不要急着换大模型先做系统性排查。我总结了一个排查顺序每一条都是踩坑换来的第一维度数据入库质量。检索不到正确结果时先人工查看切块后每个块的内容是否语义完整。我经常发现的问题包括PDF的页眉页脚被当成正文入库、扫描版PDF的OCR把表格数字识别乱掉、切块把一段话末尾的结论切到了下一个块里。这些都要在看代码之前先解决数据坏了后面都是白搭。第二维度查询侧。用户提问往往是口语化的这个能退不但知识库里的文档是书面化的退货政策允许在签收后七日内...。两者的语义在向量空间里可能离得很远。如果发现自己检索召回的文档完全不相关可以做一个查询改写Query Rewriting先把用户口语问题交给大模型改写成一句话再拿改写后的文本去向量库检索。Spring AI里这样做就好——先用一次不带检索的ChatClient调用生成知识库搜索关键词再执行相似度搜索。第三维度阈值和Top K。similarityThreshold设太高召回太少、设太低召回一堆噪声TopK设太小容易遗漏、设太大又会把低质量的块塞进Prompt。我最终的做法是TopK先设5阈值先设0.3人工检查Top 5里的有效命中数量如果有效命中超过3个就逐步抬高阈值到有效命中只剩2个再回调两档。6.2 多轮对话怎么设计上下文窗口是RAG的重灾区RAG 多轮对话是网上教程里最少讲、但实际项目里最难做对的地方。直接说结论不要把历史消息全部丢给RAG检索。常见的错误做法是用户问第二句话时把第一轮问题的答案也一起拼进上下文再基于拼接结果做向量检索。问题在哪里——第一轮的答案里包含了大量来自知识库的引用内容一旦混入用户的第二轮查询中检索时系统会把你刚才回答过的内容当成用户当前真实想知道的内容去检索于是第二轮答案变成了对第一轮答案的重复包装。我的推荐设计是维护独立的短期记忆只记录对话摘要不记录原始答案每轮用户提问先判断是否需要参考历史。简单规则是如果本轮问题不包含代词它这个刚才说的直接抛弃历史走单轮RAG如果包含代词则先用大模型把原始提问 历史摘要压缩成一个独立问题再走RAG链路Spring AI提供了MessageChatMemoryAdvisor和QuestionAnswerAdvisor两个组件来处理这类场景但如果你第一次做RAG我建议先手动实现上面的规则理解清楚每一环再上框架封装的高级组件。6.3 进阶方向Agentic RAG 与 知识库的未来形态最后简单说说Agentic RAG方向也是热搜词里频繁出现的关键词。传统RAG是单轮检索-生成Agentic RAG则让大模型自己决定检索策略——比如先判断是查数据库还是查文档库查完之后如果发现答案不够完整再发起第二次检索。Spring AI 1.0.0里已经开始提供相关支持比如QueryTransformer和动态工具调用机制。我个人的评估是如果你的知识库只有几百篇文档、用户问题方向固定暂时不需要上Agentic RAG它引入的复杂度和token成本远大于收益。但如果你的知识库是多源异构数据库、文档、API且问题非常发散Agentic RAG能把要不要查、怎么查、查完够不够这些决策交给模型编排体验会有质的提升。这个方向可以先关注等基础RAG链路稳定后再演进。7. 收尾的几点体会回头看我做RAG这条路最深的感受是这个领域的技术栈还在迅速变化但底层思路是可以沉淀的。Spring AI帮我们省掉了大量与大模型和向量库交互的样板代码但它解决不了你对自己业务数据理解不够的问题——你的文档结构、检索颗粒度、更新频率、数据之间的业务关系才是决定知识库效果的核心变量。如果你正在从零开始搭第一个RAG项目我的建议是先别追求所有功能一步到位。第一版只做最基础的文件入库和单轮问答把链路跑通然后花一周时间人工检查100条检索结果找到你数据里最常见的失效模式切块太长、相似度阈值不对、元数据缺失针对性优化这些工作做扎实了再去研究多轮对话和Agentic。一个效果稳定、架构简单的知识库远比一个功能丰富但回答经常出错的知识库有价值。另外一个小技巧把每次调试检索结果的截图和参数记下来形成一份效果基线记录。等哪天你改了Embedding模型或者切块策略这份基线就是判断改动方向对不对的依据——做RAG优化最怕没有对比凭感觉猜有了基线数据每一步调整都能量化验证。
返回列表