Spring AI 2.0下RAG工程化:切片、混合检索与Rerank实战
做 Java 后端这些年我第一次因为 RAG检索增强生成项目连续加了三个星期的班。不是模型选型难而是把一套知识库问答系统从能跑调整到好用真正的卡点全在工程细节上文档切分不合理、检索召回一塌糊涂、重排没做、线上效果又慢又飘。Spring AI 2.0 发布之后很多 Java 团队以为引入依赖就能把 RAG 跑通但实际上从 Chunking 到混合检索再到 Rerank每一步都直接影响最终的问答质量缺一环效果就断一截。这篇文章我会用 Spring AI 2.0 作为主线把我在真实项目中沉淀下来的工程化方案完整梳理一遍。内容包括切片策略怎么做、混合检索怎么融合、Rerank 模型怎么接进来以及那些常规文档里不会写的坑。适合正在做 Java RAG 问答、知识库落地的开发者也适合刚接触 RAG 的后端同学拿来当作入手指南。1. 先搞清楚RAG 项目的性能瓶颈到底在哪1.1 一条完整 RAG 链路里有哪些环节很多人把 RAG 想得很简单把文档塞进向量库用户提问把相似内容拼给大模型完事。但真实场景下一个面向生产的 RAG 系统至少包含六个环节文档解析与清洗、文本切片Chunking、向量化与入库、检索召回Retrieval、重排Rerank、答案生成Generation。其中最容易出问题的有三个地方。第一是切片切得不好语义被拦腰截断检索阶段根本召不回完整上下文。第二是检索只用向量检索会遇到同义词、精确编号、专有名词匹配不上的问题。第三是重排向量检索拿回 Top-K 之后文档之间的相关性排序往往不够精细直接丢给大模型容易把不相关的段落混进上下文影响答案质量。我见过不少团队把大量精力花在调 prompt 上晚上了很长时间发现效果没提升最后定位到是检索环节的问题。提示词只能决定大模型怎么用材料材料本身对不对、全不全是由切片、检索、重排决定的。这个认知不转变调参就是白费力气。1.2 Spring AI 2.0 在链路中承担什么角色Spring AI 从 1.0 到 2.0 的演进非常快。1.0 时期已经有 ChatClient、EmbeddingModel、VectorStore 这些基础抽象但 RAG 相关的组件还比较零散需要自己拼装。到了 2.0官方把 RAG 链路抽象成了完整的组件体系QueryTransformer 负责查询改写DocumentRetriever 负责检索DocumentCombiner 负责合并结果QueryAugmenter 负责拼接上下文。也就是说上面说的六环节Spring AI 2.0 基本都给出了标准化接口我们只需要把具体实现填进去。另一个值得关注的点是 Spring AI Alibaba 这个分支。它把 DashScope通义系列的模型接入做了深度适配Embedding 可以用 text-embedding-v3Chat 可以用 qwen-plusRerank 也有对应 API。如果你所在团队本身就是阿里云技术栈用 spring-ai-alibaba 可以省掉很多自建模型的运维成本。但不管底层接哪家模型上层的 RAG 编排逻辑是一致的这篇文章的方案都可以直接复用。2. Chunking80% 的召回质量在切分阶段就决定了2.1 固定长度切片为什么不够用最早我图省事直接用固定字符数切分每段 1000 个字符重叠 200。跑出来的效果非常不稳定。问题出在几个方面一是中英文混排的文档里1000 个字符可能切在句子中间语义被硬生生截断二是像合同、技术手册这种有明确章节结构的文档固定窗口会把条款标题和条款内容切到两个 chunk 里检索时只召回内容段缺少标题做语义锚点三是表格、代码块这类特殊内容按字符切会被切得稀碎。后来我总结出一个判断标准切片之后任何一个 chunk 独立拿出来读都应该是一个基本自洽的信息单元。如果读完之后不知道这段在说什么或者关键前提在上一段里那这个切片就是失败的。固定长度切片显然过不了这个标准。2.2 三种主流切片策略的对比我把实际项目中评估过的切片方案整理成了对比表方便你按文档类型选择切片策略原理适合场景缺点固定长度切片按 token 或字符数切带重叠无结构文本、日志、碎文本容易切断语义结构信息丢失递归结构切片按段落、标题、列表层级递归切分技术文档、合同、公众号长文对无结构文本不友好语义切片利用 embedding 相似度判断语义边界对话记录、内容边界模糊的文本计算开销大需要调阈值实际项目中我很少只用一种策略。技术教程、产品手册这种层级清晰的文档用递归结构切片效果最好如果知识库里既有长文档又有零散 FAQ我会先按文档类型分流再分别走不同切片策略。这个分流思路是关键不要指望一种切片方式打天下。还有一个容易被忽略的参数是切片重叠overlap。重叠的作用是让切片边界附近的信息不至于完全丢失但重叠太大意味着同一个信息点被重复存储检索时会出现多个高相似度 chunk浪费上下文窗口。我的经验是重叠大小控制在切片的 10% 到 15% 之间比较稳妥且重叠部分尽量放在句首或句尾这种语义断裂风险最高的位置。2.3 Spring AI 2.0 里的切片落地实操Spring AI 2.x 提供了 TokenTextSplitter同时支持自定义 DocumentSplitter 实现。下面这个例子基于递归字符切分思路按 Markdown 标题层级做切分Component public class MarkdownStructureSplitter implements DocumentSplitter { Override public ListDocument split(Document document) { String content document.getContent(); ListDocument chunks new ArrayList(); // 按二级标题切出章节 String[] sections content.split((?m)^## ); int index 0; for (String section : sections) { if (section.isBlank()) continue; String chunkText section.trim(); // 对过长章节再按三级标题向下切 if (chunkText.length() 1200) { String[] subSections chunkText.split((?m)^### ); for (String sub : subSections) { chunks.add(toDocument(document, sub.trim(), index)); } } else { chunks.add(toDocument(document, chunkText, index)); } } return chunks; } private Document toDocument(Document source, String text, int index) { MapString, Object metadata new HashMap(); metadata.put(chunk_index, index); metadata.put(source, source.getMetadata().get(file_name)); return new Document(text, metadata); } }这段代码的思路是先用正则把文档按##标题切成章节章节过长就继续按###往下切并把章节序号写进 metadata。后续做检索时metadata 里的 source 和 chunk_index 可以拿来过滤、溯源前端展示引用来源时也方便。切片完成之后下一步是把 chunk 向量化并写入向量库。Spring AI 2.0 的写法大致是这样Service public class KnowledgeIngestService { private final VectorStore vectorStore; private final EmbeddingModel embeddingModel; public KnowledgeIngestService(VectorStore vectorStore, EmbeddingModel embeddingModel) { this.vectorStore vectorStore; this.embeddingModel embeddingModel; } public void ingest(Path filePath) { DocumentReader reader new MarkdownDocumentReader(); ListDocument docs reader.get(filePath); ListDocument chunks new MarkdownStructureSplitter().split(docs.get(0)); ListDocument documents chunks.stream() .map(chunk - Document.builder(chunk.getContent()) .metadata(chunk.getMetadata()) .build()) .toList(); vectorStore.add(documents); } }注意MarkdownDocumentReader是读取原始文件的切片发生在读取之后、入库之前。这里还要提醒一个细节切片的粒度要和 embedding 模型的 max input tokens 匹配。比如你用的是 text-embedding-v3它对单条文本有长度上限切片太长会被截断等于把后半段语义直接丢掉。提示切片结果一定要落库保存不要每次请求时现切。把 chunk 文本、向量、metadata 一起持久化后续要调整检索策略或者做 A/B 测试时可以随时切换而不用重新处理整个知识库。3. 混合检索不要让向量搜索单打独斗3.1 向量搜索的天然短板向量搜索的本质是语义相似度匹配它的优势是能处理意思相近但表述不同的查询。但真实业务里有大量情况是语义相似度解决不了的。比如用户问订单号 A10086 的处理进度向量相似度检索很可能匹配到含订单进度的泛化段落而不是具体到 A10086 这条记录再比如产品型号、工单编号、合同编号这类精确标识符向量模型通常很难把它们和文档中的精确字符串对应上。还有一类问题是低频专有名词。embedding 模型对训练语料里出现频率低的词向量表达往往不稳定导致检索时召不回。我自己踩过的坑是客户问磷酸铁锂电池的析锂现象文档里全篇都叫析锂用户查锂枝晶也能查到一些内容但相关性排序完全不对本质上就是专有名词和同义词的向量分布不够集中。3.2 检索融合的两种主流思路混合检索的核心思路是把向量检索和关键词检索BM25、全文索引的结果合并起来。合并方式有两种主流做法一种是加权分数融合RRFReciprocal Rank Fusion。把各路检索结果按排名赋予分数然后合并排序。RRF 的好处是不依赖各个检索源的分数尺度一致因为向量相似度和 BM25 分数根本不是同一个量纲直接加权相加没有意义RRF 用排名代替分数天然规避了这个问题。另一种是倒重在重排阶段解决。也就是检索阶段不做复杂融合只做简单的结果并集把候选集扩大然后交给 Rerank 模型统一打分排序。这种方案实现简单且最终精度由 Rerank 兜底是我目前在项目中采用的主流方案。如果知识库规模不大、Rerank 模型部署成本可控我更推荐向量 关键词召回Rerank 兜底的架构。如果 Rerank 不可用再退而求其次用 RRF 做分数融合。3.3 Spring AI 2.0 里实现向量 关键词混合检索Spring AI 2.0 的VectorStore接口有similaritySearch方法支持传入过滤条件但本身不直接支持关键词检索。要做混合检索有两种落地方式。第一种方式如果向量库用的是 PostgreSQL 的 pgvector可以直接利用 PostgreSQL 内置的全文检索能力和向量检索并行跑然后在业务代码里做融合。这个方案的优点是省掉额外中间件。示例代码如下Service public class HybridRetrievalService { private final JdbcTemplate jdbcTemplate; private final VectorStore vectorStore; public SearchResponse hybridSearch(String query, int topK) { // 1. 向量检索 ListDocument vectorHits vectorStore.similaritySearch( SearchRequest.builder(query).topK(topK).build()); // 2. 关键词检索PostgreSQL 全文检索 String sql SELECT chunk_id, ts_rank(to_tsvector(simple, content), plainto_tsquery(simple, ?)) AS rank FROM knowledge_chunk WHERE to_tsvector(simple, content) plainto_tsquery(simple, ?) ORDER BY rank DESC LIMIT ?; ; ListKeywordHit keywordHits jdbcTemplate.query(sql, (rs, rowNum) - new KeywordHit(rs.getLong(chunk_id), rs.getDouble(rank)), query, query, topK); // 3. 按 RRF 规则融合 return fuse(vectorHits, keywordHits, topK); } }第二种方式用 Elasticsearch 或者 OpenSearch 作为向量和全文的统一存储。Spring AI 2.0 提供了ElasticsearchVectorStore可以同时利用 ES 的 dense_vector 和 BM25 能力在 ES 侧完成混合查询。如果你的团队已经有 ES 集群这是最省事的路径。我在实际项目中倾向于第三种方式如果预算允许引入专门的检索中间件比如 milvus 或 es专门承接向量检索和关键词检索然后用 Java 代码做融合。这样业务逻辑清晰后续扩展知识图谱检索也方便。3.4 三路混合引入知识图谱Neo4j之后最近社区里讨论比较多的三路混合检索是在向量检索和关键词检索之外再引入一条知识图谱检索路径。思路是把文档中的实体和关系抽取出来存入 Neo4j用户提问时先做实体识别再从图谱中提取相关实体及其关联关系作为一路独立的检索结果。这种方案对多跳问答很有帮助。比如A 项目的负责人同时负责了哪些项目这种问题纯向量检索很难直接回答因为答案分散在多个文档片段里而知识图谱通过关系遍历可以精准命中。Spring AI 2.0 对 Neo4j 有原生支持Neo4jVectorStore可以同时利用图结构和向量索引对实体关系密集的领域如组织架构、供应链、医疗非常适用。但这里我要泼一盆冷水知识图谱的路由和实体抽取本身是一个不小的工程通常需要 LLM 参与信息抽取对文档质量和运行成本都有要求。如果知识库规模不大、问题模式也比较单一先做好向量 关键词两路混合把 Rerank 加上性价比更高。知识图谱可以作为第二阶段的增强项不要一上来就搞三路混合否则很容易陷入投入很大、收益不明显的泥潭。4. Rerank 重排把召回精度再拉高一个身位4.1 Rerank 解决什么问题混合检索把候选集扩大了Top-K 里难免混入一些语义沾边但实际无关的段落。Rerank 模型做的事情就是把候选段落和用户问题一起送入一个专门的交叉编码器逐条计算相关性分数然后重新排序。Rerank 和向量检索的本质区别在于向量检索是双塔结构问题和文档分别编码再用余弦相似度衡量速度快但精度上限有限Rerank 是交叉编码结构问题和文档拼接后一起过模型能捕捉更细粒度的语义交互精度更高但计算成本也更高。所以 Rerank 的正确用法是先用便宜的向量检索快速筛出 Top-50再用 Rerank 精排取 Top-5这样既保证精度又控制开销。我实测过一个案例在不做 Rerank 时混合检索 Top-5 的答案准确率大概在 65% 左右加上 Rerank 之后准确率能提升到 85% 以上。这个提升幅度非常明显几乎可以说是 RAG 链路里投入产出比最高的一个环节。4.2 Rerank 模型选型与部署方式Rerank 模型的选型我和团队实际对比过几个方向模型类型部署方式适用场景bge-reranker-v2-m3开源交叉编码vLLM / Ollama / Python 推理服务中文场景效果好可本地私有化text-embedding-rerankDashScope云端 API无需部署调用 API阿里云用户快速验证智谱 rerank API云端 API无需部署配合智谱生态Windows 本地起 vLLM 跑 rerank 模型这件事很多同学问过我。vLLM 主要面向 Linux 和 CUDA 环境Windows 下需要 WSL 或者 Docker Desktop 来跑纯 Windows 原生安装比较折腾。如果你只是想本地验证 rerank 效果更轻量的做法是直接用sentence-transformers的CrossEncoder跑一下代码量很小不依赖 vLLM。等验证通过、确定要上线了再放到 Linux 服务器上用 vLLM 或者 FastAPI 封装成服务效率和稳定性都更好。4.3 Spring AI 2.0 中接入 RerankSpring AI 2.0 没有内置 Rerank 组件但 RAG 管线的DocumentRetriever接口留了扩展点。我通常的做法是实现一个RerankingDocumentRetriever在基础检索器返回结果之后调用 Rerank 服务。核心代码大概是这样的Component public class RerankingDocumentRetriever implements DocumentRetriever { private final DocumentRetriever delegateRetriever; private final RerankClient rerankClient; public RerankingDocumentRetriever( VectorStore vectorStore, RerankClient rerankClient) { this.delegateRetriever new VectorStoreDocumentRetriever(vectorStore); this.rerankClient rerankClient; } Override public ListDocument retrieve(Query query) { // 第一步粗召回 Top-50 ListDocument candidates delegateRetriever.retrieve(query); // 第二步调用 Rerank 服务精排 ListRerankResult reranked rerankClient.rerank( query.text(), candidates, 5); // 第三步按重排顺序返回 Top-5 return reranked.stream() .map(r - candidates.get(r.index())) .toList(); } }然后把这个 retriever 接到 RAG 的 Advisor 上Bean RetrievalAugmentationAdvisor ragAdvisor( DocumentRetriever retriever, ChatModel chatModel) { return RetrievalAugmentationAdvisor.builder() .documentRetriever(retriever) .queryAugmenter(new ContextualQueryAugmenter()) .build(); }这里有一个值得注意的点粗召回数量要留足。我的经验是向量 关键词混合召回 Top-50再重排取 Top-5 是比较稳的组合。如果粗召回只取 Top-10Rerank 的发挥空间就太小了某些被埋在 11 到 20 位的有效文档根本没有机会被重排捞回来。召回率是精排的上限这句话在 RAG 里同样成立。另外接入本地部署的推理模型比如团队用 Ollama 或 vLLM 自建的 deepseek时Rerank 模型最好单独部署不要和对话模型共用一套服务。因为 Rerank 请求是高频短任务对话模型是低频长任务混在一起部署容易出现资源抢占线上延迟会忽高忽低。我在项目里就把 Rerank 服务单独拆了一个副本问题迎刃而解。5. 工程落地的常见坑与排查实录5.1 实战问题速查下面这张表是我在项目里踩过、或在社区里被反复问到的坑直接按问题现象给出排查方向和解决思路现象根因排查方式解决方案回答答非所问切片把上下文切断或召回结果混入无关 chunk打印检索出来的文档原文人工检查调整切片策略增加 Rerank 过滤检索结果都是同一个文档的重复片段切片重叠过大或同一内容多次入库检查 chunk 文本相似度检查入库幂等逻辑缩小重叠比例入库前做去重关键词命中但向量召不回专有名词、编号类文本向量化失效用精确查询测试关键词检索通道增加关键词检索路做混合检索Rerank 后效果反而变差Rerank 模型与领域不匹配或调用逻辑有 bug比如传参顺序错误抽样对比 Rerank 前后排序换领域匹配的 Rerank 模型修正调用逻辑线上响应太慢召回数量过大LLM 上下文过长查看链路耗时分布减少粗召回数量限制拼接上下文长度向量库越跑越大成本失控重复入库、历史数据未清理统计向量库文档数量变化建立增量入库机制定期清理过期 chunk还有一个我特别想强调的坑Document的 id 必须稳定。如果每次入库都生成新的随机 id同一份文档更新之后旧版本不会被覆盖向量库会积累大量过期数据检索结果自然被污染。我现在的做法是在切片时用source chunk_index生成确定性 id更新文档时先按 source 删除旧 chunk再写入新 chunk。排查这类问题最有效的手段是把 RAG 链路的中间结果全部打日志。用户问题是什么、粗召回返回了哪些 chunk、Rerank 之后保留了哪些、最终拼进 prompt 的上下文是什么全部记录下来。这样一旦线上答案质量出问题能快速定位是检索环节还是生成环节的问题。我在项目里做了一个简单的调试页面开发模式下能看到每一次问答的完整链路调试效率高了很多。5.2 性能与成本平衡的建议RAG 工程化不只是效果要准还要考虑延迟和成本。在实际项目里我按经验画了这样一条资源投入的优先级第一优先把 Chunking 做好。这个阶段只花开发时间不增加任何推理成本但对效果的影响最大。第二优先加 Rerank。Rerank 每多跑一次都要增加计算成本但它能显著减少最终进入 LLM 的无效上下文反而能节省 token 消耗。第三优先做混合检索。关键词检索的成本很低主要增加的是开发复杂度和一个全文索引的存储成本。关于上下文长度我的建议是不要盲目追求把 Top-K 全塞给模型。实测下来大多数企业知识库问答场景给 LLM 的检索上下文控制在 3 到 5 个 chunk 最合适。检索质量足够高的时候3 个高质量 chunk 比 8 个凑数的 chunk 效果好得多而且延迟和 token 成本都会明显降低。还有一个容易被忽视的点query 改写Query Rewriting在高频场景里很重要。用户提问往往很口语化比如那个单子怎么样了如果不做改写直接检索召回效果很差。Spring AI 2.0 提供了RewriteQueryTransformer可以用 LLM 把口语化问题改写成适合检索的书面表达。但注意这是有 extra LLM 调用成本的建议只在检索结果质量明显偏弱时开启或者用规则先做简单的关键词提取避免所有请求都额外多一次模型调用。写在最后的一点经验把 Chunking、混合检索、Rerank 这三层真正串起来之后我的一个直观感受是RAG 的效果不是靠某一个大招提升的而是每一层都做对一点最后叠加出来的。切片留好了语义边界检索才能召得全混合检索解决了不同表达方式的覆盖问题Rerank 把精度补上最后一刀。每一步单独看都算不上惊艳但合在一起线上问答质量的提升是肉眼可见的。最后再分享一个小技巧任何 RAG 优化改动上线前一定要准备一组固定的评估问题集几十到上百条覆盖典型场景的问答对用这组问题反复跑对比改动前后的答案质量。不要凭感觉判断好像变好了用同一套标准反复验证才能避免优化一个地方、弄坏另一个地方。这套评估集越贴近真实用户提问你在 RAG 工程化上走的弯路就会越少。