RAG工程优化实战:Chunking、混合检索与Rerank核心策略
发布时间:2026/9/25 19:41:12来源:尧图网络
1. 为什么 RAG 工程优化绕不开 Chunking、混合检索和 RerankRAG 这个词现在已经被说烂了但真正在生产环境里跑过知识库问答的人都知道把文档塞进向量库、检索出 Top-K 丢给大模型这套最朴素的流程在实际业务里几乎不可用。问题出在哪检索回来的内容要么答非所问要么关键信息被切碎要么明明知识库里有答案却排在第 15 位被截断。Spring AI 2.0 在 RAG 工程化上做了不少改进但框架只是给了你工具怎么用好这些工具才是分水岭。这篇文章要聊的就是 RAG 工程优化里最核心的三件事Chunking分块策略、混合检索Hybrid Search和Rerank重排序。这三个环节直接决定了你的知识库问答系统是能用还是好用。我见过太多团队在 Embedding 模型选型上纠结了两周结果 Chunking 用了个固定 512 字符切分检索效果一塌糊涂。也见过有人把向量检索的 Top-K 调到 50指望大模型自己从里面挑出正确答案结果上下文塞满了噪声模型反而被带偏。这篇文章适合谁看如果你正在用 Spring AI 搭建 RAG 知识库或者已经搭了一个但效果不理想想从工程层面做优化那这篇内容就是写给你的。我会从设计思路、核心原理、实操步骤到踩坑经验把这三个环节拆开揉碎讲清楚。代码基于 Spring AI 2.0 的 API 风格但思路是通用的换成其他框架也一样适用。注意本文涉及的代码示例基于 Spring AI 2.0 的公开 API 风格编写具体版本号和方法签名请以你项目实际依赖的版本为准。不同版本之间 API 可能有调整建议先确认版本再对照修改。2. Chunking 策略别再用固定长度切分了2.1 固定长度切分为什么不够用大部分 RAG 入门教程里Chunking 这一步都是这么写的用TokenTextSplitter或者RecursiveCharacterTextSplitter设置 chunkSize 为 500 到 1000overlap 为 50 到 100然后一把梭。这个方案不是不能用但它的问题在于它假设所有文本的结构是均匀的。现实中的文档是什么样技术手册里有大段代码块和表格法律合同里有条款编号和嵌套引用产品文档里有标题层级和列表项。你用固定长度去切结果就是一个完整的代码示例被切成两半前半段在 chunk 3后半段在 chunk 4一个表格的表头和表体分到了不同的 chunk一个条款的但书部分被切到了下一个 chunk检索的时候只召回了主条款但书没回来答案就错了。我做过一个对比测试同一份技术文档固定 512 字符切分和按语义结构切分在 100 个测试问题上的召回率差了将近 30 个百分点。这不是模型的问题是切分策略的问题。2.2 语义分块的核心思路语义分块Semantic Chunking的核心思想很简单让每个 chunk 尽可能包含一个完整的语义单元。具体怎么做常见的有几种方案。第一种是基于文档结构的切分。Markdown 文档按标题层级切HTML 按 DOM 结构切PDF 按段落和章节切。Spring AI 2.0 里可以用DocumentReader配合自定义的TextSplitter来实现。比如你可以在TokenTextSplitter的基础上先按\n##这样的标题标记做一级切分再对每个章节内部做二级切分。第二种是基于语义相似度的切分。把文本按句子拆开计算相邻句子的 Embedding 相似度当相似度低于某个阈值时认为语义发生了转折在这里切一刀。这个方案效果不错但计算成本高适合对质量要求极高的场景。第三种是递归切分加结构感知。这是目前工程上最实用的方案。RecursiveCharacterTextSplitter本身就支持按分隔符优先级递归切分你可以把分隔符列表配置成[\n## , \n### , \n\n, \n, 。, , , , , , ]这样它会优先在标题处切其次在段落处切最后才在句子和词语处切。// Spring AI 2.0 风格的 TokenTextSplitter 配置示例 TokenTextSplitter splitter TokenTextSplitter.builder() .withChunkSize(800) // 每个 chunk 的目标 token 数 .withMinChunkSizeChars(350) // 最小字符数避免产生过短的 chunk .withMinChunkLengthToEmbed(5) // 低于此长度的 chunk 不参与 embedding .withMaxNumChunks(10000) // 单文档最大 chunk 数防止异常文档撑爆内存 .withKeepSeparator(true) // 保留分隔符保持语义完整性 .build();2.3 Chunk 大小和 Overlap 怎么定Chunk 大小定多少这个问题没有标准答案但有几个经验值可以参考。Chunk 大小如果你的知识库以问答对为主chunk 可以小一些256 到 512 token 就够了因为每个问答对本身就是独立的语义单元。如果是技术文档、研究报告这类需要上下文连贯的内容chunk 要大一些800 到 1200 token 比较合适。太小了信息不完整太大了检索精度下降。Overlapoverlap 的作用是防止关键信息刚好落在切分边界上被切断。一般设置为 chunk 大小的 10% 到 20%。比如 chunk 是 800 tokenoverlap 设 100 到 160 token。overlap 太大浪费存储和检索带宽太小起不到保护作用。这里有个容易被忽略的点overlap 不是越大越好。我见过有人把 overlap 设成 chunk 大小的一半结果检索出来的 Top-5 里有 3 个是高度重叠的内容浪费了宝贵的上下文窗口。而且 overlap 大了之后向量库里会有大量近似重复的向量检索时互相竞争反而降低了多样性。2.4 特殊内容的处理技巧表格和代码块是 Chunking 的两个老大难。表格的处理原则是能不分就不分。如果一个表格的 token 数在 chunk 大小范围内就把它作为一个完整的 chunk。如果表格太大必须分那就要在切分后的每个 chunk 里重复表头否则大模型看到一堆没有列名的数字根本不知道每一列是什么意思。代码块的处理原则是按函数或类切分不要按行数切分。一个完整的函数被切成两半检索出来也没法用。可以在切分前先用正则识别出代码块的边界把每个完整的代码块作为一个独立的 chunk如果代码块太大再考虑按函数签名切分。// 表格和代码块保护的切分预处理思路 public ListDocument smartSplit(Document doc) { String content doc.getContent(); // 1. 提取并保护代码块 ListString codeBlocks extractCodeBlocks(content); // 2. 提取并保护表格 ListString tables extractTables(content); // 3. 对剩余文本做常规切分 // 4. 将代码块和表格作为独立 chunk 插入 // 5. 为每个 chunk 添加元数据标记类型 return mergedChunks; }实操心得给每个 chunk 加上元数据如来源文档、章节标题、内容类型在检索后可以据此做过滤或加权。比如用户问的是代码相关的问题可以优先召回标记为代码类型的 chunk。这个技巧在混合检索里特别有用。3. 混合检索向量检索不是万能的3.1 纯向量检索的局限性向量检索的本质是语义相似度匹配。它擅长处理意思相近但用词不同的情况比如用户问怎么重置密码文档里写的是密码找回流程向量检索能匹配上。这是它的优势。但向量检索有几个硬伤。第一对精确匹配不敏感。用户问Spring AI 2.0 的 ChatClient 怎么配置向量检索可能召回一堆讲 ChatClient 用法的通用文档但真正包含2.0版本特性的文档反而排不到前面。第二对专有名词和缩写不友好。比如RAG、MCP、SSE这些缩写Embedding 模型可能没有充分训练过向量表示不准确。第三对数值和代码不敏感。用户问超时时间设多少文档里写的是timeout3000ms向量检索很难精确匹配到。3.2 混合检索的架构设计混合检索的思路是同时跑向量检索和关键词检索然后把两路结果融合。向量检索负责语义召回关键词检索通常是 BM25 或 TF-IDF负责精确匹配。两路互补召回率和准确率都能提升。在 Spring AI 2.0 里向量检索可以用VectorStore的similaritySearch方法。关键词检索 Spring AI 本身没有内置 BM25 实现但可以借助 Lucene 或者 Elasticsearch 来做。如果不想引入额外组件也可以用简单的 TF-IDF 实现一个轻量级的关键词检索器。融合策略常见的有两种。一种是加权求和给向量检索和关键词检索各分配一个权重比如 0.7 和 0.3然后把归一化后的分数加权相加。另一种是RRFReciprocal Rank Fusion不看具体分数只看排名把两路结果的排名做倒数融合。RRF 的好处是不需要调权重对分数尺度不敏感工程上更省心。// RRF 融合的简化实现 public ListDocument hybridSearch(String query, int topK) { // 向量检索 ListDocument vectorResults vectorStore.similaritySearch( SearchRequest.builder().query(query).topK(topK * 2).build() ); // 关键词检索 ListDocument keywordResults keywordSearcher.search(query, topK * 2); // RRF 融合 MapString, Double scoreMap new HashMap(); int k 60; // RRF 常数通常取 60 for (int i 0; i vectorResults.size(); i) { String id vectorResults.get(i).getId(); scoreMap.merge(id, 1.0 / (k i 1), Double::sum); } for (int i 0; i keywordResults.size(); i) { String id keywordResults.get(i).getId(); scoreMap.merge(id, 1.0 / (k i 1), Double::sum); } // 按融合分数排序返回 return scoreMap.entrySet().stream() .sorted(Map.Entry.String, DoublecomparingByValue().reversed()) .limit(topK) .map(e - findDocumentById(e.getKey())) .collect(Collectors.toList()); }3.3 关键词检索的实现选择关键词检索这块有几个方案可选。Lucene最轻量纯 Java 库不需要额外部署服务。适合中小规模知识库几万到几十万文档完全够用。缺点是分布式支持弱大规模场景需要自己分片。Elasticsearch功能最全BM25 实现成熟支持复杂的查询 DSL 和聚合分析。缺点是运维成本高需要单独部署集群。如果你的团队已经有 ES 集群直接复用是最省事的。PostgreSQL 全文检索如果向量库用的是 pgvector那全文检索可以直接用 PostgreSQL 的tsvector和tsquery不用引入新组件。缺点是中文分词需要额外配置默认的分词器对中文支持不好。轻量级 TF-IDF如果知识库规模不大几千文档以内自己实现一个简单的 TF-IDF 检索器也就百来行代码。优点是零依赖缺点是效果不如 BM25而且没有倒排索引检索速度慢。我的建议是优先复用现有基础设施。已经有 ES 就用 ES已经用 pgvector 就用 PostgreSQL 全文检索。如果什么都没有从 Lucene 起步是最平衡的选择。3.4 中文分词的坑做中文 RAG 知识库分词是个绕不过去的坎。英文按空格切就行了中文得用分词器。Lucene 自带的中文分词器是单字切分效果很差。需要引入 IK Analyzer 或者 HanLP 这类中文分词器。IK Analyzer 的配置要注意两点。第一自定义词典。你的业务领域里的专有名词比如产品名、技术术语需要加到自定义词典里否则会被切碎。第二停用词表。中文里的的、了、是这些停用词要过滤掉否则会干扰 BM25 的打分。!-- IK Analyzer 自定义词典配置示例 -- properties commentIK Analyzer 扩展配置/comment entry keyext_dictcustom/mydict.dic/entry entry keyext_stopwordscustom/mystopword.dic/entry /properties注意分词器的选择要和你的 Embedding 模型匹配。有些 Embedding 模型对中文分词不敏感直接用字符级输入也能工作。但关键词检索这块分词质量直接决定 BM25 的效果不能马虎。4. Rerank把最相关的文档推到最前面4.1 为什么需要 Rerank混合检索解决了召回的问题但召回回来的文档排序不一定准确。向量检索用的是双塔模型Bi-Encoder查询和文档分别编码然后算余弦相似度。这种架构速度快但精度有限因为查询和文档之间没有交互。Rerank 用的是交叉编码器Cross-Encoder把查询和文档拼在一起输入模型模型直接输出相关性分数。因为有交互精度比双塔模型高很多。代价是速度慢没法对全量文档做实时打分只能对召回回来的 Top-N 做精排。典型的 RAG 流程是召回阶段取 Top-50 到 Top-100Rerank 阶段精排出 Top-5 到 Top-10最后丢给大模型。这样既保证了召回率又保证了精度还控制了上下文长度。4.2 Rerank 模型的选型Rerank 模型的选择要考虑几个因素效果、速度、部署成本、语言支持。BGE-Reranker智源研究院出的开源模型有 base 和 large 两个版本。中文效果很好英文也不错。base 版本推理速度快适合对延迟敏感的场景。large 版本效果更好但需要更多显存。Cohere RerankCohere 提供的 API 服务效果很好支持多语言。缺点是要走网络请求有延迟和成本而且数据要传到第三方。Jina RerankerJina AI 出的模型也有 API 和本地部署两种方式。效果不错支持长文本。Cross-Encoder/ms-marcoSentence-Transformers 提供的预训练交叉编码器英文效果好中文一般。我的建议是中文场景优先用 BGE-Reranker本地部署数据不出域效果有保障。如果显存有限用 base 版本如果追求极致效果用 large 版本。英文场景可以考虑 Cohere 或 Jina 的 API省去部署麻烦。4.3 本地部署 Rerank 模型的实操在 Windows 上部署 Rerank 模型最省事的方案是用 ONNX Runtime 或者直接用 Python 起一个推理服务。方案一Python FastAPI sentence-transformers。这是最直接的方式几行代码就能起一个 Rerank 服务。# rerank_service.py from fastapi import FastAPI from pydantic import BaseModel from sentence_transformers import CrossEncoder app FastAPI() model CrossEncoder(BAAI/bge-reranker-base, max_length512) class RerankRequest(BaseModel): query: str documents: list[str] top_k: int 5 app.post(/rerank) def rerank(req: RerankRequest): pairs [[req.query, doc] for doc in req.documents] scores model.predict(pairs) ranked sorted(zip(req.documents, scores), keylambda x: x[1], reverseTrue) return {results: [{document: d, score: float(s)} for d, s in ranked[:req.top_k]]}启动命令uvicorn rerank_service:app --host 0.0.0.0 --port 8000方案二ONNX Runtime 直接推理。把模型导出为 ONNX 格式用 ONNX Runtime 在 Java 里直接调用省去 Python 服务。这个方案部署简单但需要自己处理 tokenization稍微麻烦一点。方案三vLLM 部署。如果你的环境里有 vLLM可以用它来部署 Rerank 模型。vLLM 的吞吐量比 sentence-transformers 高很多适合高并发场景。不过 vLLM 对 Rerank 模型的支持需要确认版本兼容性。实操心得Rerank 服务的超时时间要设置合理。我一般设 3 到 5 秒超时就降级为不 Rerank直接用混合检索的结果。这样即使 Rerank 服务挂了整个问答流程也不会完全不可用。4.4 Spring AI 中集成 RerankSpring AI 2.0 本身没有内置 Rerank 的抽象但可以通过自定义DocumentPostProcessor或者直接在检索后手动调用 Rerank 服务来实现。// 在检索流程中集成 Rerank public ListDocument retrieveWithRerank(String query, int topK) { // 1. 混合检索召回 Top-50 ListDocument candidates hybridSearch(query, 50); // 2. 调用 Rerank 服务精排 ListDocument reranked rerankService.rerank(query, candidates, topK); // 3. 返回精排后的结果 return reranked; }如果 Rerank 服务是 HTTP 接口可以用 Spring 的RestClient或WebClient调用。注意要加超时和重试机制Rerank 服务不稳定的时候不能拖垮整个问答链路。5. 完整 RAG 链路的工程化落地5.1 整体架构设计把 Chunking、混合检索、Rerank 串起来一个完整的 RAG 链路是这样的离线阶段文档加载 → 格式解析 → 智能切分 → 向量化 → 存入向量库 关键词索引。在线阶段用户查询 → 查询改写可选→ 向量检索 关键词检索 → RRF 融合 → Rerank 精排 → 上下文组装 → 大模型生成 → 返回答案。Spring AI 2.0 的ChatClient和VectorStore抽象让这个链路的搭建变得简单但每个环节的参数调优才是真正花时间的地方。5.2 参数调优的实操建议召回数量混合检索阶段建议召回 30 到 50 个候选。太少了 Rerank 没有发挥空间太多了 Rerank 延迟高。如果 Rerank 服务性能好可以放到 100。Rerank 后保留数量最终丢给大模型的 chunk 数量建议 3 到 8 个。太少了信息不全太多了噪声大而且浪费 token。具体数量取决于 chunk 大小和模型上下文窗口。相似度阈值向量检索可以设一个最低相似度阈值低于阈值的直接过滤掉。这个阈值需要根据你的 Embedding 模型和业务数据来调一般设在 0.5 到 0.7 之间。阈值太高会漏召回太低会引入噪声。RRF 常数 kRRF 公式里的 k 值通常取 60这是原论文的推荐值。k 越大排名靠后的文档得分衰减越慢融合结果越平滑。一般不需要改。5.3 效果评估怎么做RAG 系统调优不能凭感觉得有评估指标。常用的指标有指标含义目标值召回率RecallKTop-K 里包含正确答案的比例 90%精确率PrecisionKTop-K 里相关文档的比例 70%MRR第一个相关文档排名的倒数均值 0.8答案准确率最终答案正确的比例 85%平均延迟端到端响应时间 3s评估集的构建很关键。我一般会从真实用户问题里采样 100 到 200 个人工标注每个问题的正确答案和对应的文档 chunk。然后跑评估脚本对比不同参数配置下的指标变化。注意不要只盯着答案准确率。有时候答案对了但检索回来的文档其实不相关只是大模型自己脑补出了正确答案。这种 case 在评估时要特别标记出来因为它说明你的检索环节有问题只是被大模型的生成能力掩盖了。5.4 常见问题排查表问题现象可能原因排查方向检索不到相关文档Chunk 太大/太小、Embedding 模型不匹配检查 Chunk 大小换 Embedding 模型测试检索结果不相关纯向量检索对精确匹配不敏感引入混合检索调高关键词检索权重相关文档排名靠后缺少 Rerank 环节加入 Rerank检查 Rerank 模型效果答案不完整Chunk 切分破坏了语义完整性调整切分策略增加 Overlap答案包含错误信息检索到了过时或冲突的文档检查知识库更新机制加时间过滤响应太慢Rerank 或大模型调用延迟高加缓存优化 Rerank 批量推理换更快的模型中文检索效果差分词器配置不当换 IK Analyzer加自定义词典6. 几个容易踩的坑和我的经验之谈6.1 Chunking 的坑坑一Overlap 导致重复召回。前面提过overlap 太大会导致 Top-K 里全是近似重复的内容。我的做法是在 Rerank 之后加一个去重步骤如果两个 chunk 的文本相似度超过 0.9只保留分数高的那个。坑二元数据丢失。切分后的 chunk 如果不带元数据检索回来你都不知道它是从哪个文档、哪个章节来的。元数据在过滤、加权、展示引用来源的时候都用得上。Spring AI 的Document对象支持自定义元数据切分的时候记得把来源信息带上。坑三特殊字符处理。PDF 解析出来的文本经常有乱码、多余换行、页眉页脚。这些噪声如果不清理会严重影响 Embedding 质量。我一般会在切分前做一轮文本清洗去掉连续空行、页眉页脚模式、特殊 Unicode 字符。6.2 混合检索的坑坑一分数归一化。向量相似度和 BM25 分数的尺度完全不同直接加权求和会出问题。要么用 RRF 这种不看分数的融合方式要么先把两路分数归一化到 0 到 1 之间再加权。坑二关键词检索的召回为空。如果用户查询里的词在知识库里一个都没出现BM25 会返回空结果。这时候 RRF 融合就退化成了纯向量检索。可以在关键词检索前做一轮查询扩展用同义词或 Embedding 相似词来扩充查询词。坑三索引更新不同步。向量库和关键词索引是两套存储文档更新的时候要保证两边都更新。我见过有人只更新了向量库忘了更新 ES 索引结果新文档检索不到。建议把索引更新封装成一个事务性的操作要么都成功要么都回滚。6.3 Rerank 的坑坑一Rerank 模型和 Embedding 模型不匹配。Rerank 模型是在特定数据上训练的如果和你的 Embedding 模型风格差异太大效果可能不升反降。建议在正式上线前做 A/B 测试对比有 Rerank 和无 Rerank 的效果。坑二Rerank 延迟太高。Cross-Encoder 的计算量和文档数量成正比50 个文档的 Rerank 可能需要几百毫秒到几秒。如果对延迟敏感可以减少召回数量或者用更小的 Rerank 模型。坑三Rerank 服务单点故障。Rerank 服务挂了整个问答链路就断了。一定要加降级策略Rerank 超时或失败时直接返回混合检索的结果保证系统可用性。6.4 我的经验总结做了几个 RAG 项目之后我最大的体会是RAG 的效果优化是一个系统工程没有银弹。Chunking、混合检索、Rerank 每个环节都能提升效果但每个环节也都有代价。Chunking 做细了索引构建时间变长混合检索加了关键词检索系统复杂度上升Rerank 加了精排延迟增加。我的建议是分阶段优化。第一版先把基础链路跑通用固定切分 纯向量检索看看效果基线在哪。第二版优化 Chunking把切分策略改成结构感知的看看召回率提升多少。第三版加混合检索第四版加 Rerank。每加一个环节都做一次评估确认效果有提升再保留没提升就回退。还有一个容易被忽略的点查询改写。用户的问题往往很口语化直接拿去检索效果不好。可以在检索前加一步查询改写用大模型把用户问题改写成更适合检索的形式。这一步对最终效果的提升有时候比 Rerank 还明显。最后分享一个小技巧给 chunk 加摘要。在切分之后用大模型给每个 chunk 生成一句话摘要把摘要和原文一起做 Embedding。检索的时候摘要能帮助匹配到语义相关但用词不同的内容。这个技巧在长文档场景下特别有效因为长文档的 chunk 往往包含多个主题摘要能起到主题标签的作用。这个内容后续还可以这样扩展加入查询改写环节用大模型做查询扩展和改写引入 Agentic RAG 的思路让模型自己决定检索策略或者把 RAG 和 MCP 结合起来让知识库检索成为一个可被 Agent 调用的工具。这些方向都值得深入探索但前提是先把 Chunking、混合检索、Rerank 这三个基础环节做扎实。
网站建设高端定制企业官网