第六章 工业级 RAG:优化、评估与可运营系统¶
朴素 RAG 解决“能不能检索”,工业 RAG 解决“在真实数据、真实权限和真实流量下,能否稳定给出可追溯答案”。本章把优化分为查询、索引、召回、重排、上下文、生成和运营七层。优化必须由错误分析驱动,不要一次叠加十种技巧后只看主观示例。

图 6-1 离线评估找到失败类型,线上反馈产生新样本,数据与组件按版本迭代。
6.1 查询变换¶
先建立直觉。 用户查询常缺少上下文、包含代词或一次询问多个目标。查询变换要提高可检索性,同时保留原意并避免引入用户没有说过的事实。 读这一节时,先不要急着记缩写,而要不断追问:输入是什么,经过了哪些可观察变换,输出又怎样被验证。
把过程拆开看:
- 结合对话消解指代。
- 生成独立检索查询。
- 必要时拆为子问题。
- 保存原查询用于最终回答。
最小例子。 ‘它支持退款吗’需要从对话确定‘它’指哪个产品;若不确定,应先澄清而不是擅自选实体。 例子越小,越容易手算、打印中间量并判断实现是否偏离定义。
容易踩坑。 改写模型加入答案、把所有查询扩展成冗长段落、多个子查询结果不去重,会降低精度与成本。 排错时应保存失败输入和版本,先定位最早出现异常的步骤,再决定是否调整模型或超参数。
动手任务:为十个多轮问题人工写标准独立查询,评估自动改写的实体保持率。 完成后不要只保存最终输出,还要保存代码、配置、中间张量或检索结果,以及你对结果的解释。
用户查询常缺省上下文,例如“它什么时候生效”。首先要结合对话把问题改写为独立查询,但不能把模型猜测当事实。可让模型输出 standalone_query 和 assumptions,对关键假设要求用户确认。
Multi-query 生成多个检索视角,适合术语不一致;HyDE 先生成假设文档再嵌入,可能改善语义对齐,也可能把错误假设带入检索;查询分解把复杂问题拆成多个子问题,再合并证据。是否启用应看评估集,不是“越高级越好”。
6.2 混合检索与融合¶
从问题出发。 稀疏检索擅长关键词和编号,稠密检索擅长语义改写。混合检索用融合而非简单拼分数,因为两类分数尺度通常不可比。 先把名词放到一边,沿着输入、状态变化和输出走一遍;只有能指出证据落在哪一步,概念才算真正掌握。
沿数据流逐步检查:
- 分别运行 BM25 与 Dense。
- 保留各自排名。
- 用 RRF 或学习融合。
- 再做去重和权限过滤。
用小数据走一遍。 RRF 按 1/(k+rank) 累积分数,不要求原始分数同尺度;同一文档在两路都靠前会获得更高融合排名。 先在纸面预测结果,再让程序打印中间状态;预测与运行不一致的地方,正是需要继续追查的知识缺口。
这里最容易出现的误解。 直接相加余弦与 BM25 分数、先截断过小 top-k、融合后丢失来源路由,会让结果不稳定。 出现异常时先冻结样本、配置和环境,从最靠近输入的环节开始验证;不要同时更换模型、数据和超参数。
小实验:实现 RRF,用包含产品型号与同义问法的评估集比较三种检索。 提交物应包含预期、运行证据和失败记录;只有别人能按同样步骤复现,结果才具有学习价值。
Dense 检索擅长语义相似,BM25 擅长专有名词、编号和精确词。混合检索分别召回后可用 Reciprocal Rank Fusion:
$$ \mathrm{RRF}(d)=\sum_r\frac{1}{k+\mathrm{rank}_r(d)}. $$
from collections import defaultdict
def reciprocal_rank_fusion(rank_lists, k=60):
scores = defaultdict(float)
for ranked_ids in rank_lists:
for rank, doc_id in enumerate(ranked_ids, start=1):
scores[doc_id] += 1.0 / (k + rank)
return sorted(scores.items(), key=lambda x: x[1], reverse=True)
RRF 不依赖不同检索器分数的量纲,容易作为强基线。若业务有时间、地区、产品线和权限条件,应先做元数据过滤或按业务规则融合。
6.3 重排与上下文压缩¶
先看它解决什么。 重排器在候选集上做更昂贵的查询—文档联合判断,上下文压缩则只保留支持回答的句段。二者目标不同:前者改顺序,后者省窗口。 理解的标准不是会复述定义,而是能画出数据流、预测中间结果,并设计一个让错误暴露出来的检查。
可以把实现分成以下环节:
- 宽召回获得候选。
- Cross-Encoder 逐对评分。
- 按预算选择文档。
- 抽取关键句并保留来源映射。
一个可以手算的例子。 召回 50 个块、重排取 8 个,再压缩为 2,000 token;任何抽取句都必须能回指原块与页码。 这里故意不用大模型或大数据,因为小输入能把每个轴、分数和状态变化完整暴露出来。
排错时先看这些地方。 在召回不足时指望重排创造正确文档、压缩器改写事实、批处理不当造成巨大延迟,都是常见问题。 修复不能止于‘这次跑通’,还要把失败样本变成自动测试,防止同类问题在下一版本重新出现。
现在动手:分别测召回 top-k、重排 top-n 与压缩长度对 Recall、引用准确率和 P95 的影响。 把关键断言写进测试,并在 README 说明怎样运行、怎样判断正确、当前实现还不支持什么。
双塔 Embedding 预先编码文档,召回快;Cross-Encoder 把 query 与候选文本共同编码,交互充分但更慢。因此常先召回几十到几百条,再重排到少量证据。重排模型要匹配语言和领域,并测批处理延迟。
上下文压缩不只是摘要。可从 chunk 中抽取与查询相关句子、合并相邻片段、保留标题链和表头。摘要会引入生成误差,高风险场景应保留原文引用并允许回看。
“Lost in the Middle” 提醒我们:长上下文不保证每段都被同等利用。把最强证据放在模型更容易关注的位置,并减少相互矛盾与重复片段,往往比无限扩充上下文有效。
6.4 自适应 RAG 与反馈回路¶
抓住这一节的主线。 自适应 RAG 先判断问题是否需要检索、需要哪类数据源和检索深度,再依据结果质量决定补检索或拒答。反馈必须可审计,不能直接让线上点击无限改变索引。 遇到新术语,先找它在系统中的位置:它读取什么、保存什么、改变什么,以及失败时会留下什么信号。
真正动手时按这个顺序走:
- 查询分类与路由。
- 质量门控检查证据。
- 必要时改写重试。
- 记录反馈进入离线评审。
先做最小实验。 闲聊可不检索,订单状态走结构化 API,政策问题走文档索引;没有足够证据时返回限制说明。 把随机性固定并保留中间量,这个例子就能成为后续优化时的回归基线。
别被表面现象带偏。 路由标签没有评估、失败无限循环、把用户点赞直接当事实正确、不同租户反馈混合,都会带来风险。 先区分定义错误、实现错误、数据问题和性能瓶颈。四类问题需要的证据不同,不能靠盲目调参混在一起处理。
验证任务:设计路由混淆矩阵,并为每个路由规定最大步数与降级答案。 实验结束后写三句话:观察到了什么、这些证据支持什么结论、还有哪种解释尚未排除。
不是每个问题都需要检索。系统可先分类:闲聊直接回答;知识问题检索;实时问题走搜索/API;结构化数据走 SQL;高风险问题转人工。分类错误会导致系统性失败,所以要保留兜底路径。
反馈回路可基于低置信度、引用缺失、用户点踩和人工纠错触发再次检索。不要让模型无限自我反思;设置最大轮数、预算和停止条件。反馈数据进入训练或评估前要去重、脱敏和人工抽检,避免把恶意输入写回知识库。
6.5 多模态、表格与图 RAG¶
先把概念落到可观察对象上。 多模态与表格 RAG 的难点是保持结构关系;图 RAG 则显式表示实体与关系。它们不是默认更好,只有普通文本检索无法表达问题结构时才值得引入。 这一节的重点是建立因果链。先说明问题,再看计算或流程,最后用可重复实验验证结论。
把抽象概念还原成操作:
- 保留页面布局与坐标。
- 为图像生成可追溯描述或向量。
- 为表格保存表头关系。
- 图谱实体链接并记录证据。
把它缩小到能逐项检查。 询问‘2024 年华东区哪个季度增长最高’需要保留年份、区域、季度和指标列,单行文本块可能丢失这些关系。 如果这个小例子还不能解释清楚,扩大数据只会让错误更难发现。
需要特别守住的边界。 OCR 文本与图像重复入库、实体消歧错误、图谱边无来源、把图 RAG 当万能推理器,都会降低可信度。 保留完整 trace 比猜原因更重要。找到第一次偏离预期的位置,通常比分析最终错误输出更高效。
本节练习:选择一张表和一页图文 PDF,设计可回答五个问题的结构化表示。 除代码外,请保留一份结果说明,标明环境、随机种子、输入规模和你主动检查过的边界。
扫描 PDF 需要 OCR 与版面分析;图片应生成可检索描述并保留原图坐标;表格应保留表头、单位和合并单元格关系。多模态 Embedding 可以统一召回,但最终证据展示仍要回到原始页面。
Graph RAG 适合实体关系、多跳问题和全局主题总结。它需要实体消歧、关系抽取、社区或路径检索,构建和更新成本更高。若问题主要是单文档事实查找,普通 RAG 往往更简单可靠。
6.6 评估体系:从数据集到故障归因¶

