ARTICLE DETAIL

资讯详情

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

RAG 生产环境实战:分块、召回与重排的六条调优结论

RAG 生产环境实战:分块、召回与重排的六条调优结论 1. 从一次翻车现场说起RAG 到底难在哪去年秋天我接手了一个企业知识库项目文档量大概四万份格式从 PDF、Word 到 Confluence 导出的 HTML 都有。当时团队里所有人都觉得这事不难——LangChain 或者 Spring AI 的 RAG 模板一套向量库一接大模型一挂demo 当天就跑通了。演示的时候老板问了一个问题系统答得头头是道大家都很满意。上线第三天客服部门反馈说问“差旅报销标准是多少”系统给出的答案是三年前已经废止的旧版本。又过了两天技术部门问“接口超时重试策略怎么配”系统把五份不同产品的文档混在一起拼了个四不像的答案。最离谱的一次有人问“年假怎么算”它引用了一份员工手册里关于“年假申请流程”的段落但完全没提计算规则——因为计算规则在另一份文档里而分块的时候正好被切断了。这就是 RAG 落地最真实的模样demo 十分钟上线三个月。问题从来不在“能不能跑通”而在“跑通之后能不能用”。分块策略、召回质量、重排逻辑这三个环节里任何一个偷懒最后都会以“答非所问”的形式暴露在用户面前。我把这几个月踩过的坑和调优过程整理成六条实战结论每一条都对应一个具体的决策点和背后的取舍逻辑。如果你正在做 RAG 项目或者准备把现有的原型推向生产环境这些经验应该能帮你少走一些弯路。文章会涉及 embedding 模型选型、分块参数计算、召回策略对比、重排模型部署等具体操作也会给出可以直接参考的配置和代码片段。2. 分块不是切菜策略选择与参数计算2.1 为什么固定长度分块几乎总是错的刚开始做的时候我用的是最朴素的办法按 512 个 token 切重叠 50 个 token。这个方案在技术文档上勉强能用但遇到结构化内容就完蛋。比如一份产品规格书表格里的参数和表头被切到两个块里召回的时候只拿到半张表模型只能瞎猜。再比如法律条款一条完整的免责声明被从中间切开前半段说“本公司不承担以下责任”后半段列了五种情形结果只召回了后半段意思完全反了。固定长度分块的根本问题在于它假设文本的语义边界和 token 数量边界是一致的。但真实文档里一个完整的语义单元可能只有 30 个 token比如一个术语定义也可能有 800 个 token比如一段完整的操作步骤。用同一个长度去切所有内容必然会在某些地方切断语义。我后来换成了递归字符分块按优先级依次尝试用段落、换行、句号、逗号来切分。LangChain 的RecursiveCharacterTextSplitter就是这个思路但默认参数需要根据你的文档类型调整。对于中文技术文档我把分隔符优先级设成了[\n\n, \n, 。, , ]块大小设成 400重叠 80。这个 400 不是拍脑袋来的——我统计了文档里所有完整段落的 token 分布中位数在 380 左右取 400 能覆盖大部分自然段落。2.2 语义分块的实际效果与代价递归分块解决了大部分问题但还有一类场景搞不定跨段落的逻辑关联。比如一份故障排查指南第一段描述现象第二段分析原因第三段给出解决方案。按段落切分后这三段变成了三个独立的块召回时可能只命中其中一段模型就缺少了完整的上下文。这时候可以考虑语义分块。基本思路是先用 embedding 模型把每个句子向量化然后计算相邻句子的余弦相似度在相似度骤降的地方切分。我用的阈值是 0.75——低于这个值就认为语义发生了转折。实测下来语义分块在叙述性文档上效果很好能把完整的因果链保留在同一个块里。但代价也很明显。第一是计算成本四万份文档全部做句子级 embedding用 text-embedding-3-small 跑了一遍花了将近四十美元。第二是分块数量不稳定有的块只有两句话有的块有十几句导致后续召回时长度差异很大。第三是对于列表型内容比如参数表、步骤清单语义相似度一直很高反而不会切分最后得到一个超长的块。我的折中方案是混合策略先用递归分块做粗切然后对每个块做一次语义完整性检查。具体做法是计算块内首句和末句的 embedding 相似度如果低于 0.6说明这个块可能跨越了语义边界就把它拆成两个。这个检查的成本很低因为只需要对边界句子做 embedding不需要全量计算。2.3 分块参数的计算过程与实测数据块大小和重叠长度这两个参数我前前后后调了十几轮。下面这张表是不同参数组合在测试集上的召回命中率对比测试集包含 200 个问题每个问题都有标注的标准答案所在文档。块大小重叠长度召回命中率平均块数量备注2563261%18700块太碎上下文丢失严重5125074%9400基线方案表格和条款容易切断4008082%12100中文技术文档的最佳平衡点60010076%8100块太大噪声增多80015068%6200召回精度明显下降最终我选了 400/80 这组参数。这里有个细节重叠长度不是越大越好。我试过把重叠加到 150召回命中率反而降了 3 个百分点。原因是重叠部分会产生大量近似重复的块在向量空间里形成密集区域导致检索时容易命中这些“冗余块”而不是真正相关的块。另外不同文档类型应该用不同的参数。产品手册和 API 文档适合 300/60因为内容密度高、术语多培训材料和操作指南适合 500/100因为需要更完整的上下文。我在系统里做了一个文档类型识别根据文件路径和元数据自动选择分块参数这个改动让整体命中率又提升了 5 个百分点。注意分块之后一定要做一次人工抽检。我随机抽了 50 个块逐条阅读发现大约 8% 的块存在语义不完整的问题。这个比例在可接受范围内但如果超过 15%说明分块策略需要重新调整。3. 召回环节的取舍向量、关键词还是混合3.1 纯向量召回的三个致命盲区向量召回是 RAG 的默认方案但它有三个场景几乎必然失效。第一个是精确匹配场景。用户问“错误码 E5023 是什么意思”向量模型会把 E5023 编码成一个语义向量但错误码本身没有语义它就是一个标识符。结果就是召回了一堆包含其他错误码的文档唯独漏掉了 E5023 的那份。我试过用 text-embedding-3-large在这个场景下的命中率只有 43%。第二个是长尾术语。公司内部有很多自研系统的代号比如“天枢平台”“玄武网关”这些词在通用 embedding 模型的训练数据里根本不存在编码出来的向量是随机的。用户搜“天枢平台的鉴权方式”向量召回完全找不到相关文档。第三个是否定和排除条件。用户问“哪些接口不需要鉴权”向量模型会把“不需要鉴权”和“需要鉴权”编码成非常相似的向量因为它们在语义上高度相关。结果就是召回了一堆讲鉴权流程的文档但用户要的是例外清单。3.2 混合召回的实现与权重调优解决这三个盲区最直接的办法是引入关键词召回。我用的是 BM25 算法在 Elasticsearch 里建了一个倒排索引和向量库并行检索。BM25 对精确匹配和长尾术语的效果非常好E5023 这个场景下命中率直接拉到 91%。但 BM25 也有自己的问题它不理解同义词。用户搜“登录失败”文档里写的是“认证不通过”BM25 就匹配不上。所以最终方案是混合召回把向量得分和 BM25 得分加权融合。权重的确定过程是这样的我先用网格搜索跑了一遍向量权重从 0.3 到 0.9步长 0.1BM25 权重取补数。测试集上的结果如下向量权重BM25 权重命中率适用场景0.90.171%语义问答为主0.70.383%通用场景最佳0.50.579%术语密集场景0.30.768%精确检索为主最终我选了 0.7/0.3 这组。但这里有个坑向量得分和 BM25 得分的量纲不一样。向量相似度通常在 0.6 到 0.95 之间而 BM25 得分可能从 0 到 30 不等。直接加权会导致 BM25 主导结果。我的做法是先对两组得分分别做 min-max 归一化然后再加权。归一化之后0.7/0.3 的权重才真正生效。3.3 召回数量与上下文窗口的平衡召回多少个块合适这个问题我纠结了很久。召回太少可能漏掉关键信息召回太多上下文窗口塞不下而且噪声会干扰模型判断。我的做法是分两步先召回 top-50然后用重排模型精排到 top-5。为什么是 50 而不是 20 或 100因为我统计了测试集里标准答案在召回列表中的排名分布。结果显示92% 的标准答案出现在前 50 名里而前 20 名只覆盖了 78%。从 50 增加到 100覆盖率只提升了 3 个百分点但检索延迟翻了一倍。top-5 的选择依据是上下文窗口。我用的模型上下文是 8K token每个块平均 400 token5 个块就是 2000 token加上系统提示词和用户问题总共不到 3000 token留足了生成空间。如果块更大或者需要更多上下文可以放宽到 top-8但超过 8 个块之后模型对中间位置的块注意力明显下降这是“迷失在中间”现象的典型表现。实操心得召回阶段不要过早做截断。我见过有些方案在向量检索时就设 top-10然后直接送给模型。这样做的问题是重排模型没有足够的候选集来发挥价值。正确的做法是召回阶段放宽到 50 甚至 100把精排的压力交给重排模型。4. 重排模型从锦上添花到不可或缺4.1 为什么需要重排向量召回和 BM25 都是基于“相似度”的检索它们衡量的是查询和文档在向量空间或词频空间上的接近程度。但“相似”不等于“相关”。用户问“如何重置密码”一篇讲“密码强度要求”的文档在向量空间里和问题的距离可能很近但它并不是用户想要的答案。重排模型的作用就是引入一个更精细的相关性判断。它通常是一个交叉编码器把查询和每个候选文档拼接在一起输入模型输出一个相关性分数。因为查询和文档在模型内部做了充分的交互所以判断精度远高于双编码器的向量召回。我用的重排模型是 bge-reranker-v2-m3部署在一张 T4 显卡上延迟大概 80 毫秒处理 50 个候选块。这个延迟在可接受范围内因为向量召回本身也要 50 到 100 毫秒。4.2 重排前后的效果对比下面这组数据来自我的测试集对比了不同方案在 top-5 命中率上的表现方案top-5 命中率平均延迟纯向量召回62%45ms向量 BM25 混合74%90ms混合召回 重排89%170ms混合召回 重排 查询改写93%210ms重排带来的提升是决定性的从 74% 到 89%十五个百分点的差距。这十五个点意味着什么意味着每 100 个问题里少了 15 个答非所问的情况。对于企业知识库来说这个提升直接决定了用户会不会继续用这个系统。查询改写是另一个提升点。用户的问题往往口语化、有歧义比如“那个报错怎么解决”。查询改写模块会把这个问题扩展成“错误码 报错 解决方案 排查步骤”然后再去检索。我用的是一个小模型做 few-shot 改写成本很低但效果很明显。4.3 重排模型的部署与调优细节重排模型部署有几个坑要注意。第一是批处理大小。bge-reranker-v2-m3 在 T4 上batch size 设成 8 时延迟最低设成 16 反而变慢因为显存不够导致频繁换页。我实测下来batch size 8 的吞吐量是 batch size 16 的 1.4 倍。第二是分数阈值。重排模型输出的分数是 logits不是概率。我一开始直接拿分数排序后来发现有些低分块其实也包含关键信息。我的做法是设一个动态阈值取 top-5 的平均分然后保留所有分数高于平均分 0.8 倍的块。这样既能保证精度又不会漏掉边缘相关的块。第三是模型选择。bge-reranker-v2-m3 支持多语言中文效果很好。但如果你的场景是纯英文Cohere 的 rerank 模型效果更好只是需要调 API有数据出境的合规问题。本地部署的话bge 系列是目前最稳妥的选择。注意重排模型不是万能的。如果召回阶段完全没有命中相关文档重排也变不出来。我遇到过一个问题标准答案在召回列表里排第 87 位重排后只升到了第 12 位还是进不了 top-5。后来发现是分块的时候把关键段落切碎了调整分块参数后才解决。所以分块、召回、重排是一个链条任何一个环节出问题后面的环节都补不回来。5. 六个实战结论的完整复盘5.1 结论一分块策略决定召回上限这是我最深刻的体会。分块做不好后面怎么调召回和重排都是事倍功半。我做过一个对比实验同一套召回和重排配置分块参数从 512/50 改成 400/80top-5 命中率从 74% 直接跳到 82%。八个百分点的提升没有改任何模型没有加任何算力只是把块切得更合理了。具体来说分块要关注三个维度语义完整性、长度一致性、元数据保留。语义完整性靠递归分块和语义检查来保证长度一致性靠参数调优来控制元数据保留最容易被忽略——每个块必须带上来源文档、章节标题、页码这些信息否则重排和生成阶段无法做引用溯源。5.2 结论二混合召回是生产环境的标配纯向量召回在 demo 阶段够用但生产环境一定会遇到精确匹配和长尾术语的问题。混合召回不是可选项是必选项。BM25 的引入成本很低Elasticsearch 本身就支持关键是要做好分数归一化和权重调优。我现在的默认配置是向量 0.7、BM25 0.3但这个权重不是固定的。系统会根据查询类型动态调整如果查询里包含错误码、版本号、专有名词BM25 权重自动升到 0.5如果是自然语言问句向量权重保持 0.7。这个动态调整逻辑用简单的规则就能实现不需要机器学习模型。5.3 结论三重排的投入产出比最高如果只能在一个环节加资源我会选重排。向量召回和 BM25 都是“粗筛”重排是“精挑”。从 74% 到 89% 的提升只需要一张 T4 显卡和 80 毫秒延迟这个投入产出比在 RAG 系统里是最高的。但重排模型的选择要注意不要用太大的模型。我试过 bge-reranker-v2-gemma效果确实好一点但延迟从 80 毫秒涨到了 400 毫秒吞吐量降了五倍。对于在线服务来说这个代价太大了。m3 版本在效果和延迟之间取得了很好的平衡。5.4 结论四查询改写是低成本高回报的补充查询改写是我最后加上的模块但效果出乎意料。用户的问题往往很短、很模糊直接拿去检索效果很差。用一个小模型做 few-shot 改写把问题扩展成包含同义词和相关术语的查询召回命中率能提升 4 到 5 个百分点。改写的 prompt 我调了很多版最终稳定下来的格式是给定用户问题输出三个改写版本分别侧重同义词扩展、术语标准化、意图澄清。然后把三个版本分别检索结果合并去重。这个方案比单次改写效果更好成本也只是多两次检索。5.5 结论五评估体系比调优技巧更重要没有评估体系调优就是盲人摸象。我一开始靠感觉调参数改来改去不知道哪个方案更好。后来建了一个 200 题的测试集每道题标注了标准答案所在的文档和段落每次改动都跑一遍评估看命中率和 MRR 的变化。测试集的构建也有讲究。不能只挑简单问题要覆盖各种类型事实型、对比型、否定型、多跳型。我现在的测试集里事实型占 40%对比型占 25%否定型占 15%多跳型占 20%。这个分布和真实用户问题的分布基本一致。5.6 结论六RAG 是一个系统工程没有银弹最后一个结论可能有点泄气但这是事实RAG 没有一招制胜的方案。分块、召回、重排、改写、评估每个环节都要投入精力。我见过太多团队在向量库选型上纠结两周却在分块策略上花了两小时。这是本末倒置。如果让我重新做一遍这个项目我会把时间分配成这样分块策略 30%召回方案 25%重排部署 20%评估体系 15%查询改写 10%。这个分配不一定适合所有场景但至少反映了各个环节的相对重要性。6. 常见问题与排查技巧实录6.1 召回命中率突然下降怎么排查这是最常见的问题。我的排查顺序是这样的第一步检查文档更新。如果最近有新文档入库可能是新文档的分块参数和旧文档不一致导致向量空间分布变化。解决办法是统一分块参数重新索引。第二步检查查询分布。如果用户最近开始问一些新类型的问题而测试集没有覆盖命中率下降是正常的。解决办法是更新测试集补充新类型的问题。第三步检查 embedding 模型。如果模型版本更新了向量空间会发生变化旧索引和新查询不匹配。解决办法是锁定模型版本或者全量重建索引。第四步检查重排模型。如果重排模型的分数分布发生了变化可能是模型文件损坏或者配置被改。解决办法是回滚到上一个稳定版本。6.2 模型回答引用错误文档怎么办这个问题通常出在重排环节。重排模型把不相关的块排到了前面生成模型就照着这些块编答案。解决办法有两个一是提高重排模型的精度换更大的模型或者增加训练数据二是在生成阶段加一个引用校验让模型在回答时标注每个事实的来源块如果来源块和问题不相关就拒绝回答。我用的方案是第二种。在 prompt 里明确要求模型对每个关键事实标注来源然后在后处理阶段检查来源块的相关性分数。如果分数低于阈值就把这个事实从回答里删掉或者标记为“不确定”。6.3 多跳问题怎么处理多跳问题是指需要综合多个文档才能回答的问题。比如“A 系统的接口超时时间是多少这个时间在 B 系统的配置里怎么改”。标准答案分散在两个文档里单次召回很难同时命中。我的解决方案是迭代检索。第一轮先召回和 A 系统相关的块提取出超时时间这个关键信息然后用这个信息作为新的查询去检索 B 系统的配置文档。这个方案需要模型具备一定的推理能力我是在生成阶段让模型输出一个“需要进一步检索”的标记然后触发第二轮检索。6.4 常见问题速查表问题现象可能原因排查方法解决方案召回命中率下降文档更新或查询分布变化检查最近入库文档和用户查询日志统一分块参数更新测试集回答引用错误文档重排精度不足检查重排分数分布换更大模型或加引用校验多跳问题答不全单次召回覆盖不足分析标准答案的文档分布迭代检索或查询分解延迟突然升高索引膨胀或模型负载高检查索引大小和 GPU 利用率重建索引或扩容同义词搜不到纯向量召回盲区测试同义词查询引入 BM25 混合召回6.5 几个容易被忽略的细节第一个细节是文档预处理。PDF 里的表格、图片、页眉页脚如果不做处理直接抽取文本会引入大量噪声。我的做法是用布局分析工具先把文档切成文本块、表格块、图片块表格单独做结构化处理图片走 OCR 或者多模态 embedding。第二个细节是元数据设计。每个块除了文本内容还要存来源文件、章节路径、创建时间、文档类型这些元数据。这些信息在重排和生成阶段非常有用。比如可以根据文档类型调整重排权重技术文档的权重要高于会议纪要。第三个细节是缓存策略。高频问题的检索结果可以缓存避免重复计算。我用 Redis 做了一层查询缓存命中率大概 30%延迟从 170 毫秒降到了 20 毫秒。缓存失效策略是按文档更新事件触发有新文档入库就清空相关缓存。第四个细节是监控告警。RAG 系统需要监控的指标包括召回命中率、重排分数分布、生成答案的引用准确率、端到端延迟。这些指标要设阈值告警一旦异常就自动回滚到上一个稳定配置。7. 后续可以继续深挖的方向这套系统跑到现在大概半年整体命中率稳定在 90% 左右用户满意度比上线初期提升了很多。但还有几个方向我觉得值得继续投入。第一个是 GraphRAG。现在的方案是把文档切成独立的块块与块之间的关联丢失了。如果用知识图谱把实体和关系抽出来检索的时候可以沿着图谱遍历多跳问题的效果会好很多。我试过微软的 GraphRAG 方案在小型数据集上效果不错但索引成本很高四万份文档跑了一遍花了两天。第二个是 Agentic RAG。现在的流程是线性的召回、重排、生成。如果引入 Agent 机制让模型自己决定什么时候检索、检索什么、要不要多轮检索灵活性会高很多。AgentScope 2.0 在这方面做了一些探索我还在评估阶段。第三个是 embedding 模型的持续优化。通用 embedding 模型在垂直领域的效果有限用领域数据做微调可以显著提升召回质量。我收集了大概五千对查询-文档对准备做一轮微调实验目标是让 top-50 召回覆盖率从 92% 提升到 96%。这些方向都需要投入时间和算力但 RAG 这个领域就是这样没有一劳永逸的方案只有持续迭代。我现在的心态是先把基础的分块、召回、重排做扎实再考虑上层的高级玩法。基础不牢再花哨的架构也是空中楼阁。
返回列表