主题
RAG 检索与生产调优
一句话:RAG 的上半场是把文档变成可检索资产(解析/切片/混合检索/元数据权限),下半场是把"查得到"调成"答得对"(重排漏斗/GraphRAG/Agentic RAG/失败归因)。本篇合并两半场,按管线顺序读。
上篇 · 检索管线与资产治理
为什么需要
三类问题参数化记忆解决不了:训练截止之后的事实、企业私有文档、需要精确引用与可删除的知识。模型的权重是训练时的压缩快照,它无法告诉你某条制度是第几版,也无法在文档下架后"忘记";它只会按概率生成一个看起来合理的答案。
RAG 的因果链很短:把知识从"权重里"移到"外部索引里",推理时按需取回。于是更新知识变成更新索引,回答可以附出处,权限可以按文档控制。代价是把一个"生成问题"拆成了"检索 + 生成"两个问题,而检索的上限决定了最终答案的上限。
核心机制
解析:第一次信息损失发生在切分之前
PDF 的双栏、页眉页脚、合并单元格表格、扫描件,在转成纯文本时就会丢结构。正确做法是先解析成结构化元素(标题层级、段落、表格、代码块、列表),保留 source、section_path、页码或锚点,再进入切分。表格优先转 Markdown 或键值对,代码块整体保留。
这一步决定了后面所有环节的天花板:解析阶段丢掉的表头,切分和 embedding 都补不回来。判断标准很朴素——随机抽 20 个解析结果,能否只看文本还原出原文的章节关系。
切分:结构优先,长度兜底
切分在权衡两个互斥目标:块小,向量语义集中、检索准,但答案可能被切断;块大,上下文完整,但无关内容稀释相似度。所以顺序是先用文档结构(标题、段落、表格边界)切,再用长度兜底,而不是反过来。
常见五种做法:固定窗口(简单、无结构语料可用)、结构感知(按标题与元素边界)、语义切分(用模型判断断点,准但预处理成本高)、父子块(子块检索、父块生成)、上下文增强(入库前给块拼上文档标题、章节路径或一句摘要)。父子块解决的是"检索要小、生成要大"的矛盾;上下文增强解决的是"块离开原文后指代不清"。
重叠不是免费收益:10%–15% 的重叠能覆盖大部分跨界短答案,但存储与 embedding 成本同比例上升;先换切分策略,再考虑加重叠。
索引与混合检索:语义与精确各管一段
写入与查询必须用同一个 embedding 模型、同一版本、同一归一化方式;换模型等于换向量空间,只能全量重建,用新 collection 加别名切换,不要原地混写。向量索引(如 HNSW)用 M、efConstruction、ef 在召回与延迟之间调参,数据量变化后要重新验证 Recall。
纯向量擅长改述,弱在精确串:版本号、错误码、产品名、内部代号。BM25 正好相反。生产默认两路各召回 30–50 条,用 RRF 按排名融合,而不是把余弦分数和 BM25 分数线性加权——两者尺度不同,归一化本身就会引入偏差。
这条链路里,解析和切分决定"能不能被找到",混合检索决定"改述和精确串都不漏",RRF 只解决融合、不提升单路质量。任何一环的召回损失都会向后传导,所以优化顺序必须从上游开始。
元数据、权限与新鲜度
元数据在入库时就要写全:来源、章节、更新时间、文档版本、租户、ACL 标签、内容类型,以及便于复现的内容哈希。它承担三件向量做不到的事——精确过滤(只查某版本 / 某租户)、引用溯源、权限隔离。
权限必须在检索前过滤,不能先召回再在 Prompt 里删:后置过滤既漏信息,又可能把无权限内容送进模型上下文。先确定身份与作用域,再检索;"先跨租户召回、再提醒模型别泄漏"已经晚了。
知识是资产,不是一次性索引:每条知识要有作用域、版本和删除语义。源文档更新时,用稳定主键做 upsert;删除要沿派生链清理原文、摘要、索引与缓存,而不是只删一条向量。embedding 或切分策略变更走"新索引 + 别名切换",避免新旧向量混在一个空间。
版本与删除语义决定了知识库会不会“越用越脏”:没有墓碑,删掉的文档仍会被召回;没有别名切换,新旧向量混用会让相似度失去意义。权限放在召回之前,则是因为内容一旦进入模型输入,提示词层面的“不要泄漏”只是补救,不是隔离。
实战场景
场景:换上更大的生成模型错误率不降——切分把表格切成了条件与数值两半
现象:保险条款问答在“赔付上限”“免赔额”这类问题上错答集中,端到端错误率 9.6%。团队的判断是模型不够强,换成更大的生成模型跑了两周,错误率只到 9.1%(经验值,需校准),推理成本涨了一倍,错答的还是同一批题。
排查过程:先量检索再谈生成。对失败 query 逐层打印:金标文档在 Recall@50 里,但在 Recall@20 中排名靠后;nDCG@10 尚可,排序不是主要问题;把被引块与结论逐句核对,发现引用真实存在、块里只有金额没有条件——失败模式是“用了但用不全”,指向组装与上游切分,而不是模型。回到解析做抽检:随机抽 20 个解析结果,只看文本能否还原原文的章节关系,三成还原不了;合并单元格的表头在转纯文本时丢掉,表格里“重疾赔付上限”那一行被切成两块,条件落在上一块、数值落在下一块,向量命中的是数值那半句。
对症做法:按调优顺序从上游动手,一轮只改一项,用同一套黄金集配对检验。
- 解析:表格优先转 Markdown 或键值对,代码块整体保留,
source、section_path、页码或锚点全带上。解析阶段丢掉的表头,切分和 embedding 都补不回来。 - 切分:先用文档结构(标题、段落、表格边界)切,长度兜底,不是反过来;合同与规范类块更小且按条款切;表格用父子块——子块检索、父块生成,把“检索要小、生成要大”的矛盾交给结构解决。
- 上下文增强:入库前给块拼上文档标题与章节路径,解决块离开原文后指代不清的问题——“本表第 3 行”脱离表格就没有意义。
- 回归:黄金集 100–300 条,按简单事实、跨段/多跳、无答案三类分层;重测 Recall@20 与 nDCG@10,再看 faithfulness(忠实度)与引用支持率。Recall@20 低于 0.8(经验值)时不动生成侧。
复盘要点:检索的上限决定最终答案的上限,模型只在上下限之间起作用。块太小会把结论切成条件与答案两半,检索命中却答不完整,父子块和上下文增强就是为此存在。这一轮如果先去换更大的模型,等于用算力掩盖切分缺陷:成本翻倍、错误率不动,还把真正的原因留在线上。
场景:混合检索线性加权调了一周,指标涨了投诉更多
现象:内部技术文档问答把稠密与稀疏两路的分数归一化后线性加权,权重从 0.5 调到 0.75,nDCG@10 涨 3 个点,周会按“优化有效”上报;同期追问率与转人工率反升,投诉集中在“错误码查不到”“版本号答错”这类精确串问题。
排查过程:把失败按 query 类型分桶,而不是看总分。改述类命中正常,精确串类(错误码、型号、内部代号)大量召回为空。查日志里记录的 query、改写后 query、召回 ID 与分数、融合排名:错误码在 BM25 一路排第 2,融合后掉到 60 名开外——线性加权把单路的强精确命中压掉了,而余弦分数与 BM25 分数尺度不同,归一化本身就引入偏差。指标上涨是因为被加权抬高的一路在多数改述题上更稳,代价是精确串整类失效;用户投诉测的正是这一类。
对症做法:
- 融合改回 RRF(k=60)按排名融合,两路各召回 30–50 条,不做分数线性加权。RRF 只解决融合,不提升单路质量,稀疏与稠密各管一段才是收益来源。
- 精确串交给 BM25 兜底,改述交给向量兜底;查询改写只在“短查询 / 低分召回”条件下触发,且保留原始 query 一路走到底,避免改写偏离原意后整条链路一起错。
- 双看板拆开:检索侧看 Recall@k、nDCG、零结果率、检索 P95;生成侧看 faithfulness、引用支持率、拒答率、答案相关性;不用一个端到端满意度代替两者。
- 在线加影子指标:零结果率、引用点击率、追问率、转人工率、点踩原因分布。精确串单独成桶,任何融合改动都要证明这一桶不退化才放行。
复盘要点:公开指标与影子指标背离时,优先怀疑指标被优化而不是系统变好。拍脑袋调权重之所以危险,是它能一边涨总分一边砍掉一整类查询;把“准确率”当成一个数,就永远不知道该修哪一层。
场景:重排对全量候选打分,检索 P95 翻倍——漏斗层级放错了
现象:为了把排序做扎实,重排的输入从 50 条扩到 300 条,上线当天检索链路 P95 从 0.8s 涨到 2.6s(经验值,需校准),超时降级次数翻倍,nDCG@10 只涨 0.6 个点——提升落在噪声区间,成本是每次查询多出两百次前向传播。
排查过程:延迟按级拆开实测,不猜整条链路。对照各级预算:改写 100–300ms、embedding 20–50ms(本地)、BM25 10–30ms、ANN 10–50ms 全部正常,超出预算的只有重排一段(预算 100–300ms)。同时跑离线归因脚本,失败 query 里金标未进前 50 的占三成(经验值,需校准)——这些题的相关块根本没被召回,候选扩到 300 条对它们完全无效;重排只治“已召回但排序不对”,给没召回到东西的候选排序是空转。
对症做法:两级漏斗结构固定,每一级都能单独关闭、单独评估。
python
cands = rrf([dense[:50], bm25[:50]], k=60) # 召回宽:bi-encoder,毫秒级
ranked = rerank(q, cands[:50], batch=True, quant="int8") # 精排严:候选数线性决定成本
ctx = ranked[:6] # 3–6 条进上下文,受注意力预算约束- 候选回到 30–50 进、5–10 出、3–6 条进上下文;批量推理、INT8 量化、限制候选长度是三件主要优化,先做完再谈加候选。
- LLM 重排只对高价值、低 QPS 的查询按路由开启,不进全量路径——它的延迟与成本高一个量级。
- 超时按“跳过改写 → 跳过重排 → 只返回检索结果”逐级降级,而不是整条链路一起失败。
- 那三成召回问题按归因回上游:解析与数据治理、切分与父子块、混合检索与查询改写,这个顺序不能反。
复盘要点:漏斗的存在理由就是成本随候选数线性涨——cross-encoder 每个候选一次前向传播,bi-encoder 才能预先建索引、毫秒级扫百万条。用 300 条候选换 0.6 个点,是在错误的层要收益:真正卡住准确率的是金标没进候选,不是候选排错了。
最小上手
先跑一条最小链路,不接重排(示意):
python
docs = parse_dir("kb/") # 保留 section_path / page
chunks = structure_split(docs, max_tokens=600, overlap=80)
for c in chunks:
c.metadata.update(tenant="t1", acl=["staff"], doc_version=c.version,
updated_at=c.mtime, content_hash=sha256(c.text))
c.vector = embed(c.text_with_section_prefix) # 写入与查询同一模型
index.upsert(chunks, id=stable_id(c))
def answer(q, tenant):
vecs = embed(q)
dense = index.search(vecs, k=40, filter={"tenant": tenant})
sparse = bm25.search(q, k=40, filter={"tenant": tenant})
hits = rrf([dense, sparse], k=60)[:6]
return llm(system="只依据资料回答;不足则说不知道;每句标注 [n]",
context=number(hits), question=q)跑通判据:准备 50–100 条"问题 + 应命中的文档 ID",先测 Recall@5 与 Recall@20。Recall@20 低于 0.8(经验值)时不要调生成,先回去查解析、切分和混合检索。
生产动作
- 先建评测集再调参:100–300 条,按简单事实、跨段/多跳、无答案三类分层;无答案样本必须占一部分,否则系统学不会拒答。
- 基线指标:Recall@20 目标 > 0.9(经验值,给重排留余量)、nDCG@10 看排序质量、context precision 看注入的块有多少真相关;生成侧再看 faithfulness 与引用支持率。
- 参数起点:中文技术文档 300–800 token、重叠 10%–15%(经验值);FAQ 更小,合同与规范更小且按条款切;不要迷信 512。
- 召回配置:dense 与 BM25 各 30–50 条 → RRF(k=60)→ 重排 20 条 → 3–6 条进上下文;块数受注意力预算约束,不是越多越好。
- 元数据完整性:入库时校验必填字段,缺
tenant或acl直接拒绝写入;权限过滤在检索器封装层统一注入,避免调用方漏写。 - 冲突不静默覆盖:同一事实有多个版本时,按来源权威与时效保留,不要后写的自动盖掉先写的;检索时按权威分层,并允许答案明确说"两处说法不一致"。
- 新鲜度:监控"源更新 → 索引可见"的 P95 时延;删除必须传播,定期用已删除文档的标题做探针查询,应返回空。
- 日志:记录 query、改写后 query、召回 ID 与分数、融合排名、最终引用 ID。排障时先看"金标文档在不在召回里",再看生成。
- 证据充分性:检索为空或最高相似度过低时,输出"资料不足"而不是让模型自由发挥;答案正确不等于证据充分,引用必须能回链到具体块。
边界
- 适合事实型问答、制度/文档助手、需要引用与权限控制的场景;不适合以风格、格式、术语习惯为主的诉求,那属于微调;也不适合强关系推理与全库总结,那是 GraphRAG 的领域。
- 长上下文不能替代 RAG:塞得进不等于用得到,中间位置的信息利用率会下降;而且长上下文没有文档级权限,也无法证明答案出处。
- 向量相似不等于相关:embedding 只保证"语义近",不保证"回答了问题";精确串必须靠 BM25 兜底。
- 切分不是越细越好:块太小会把结论切成条件与答案两半,检索命中却答不完整;父子块和上下文增强就是为此存在。
- RAG 不修复坏文档:源文档过期、互相矛盾、权限标注错误,检索只会更自信地引用错误;数据治理优先于链路调优。
- 常见误区:一上来换向量库。多数收益来自解析、切分、混合检索与元数据,向量库通常排在最后。
框架层的组件抽象见 RAG 与文档检索;把检索接成工具的做法见 工具链与 MCP 平台。
讲清楚
RAG 是把知识从模型权重里搬到外部索引:离线解析文档、按结构切分、写入向量与元数据;在线改写查询、混合召回、融合、组装上下文再生成。它解决的是知识新鲜度、私有数据和可引用,代价是检索链路的每一环都会损失召回,损失会向后传导。顺序是先保证解析和切分不丢信息,再用 dense 加 BM25 覆盖改述与精确串,最后才谈重排和生成。元数据入库时写全,权限在检索前过滤,删除要沿派生链清理。没有评测集,调参全靠感觉;先量 Recall@k,再谈答案质量。
适用:2026 年 RAG 基础形态为结构感知切分 + 混合检索 + 引用约束 | 初稿 2026-10
下篇 · 进阶调优与失败归因
为什么需要
基础 RAG 跑到某个点会平台化:Recall 看着还行,用户仍说"答得不对"。团队的第一反应通常是加重排,但重排只能重排已经召回的候选——如果三成查询根本没召回到相关块,重排的收益接近零。另一种常见误判是把"引用错"当成"检索错",实际是生成阶段把无关块硬套上了答案。
所以进阶的第一步不是选技术,而是把"准确率"拆开:是没找到、找到了没用、用了但用错,还是多源冲突没处理。这四类失败的修法完全不同,混在一起就只能靠试。
核心机制
两级漏斗:召回宽、精排严,成本随候选数线性涨
向量检索是 bi-encoder:查询和文档各自编码,只在中点积时相遇,所以能预先建索引、毫秒级扫百万条,但只能判断"整体像不像"。重排是 cross-encoder:查询与候选拼在一起过完整 Transformer,每个 token 都能互相关注,判断"这段是否真的回答了问题",代价是每个候选一次前向传播。
因此结构固定为两级:bi-encoder 召回 30–50 条,cross-encoder 精排到 5–10 条。候选数翻倍,重排延迟大致翻倍;批量推理、INT8 量化、限制候选长度是三个主要优化点。LLM 重排更灵活(可给理由、可做列表级比较),但延迟与成本高一个量级,适合高价值、低 QPS 的查询,不适合全量。
上下文压缩是重排后的第二道:去掉模板化段落、重复块、与问题无关的句子,只保留能支撑答案的片段和引用锚点。风险是压掉限定条件("除……外""仅限……"),所以压缩前要先把条件、例外、权限这类关键事实提交到结构化状态,再压叙述性历史;压缩后保留原始块 ID,并抽检被删句子是否含否定或例外。
这条漏斗的每一级都在用算力换精度:召回用近似索引换吞吐,重排用全交互换判别力,压缩用额外一次模型调用换上下文密度。级数越多,延迟预算越紧,所以每一级都必须能被单独关闭和单独评估。
结构化与智能体:GraphRAG 与 Agentic RAG 各解一类问题
GraphRAG 解决的是"索引结构不够":从文档抽实体与关系建图,做社区检测并为每个社区生成摘要。它擅长全局问题("这套系统有哪些模块、彼此怎么影响")和多跳关系("这次故障影响哪些下游"),因为答案不在某一篇文档里,而在文档之间的关系里。代价是建图与维护成本高一个量级,抽取错误会沿图传播,增量更新也比向量索引麻烦。
Agentic RAG 解决的是"控制流不够":把检索做成工具,由 Agent 决定查什么、查几次、要不要换数据源。Router 模式按问题类型分流,ReAct 模式迭代检索,Self-RAG / Corrective RAG 在检索后判断相关性、不够则改写或降级到其他来源。它灵活,但延迟和成本不可预测,必须设步数上限、token 预算和"信息足够"的停止条件。
两者不是替代关系:GraphRAG 改的是索引,Agentic 改的是控制流,可以叠加;但都应该在基础链路已经调优、且失败类型明确指向它们时才引入。
失败模式:症状、责任层与处置
| 症状 | 更可能的层 | 先查什么 | 处置 |
|---|---|---|---|
| 召回为空 / 答"不知道" | 检索 | 金标文档是否在 Recall@k 内 | 切分、embedding、混合检索、查询改写 |
| 答非所问 | 查询理解 / 生成 | 改写后 query 是否偏离原意 | 保留原始 query 一路、收敛改写触发条件 |
| 引用与结论不符 | 生成 / 引用 | 被引块是否真的支持该句 | 逐句引用、引用校验、降低上下文噪声 |
| 多源说法冲突 | 组装 / 数据 | 来源权威性与时间 | 权威分级、时效优先、显式要求指出冲突 |
| 反复检索不收敛 | 智能体 | 步数与停止条件 | 步数上限、充分性判断、快速通道 |
这张表的用法是"先定位层,再选工具":召回为空时加重排是无效动作;引用错时换 embedding 也是无效动作。把每次失败标上层,周度看分布,才知道下一个优化该花在哪。
多租户、缓存与延迟预算
权限过滤必须在检索前完成:按 tenant 分区或强制 filter,在检索器封装层统一注入。测试要用负向用例——无权限用户查询敏感文档标题,必须返回空,而不是"召回了但没引用"。
缓存要带版本:key 至少包含归一化 query、tenant、ACL 版本、索引版本、Prompt 版本。只按 query 缓存,会在文档更新或权限变更后返回旧答案。语义缓存能提高命中率,但相似不等于等价,阈值要保守,且高敏感场景只用精确缓存。
延迟预算按级分配(经验值):改写 100–300ms、embedding 20–50ms(本地)/ 100–200ms(API)、BM25 10–30ms、ANN 10–50ms、重排 100–300ms、生成首 token 0.8–2s。超时按"跳过改写 → 跳过重排 → 只返回检索结果"逐级降级,而不是整条链路一起失败。
最小上手
做一个离线归因脚本,对失败 query 逐层打印(示意):
python
for case in golden_failures:
hits = retrieve(case.q, k=50)
print("gold_in_top50:", case.gold_id in ids(hits))
print("gold_rank:", rank_of(case.gold_id, hits))
reranked = rerank(case.q, hits)[:6]
print("after_rerank:", rank_of(case.gold_id, reranked))
ans = generate(case.q, reranked)
print("cited:", ans.citations, "supported:", support(ans, reranked))
print("answer_relevant:", judge_relevance(case.q, ans))跑通判据:随机抽 30 条线上差评,能明确归到"召回 / 重排 / 生成 / 冲突"四类中的一类,且同类占比最高的一类与你下一步要做的优化一致。归不了类,说明 trace 或评测字段还不够。
生产动作
归因决定动作,动作再回到同一套题上验证,这个顺序不能反:先改再找原因,等于同时移动多个变量,分数变化无法解释。只有归因稳定、单变量实验显著,灰度才有意义。
调优顺序(每一步都跑同一套黄金集,配对检验显著且护栏不退化才进入下一步):
- 解析与数据治理:坏 PDF、重复文档、过期版本先清掉。
- 切分与父子块:看 Recall@k 的拐点,块太大先降,块太小先升。
- 混合检索与查询改写:先开 BM25,再按"短查询 / 低分召回"条件触发改写。
- 重排:候选 30–50 进、5–10 出;先批量与量化,再考虑换更大的模型。
- 上下文组装:去重、按相关性排序、控制块数与位置,处理 Lost in the Middle。
- 生成约束与引用:从"请根据资料"改成"只依据资料,不足则拒答",要求逐句引用。
- GraphRAG / Agentic:只有失败分布明确指向多跳或控制流时才上,并设成本上限。
- 双看板:检索看 Recall@k、nDCG、零结果率、检索 P95;生成看 faithfulness、引用支持率、拒答率、答案相关性。不要用一个端到端满意度代替两者。
- 四状态分开记:执行是否完成、检索是否有效、证据是否完整、任务质量如何,是四个独立结论。压成一个成功,会让"跑完了但证据不全"被当成通过。
- 证据分层:原始记录(回答、轨迹、引用)、事实观测(命中/未命中、引用是否回链)、判断(是否有支持)要分开存。判断不能写回原始记录,否则下一轮评分会被上一轮结论污染,评的就不再是同一件事。
- 在线代理指标:零结果率、引用点击率、追问率、转人工率、点踩原因分布。公开指标与这些影子指标背离时,优先怀疑指标被优化而不是系统变好。
- 缓存治理:命中率、过期导致的错答数、语义缓存误命中抽检;文档更新时按索引版本批量失效。
- 实验纪律:基线先跑,第一轮只改一项;被评分的必须是本次新生成的回答与新轨迹;先看退化题,再看改善题;采用至少看三件事——严重错误是否消失、正常场景是否退化、耗时与消耗是否可接受。
- 独立验证:参与调优的题与独立验证的题分开,发布前用未参与修改的样本再跑一遍。只收集低分题会看不见正常能力退化,只收集常见题会漏掉代价高的少数。
边界
- 重排适合"已召回但排序不对";不适合召回为空——先补召回,否则只是给空集排序。
- GraphRAG 适合多跳、全局总结、实体关系密集的语料;不适合千篇以内、以单跳事实为主的库,建图成本收不回来。
- Agentic RAG 适合问题开放、需要多源交叉的场景;不适合强延迟约束或成本敏感的高 QPS 入口,简单问题必须走快速通道。
- 上下文压缩适合噪声多、模板化严重的文档;不适合条款、法规、医疗说明——删掉一个限定词就可能改变结论。
- 缓存的边界是权限与时效:key 不含 tenant / ACL / 索引版本时,缓存会变成越权与过期答案的放大器。
- 常见误区:用端到端满意度评价单个改动(噪声淹没信号);用同一个 Judge 同时评检索与生成(同源偏差);把"准确率"当一个数,导致永远不知道该修哪一层。
讲清楚
进阶 RAG 先归因,不是先加组件:错在没召回、召回没用上、引用错,还是多源冲突。重排只在已召回但排错时有效,成本随候选数线性涨;压缩提高上下文密度,但可能删掉限定条件;GraphRAG 把关系搬进索引,解多跳与全局问题;Agentic RAG 把控制流交给模型,解开放问题,但要设步数与预算。权限在检索前过滤,缓存 key 必须带租户、权限与索引版本。调优按解析、切分、混合检索、重排、组装、生成的顺序走,每步用同一套黄金集做配对检验,并分开看检索指标和生成指标。
适用:2026 年 RAG 进阶以重排 + 压缩为主、GraphRAG 与 Agentic 按失败类型选用 | 初稿 2026-10