图 06-2 把检索和回答拆开评估,才能识别参数记忆猜对与证据被忽略。 先建立直觉。 工业评估从真实流量抽样,建立可版本化数据集,并把错误归因到组件。检索、回答、忠实度、引用、延迟、成本与安全必须分别测量。 读这一节时,先不要急着记缩写,而要不断追问:输入是什么,经过了哪些可观察变换,输出又怎样被验证。
把过程拆开看:
- 定义任务与失败分类。
- 分层抽样并双人标注。
- 建立基线和置信区间。
- 发布前后持续回归。
最小例子。 总体准确率不变时,政策题可能提升而表格题下降;分桶结果能揭示回归。 例子越小,越容易手算、打印中间量并判断实现是否偏离定义。
容易踩坑。 只用 LLM judge、没有人工校准、测试集泄漏到提示、指标定义随版本变化,会让结果不可比较。 排错时应保存失败输入和版本,先定位最早出现异常的步骤,再决定是否调整模型或超参数。
动手任务:写一份 100 题评估方案,规定抽样比例、标注规范、冲突仲裁和报告模板。 完成后不要只保存最终输出,还要保存代码、配置、中间张量或检索结果,以及你对结果的解释。
评估集应覆盖头部问题、长尾问题、不可回答、冲突文档、过期文档、权限边界、表格、长文和对抗输入。每条样本至少标注:期望证据、参考结论、可接受变体、风险级别。
将端到端错误拆为:解析错误、切块错误、召回漏失、排序错误、上下文组装错误、生成不忠实、引用错误和业务规则错误。只有先归因,才能知道该换 Embedding、改 chunk、加重排还是改提示。
线上监控除延迟和错误率,还要看空召回率、证据覆盖、引用点击、拒答率、每查询 token、缓存命中和不同租户的质量差异。用户满意度受界面和期望影响,不能替代客观正确性。
6.7 可运营架构¶
从问题出发。 可运营 RAG 需要数据面与服务面分离:摄取流水线负责版本、权限和索引发布,在线服务负责查询、检索、生成与观测。 先把名词放到一边,沿着输入、状态变化和输出走一遍;只有能指出证据落在哪一步,概念才算真正掌握。
沿数据流逐步检查:
- 原始文档不可变存档。
- 增量解析与索引构建。
- 影子验证后原子切换。
- 在线 trace 关联每个版本。
用小数据走一遍。 删除一份文档时,不只从向量库删块,还要更新缓存、倒排索引、图谱和审计记录,并证明用户无法再检索。 先在纸面预测结果,再让程序打印中间状态;预测与运行不一致的地方,正是需要继续追查的知识缺口。
这里最容易出现的误解。 原地更新唯一索引、权限只在生成前检查、没有文档 lineage、缓存键不含租户与版本,都会造成泄漏或不一致。 出现异常时先冻结样本、配置和环境,从最靠近输入的环节开始验证;不要同时更换模型、数据和超参数。
小实验:画出双索引蓝绿发布架构,并写回滚、删除和权限变更流程。 提交物应包含预期、运行证据和失败记录;只有别人能按同样步骤复现,结果才具有学习价值。
把文档接入与在线问答解耦。离线服务负责解析、去重、切块、Embedding、索引和版本发布;在线服务负责路由、检索、重排、生成和引用;评估服务复放固定数据集;观测平台关联请求 trace 与组件版本。
索引发布应原子切换并可回滚。文档删除要传播到向量、关键词、缓存和备份策略。多租户系统应把权限字段写入索引并在查询层强制过滤,不能生成后再删敏感句子。
6.8 成本与延迟优化¶
先看它解决什么。 延迟优化先看时间分解:查询改写、两路召回、重排、生成各占多少。成本优化要看 token、模型调用、索引与存储,而不是只换便宜模型。 理解的标准不是会复述定义,而是能画出数据流、预测中间结果,并设计一个让错误暴露出来的检查。
可以把实现分成以下环节:
- 埋点得到阶段耗时。
- 并行独立检索。
- 批处理重排与 embedding。
- 缓存稳定且无权限风险的结果。
一个可以手算的例子。 若 70% 时间花在生成,继续优化向量索引收益有限;若 TTFT 被串行改写和重排占满,应先并行或路由。 这里故意不用大模型或大数据,因为小输入能把每个轴、分数和状态变化完整暴露出来。
排错时先看这些地方。 缓存包含用户敏感结果、为了省 token 截断关键证据、只看均值不看长尾、压测数据过短,都会制造假优化。 修复不能止于‘这次跑通’,还要把失败样本变成自动测试,防止同类问题在下一版本重新出现。
现在动手:为一次请求画甘特图,提出三项优化并预估对 P50/P95、成本和质量的影响。 把关键断言写进测试,并在 README 说明怎样运行、怎样判断正确、当前实现还不支持什么。
常用手段包括:缓存查询 Embedding 与稳定答案、缩小重排候选、并行关键词/向量检索、批量 Embedding、对长文异步建索引、对低风险查询使用小模型。缓存键必须包含知识库版本、权限范围和提示版本,否则会返回过期或越权答案。
性能优化要保持质量门槛。例如把 top_k 从 20 降到 5 可降低重排成本,却可能伤害长尾 Recall。每个优化都应在同一评估集上同时报告质量、P95 和成本。
6.9 项目表达:不要只写“搭建 RAG”¶
抓住这一节的主线。 项目表达要让读者或面试官能复现你的判断:问题、约束、基线、个人动作、实验、指标、失败和遗留问题缺一不可。 遇到新术语,先找它在系统中的位置:它读取什么、保存什么、改变什么,以及失败时会留下什么信号。
真正动手时按这个顺序走:
- 说明为什么需要 RAG。
- 给出数据规模与评估定义。
- 比较至少一个基线。
- 明确自己的决策和边界。
先做最小实验。 ‘Recall@5 从 0.69 到 0.84’还需说明查询数、相关性定义、置信区间、索引版本和硬件。 把随机性固定并保留中间量,这个例子就能成为后续优化时的回归基线。
别被表面现象带偏。 只写使用了某框架、数字无出处、把团队成果全算个人、没有 bad case,会让项目不可信。 先区分定义错误、实现错误、数据问题和性能瓶颈。四类问题需要的证据不同,不能靠盲目调参混在一起处理。
验证任务:把自己项目写成 200 字摘要,再列出可能被追问的十个证据问题。 实验结束后写三句话:观察到了什么、这些证据支持什么结论、还有哪种解释尚未排除。
一条可信的项目经历应包含业务约束、数据规模、本人负责、技术取舍、评估方法和量化结果。例如:
为内部制度问答构建可追溯 RAG;负责 PDF 版面解析、标题感知切块和混合检索。基于 420 条人工标注查询将 Recall@5 从 0.71 提升至 0.86,引用准确率从 78% 提升至 91%;通过批量重排与缓存把 P95 从 3.2 秒降至 1.9 秒。所有数字来自固定版本评估与压测环境。
不能验证的百分比不要写。面试官会追问数据怎么标、基线是什么、是否显著、线上是否一致、你做了哪部分。
6.10 实战路线¶
先把概念落到可观察对象上。 实战应从小而真实的数据集开始,先建立可解释基线,再逐项增加复杂度。每次只改变一个主要变量并保留回归结果。 这一节的重点是建立因果链。先说明问题,再看计算或流程,最后用可重复实验验证结论。
把抽象概念还原成操作:
- 建立 50—200 条标注查询。
- BM25 与 Dense 做基线。
- 加入混合和重排。
- 上线监控并维护错误簿。
把它缩小到能逐项检查。 先确保目标证据可被 BM25 找到,再比较 embedding;否则切块或解析问题会被误判成模型问题。 如果这个小例子还不能解释清楚,扩大数据只会让错误更难发现。
需要特别守住的边界。 一开始就上图谱、多 Agent 和复杂框架,评估集却只有几个演示问题,会让系统无法收敛。 保留完整 trace 比猜原因更重要。找到第一次偏离预期的位置,通常比分析最终错误输出更高效。
本节练习:完成一个两阶段里程碑:第一阶段可追溯基线,第二阶段针对最大错误类型优化并写实验报告。 除代码外,请保留一份结果说明,标明环境、随机种子、输入规模和你主动检查过的边界。
选择一组公开报告或产品手册,建立 100 条查询评估集。先做 BM25 与 Dense 两个基线,再加入 RRF、重排和查询改写,每次只改一个因素。输出四张表:检索指标、答案指标、延迟成本、错误类型分布。最后实现文档版本、权限过滤、引用跳转与一键回滚。
延伸资料:RAG Survey、Milvus 索引说明、Datawhale Agent/RAG 面试问题整理。
本章配套代码¶
下面的脚本与正文使用相同符号。建议先在代码中打印形状和中间量,再运行断言;若依赖尚未安装,至少先阅读入口函数、输入输出和测试部分。
examples/06_hybrid_rrf.py:混合检索 RRF 融合。
本章端到端实验:把知识变成可复现证据¶
本实验不是把本章代码重新抄一遍,而是把概念、实现、测试和解释串成一个小型工程。请新建独立目录,保存 README.md、环境文件、源代码、测试、运行日志和结果图。README 至少说明任务、输入输出、运行命令、预期现象、已知限制和复现条件。
实验步骤¶
- 为十个多轮问题人工写标准独立查询,评估自动改写的实体保持率。
- 实现 RRF,用包含产品型号与同义问法的评估集比较三种检索。
- 分别测召回 top-k、重排 top-n 与压缩长度对 Recall、引用准确率和 P95 的影响。
- 设计路由混淆矩阵,并为每个路由规定最大步数与降级答案。
- 选择一张表和一页图文 PDF,设计可回答五个问题的结构化表示。
- 写一份 100 题评估方案,规定抽样比例、标注规范、冲突仲裁和报告模板。
每完成一步,先写下预期,再运行代码。若结果与预期不一致,不要覆盖旧日志;建立 failures.md,记录现象、假设、证据、修复和回归测试。这样得到的不是一次性 Demo,而是一份能证明你真正理解本章内容的实验档案。
验收标准¶
- 全新环境能够按照 README 从头运行,依赖和随机种子已记录。
- 关键函数至少有正常、边界和错误输入三类测试;涉及数值计算时检查有限值与合理误差。
- 结果包含一个基线和至少一个受控改动,能够说明变化来自哪里。
- 日志保留输入规模、耗时、内存或显存、软件版本和失败样本。
- 结论区分“实验直接证明的事实”“根据事实做出的推断”和“仍未验证的猜想”。
本章自测¶
- 不看正文,用自己的话解释“用户查询常缺少上下文、包含代词或一次询问多个目标”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“稀疏检索擅长关键词和编号,稠密检索擅长语义改写”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“重排器在候选集上做更昂贵的查询—文档联合判断,上下文压缩则只保留支持回答的句段”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“自适应 RAG 先判断问题是否需要检索、需要哪类数据源和检索深度,再依据结果质量决定补检索或拒答”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“多模态与表格 RAG 的难点是保持结构关系”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“工业评估从真实流量抽样,建立可版本化数据集,并把错误归因到组件”,并给出一个可以证伪的测试。
回答时先画图或写形状,再给结论。若只能说出术语而不能给出最小例子、边界条件和验证方法,说明这一节仍需要回到代码中重做。