
系列:生产级 LLM 应用方法论 07
日期:2026-06-21
适合读者:正在建设企业知识库问答、客服助手、内部搜索、研究助理或 Agent 检索层的工程师、产品负责人和技术负责人。
摘要
第六篇讲了知识库摄取与 chunking:把原始文档处理成可检索、可引用、可过滤、可审计的证据单元。
这一篇解决下一个问题:证据单元已经入库了,怎样把正确证据稳定排到模型面前?
很多 RAG 系统的第一个版本都是“向量检索 top-k + prompt”。这个方案适合 demo,但很快会在真实场景里暴露问题:用户问错误码、合同条款、产品型号、表格字段、专有名词时,向量相似度未必可靠;用户问概念性问题、同义改写、跨文档总结时,纯关键词又太僵硬;候选召回到了以后,如果没有重排,前 5 条经常被相似但不支持答案的 chunk 占掉。
生产级 RAG 的检索层不应该被理解成一个向量库调用,而应该被理解成一条排序流水线:
Query Understanding
-> Metadata Filter
-> Sparse Retrieval + Dense Retrieval
-> Hybrid Fusion
-> Candidate Pool
-> Reranker
-> Diversity and Policy Filters
-> Context Packing
-> Eval and Fix Loop
本文给出一套可落地的混合检索与重排方法,包含 sparse 与 dense 各自负责什么、如何做 RRF 融合、候选池取多大、reranker 放在哪里、怎样处理低置信度、如何按查询类型评测,以及一个最小 Python 排序管线示例。
目录
- 1. 不要把 RAG 检索理解成向量 top-k
- 2. 一条生产检索链路长什么样
- 3. Sparse Retrieval:关键词不是落后方案
- 4. Dense Retrieval:语义召回解决改写和概念匹配
- 5. Hybrid Fusion:把不同检索器变成一个候选池
- 6. Candidate Pool:重排前要先给足候选
- 7. Reranker:把“像”变成“能支持答案”
- 8. Query Rewrite 与 Query Decomposition
- 9. 多样性、权限、版本和新鲜度
- 10. 分数阈值与低置信度策略
- 11. 一个最小混合检索示例
- 12. 如何评测混合检索是否真的更好
- 13. 生产架构建议
- 14. 常见误区
- 15. 落地路线图
- 16. 发布前自检清单
- 17. 总结
- 参考资料
1. 不要把 RAG 检索理解成向量 top-k
向量检索很有用,但它不是完整的检索系统。
假设用户问:
订单命中 E7782 时是否还能自动退款?
这个问题里有一个很强的关键词信号:E7782。如果知识库里有一段:
错误码 E7782 表示支付渠道返回风险拦截。命中该错误码后,退款申请必须进入人工审核,不得自动退款。
纯语义向量检索不一定把它排第一。因为向量模型可能更重视“自动退款”“人工审核”这些语义相近的段落,而不是错误码本身。这个时候 BM25、字段匹配、短语匹配和精确匹配非常重要。
再看另一个问题:
客户说到账时间太久,客服应该怎么解释跨境退款?
这里用户没有直接说“跨境支付订单需人工审核”“1-5 个工作日”“原支付渠道”,但语义上明显在问这些政策。纯关键词检索可能错过,向量检索更适合。
生产 RAG 的关键不是在关键词和向量之间二选一,而是承认它们解决的问题不同。
| 检索方式 | 擅长 | 不擅长 |
|---|---|---|
| 关键词 / BM25 / sparse | 错误码、产品型号、合同编号、专有名词、原文短语、字段名 | 同义改写、概念性问题、跨句语义 |
| 向量 / dense | 同义表达、意图匹配、概念相关、自然语言问题 | 精确符号、短字符串、罕见名词、最新术语 |
| 元数据过滤 | 权限、租户、版本、语言、时间、文档类型 | 判断内容是否真正回答问题 |
| reranker | 细粒度相关性、证据是否支持答案、排序纠偏 | 处理全库召回,成本和延迟较高 |
所以检索层的目标不是“取 top-k”,而是:从全量知识库中构造一个足够全、足够准、可解释、可审计的证据候选集合,再把最能支持答案的证据放进上下文。
2. 一条生产检索链路长什么样

一条更可靠的 RAG 检索链路通常包含这些阶段。
| 阶段 | 目标 | 关键问题 |
|---|---|---|
| Query Understanding | 理解用户意图、实体、时间、约束 | 用户在问规则、事实、流程、对比还是总结? |
| Metadata Filter | 先收紧可访问范围 | 租户、权限、语言、版本、状态是否正确? |
| Sparse Retrieval | 召回精确词面匹配 | 错误码、术语、标题、字段名能否命中? |
| Dense Retrieval | 召回语义相似内容 | 同义表达和概念相关内容能否命中? |
| Hybrid Fusion | 合并不同检索器结果 | 不同分数尺度如何融合?重复 chunk 如何处理? |
| Candidate Pool | 给 reranker 足够候选 | 召回池是否足够大但不失控? |
| Reranker | 重新判断候选是否真正回答问题 | 证据和问题之间是否有直接支持关系? |
| Diversity Filter | 避免同源重复挤占上下文 | 是否需要来源上限、MMR、相邻 chunk 补全? |
| Context Packing | 把证据组织给生成模型 | 顺序、引用、预算、冲突证据怎么处理? |
| Eval Loop | 发现失败模式并修复 | 哪类问题掉召回、掉排序、掉引用? |
这里最容易被忽略的是两个事实。
第一,metadata filter 不是可选项。企业 RAG 里,权限和版本应该在检索前参与过滤,而不是把不该看的证据召回后交给模型“自觉不要用”。
第二,reranker 不是召回器。reranker 更擅长在几十条候选里做精排,不适合直接面对百万级语料。召回阶段如果没有把正确证据放进候选池,reranker 再强也救不了。
3. Sparse Retrieval:关键词不是落后方案
Sparse retrieval 通常指 BM25、倒排索引、短语匹配、字段加权、词项扩展等关键词检索方法。它看起来不如向量检索“智能”,但在很多生产问题里反而是最可靠的底座。
3.1 什么时候必须保留关键词检索
这些查询强烈依赖词面匹配:
- 错误码:
E7782、HTTP 429、ORA-00001 - 产品型号:
XG-2400、Pro Max、SKU-9812 - 字段名:
refund_status、effective_date - 合同条款:
第 6.2 条、force majeure - 法规编号:
GDPR Article 15、SOX 404 - 内部项目代号:
Aurora、Bridge-v2 - 人名、地名、机构名、报告标题
这些词的语义空间未必稳定。一个错误码可能没有自然语言含义,一个 SKU 可能只是一串字符。向量检索如果没有足够训练语境,很难保证精确召回。
3.2 关键词检索不要只用正文
上一篇文章强调 chunk metadata。这里它会直接影响关键词召回质量。
推荐把这些字段纳入 sparse index,并设置不同权重:
| 字段 | 作用 | 建议 |
|---|---|---|
source_title |
文档级主题 | 高权重 |
hierarchy_path |
章节路径 | 高权重 |
text |
证据正文 | 标准权重 |
keywords |
人工或自动抽取术语 | 中高权重 |
product_line |
产品线 | 用于过滤或加权 |
version |
版本 | 用于过滤,不建议只靠排序 |
status |
active/deprecated | 用于过滤 |
如果只把 chunk 正文放进索引,很多短 chunk 会失去上下文。例如正文只有:
不得自动退款。
但它的标题路径是:
退款政策 > 风险拦截 > E7782 处理
检索时标题路径比正文更关键。
3.3 Sparse 检索的工程细节
生产里建议至少支持:
- 精确短语匹配:适合错误码、条款号、标题;
- 字段加权:标题和章节路径权重大于正文;
- analyzer 策略:中文分词、英文大小写归一、符号保留;
- 同义词表:业务别名、旧称、新称;
- 停用词控制:不要把“退款”“订单”这类领域高频词全部丢掉;
- query boost:当识别出错误码、SKU、字段名时,提高 sparse 权重。
关键词检索不是“传统方案”,它是 RAG 对现实世界符号系统的尊重。
4. Dense Retrieval:语义召回解决改写和概念匹配
Dense retrieval 使用 embedding 把 query 和 chunk 映射到向量空间,再按相似度召回。它的价值在于处理用户不会逐字复述文档的情况。
例如知识库里写的是:
跨境支付订单需进入人工审核。审核完成后,退款通常在 1-5 个工作日内退回原支付渠道。
用户可能会问:
海外订单退款为什么不能马上到账?
这时没有明显的错误码或条款号,dense retrieval 更容易召回正确证据。
4.1 Dense 检索依赖 chunk 质量
如果 chunk 太长,embedding 会把多个主题混在一起;如果 chunk 太短,语义不完整。向量召回的质量不是只由 embedding model 决定,还由这些因素共同决定:
- chunk 是否语义完整;
- 标题路径是否进入索引文本;
- 表格是否被转成可理解的文本;
- 旧版本是否被过滤;
- 同一文档是否重复入库;
- 用户 query 是否需要改写或拆解。
这也是为什么第六篇先讲摄取和 chunking。没有好的证据单元,检索算法只能在噪声里排序。
4.2 Dense 检索的参数起点
可以从下面的配置起步,再用评测集调整。
| 场景 | dense top-n | 说明 |
|---|---|---|
| 小型 FAQ / 文档少 | 20-40 | 候选充足即可 |
| 中型企业知识库 | 50-100 | 给融合和重排留空间 |
| 多租户 / 多产品线 | 每个过滤范围 30-80 | 先过滤再召回 |
| 长文档研究助理 | 80-150 | 需要更强去重和多样性控制 |
不要一开始就把 top_k=5 作为检索结果直接喂给模型。top-k 是最终上下文数量,不应该等于召回数量。召回阶段要更宽,精排和上下文阶段再收紧。
5. Hybrid Fusion:把不同检索器变成一个候选池
Hybrid search 的基本思路是:同一个 query 同时跑 sparse 和 dense,再把两个结果列表融合成一个候选池。
困难点在于,BM25 分数和向量相似度不是同一个尺度。BM25 可能是 12.7,cosine similarity 可能是 0.83。直接相加通常没有意义。
5.1 推荐从 RRF 开始
一个实用起点是 Reciprocal Rank Fusion,简称 RRF。它不直接比较原始分数,而是比较排名位置。
直觉很简单:
- 同时在 sparse 和 dense 里排名靠前的 chunk,应该更靠前;
- 只在一个通道里排名很靠前的 chunk,也应该保留;
- 排名越靠后,贡献越小。
常见形式:
score(doc) = sum( weight_i / (k + rank_i(doc)) )
其中 rank_i 是某个检索器里的排名,k 是平滑常数,常用 60 左右。权重可以按 query 类型调整。
5.2 权重不应该固定不变
同一个系统里,不同 query 应该有不同策略。
| Query 类型 | sparse 权重 | dense 权重 | 原因 |
|---|---|---|---|
| 错误码 / SKU / 条款号 | 高 | 中 | 精确符号优先 |
| FAQ 自然语言问题 | 中 | 高 | 用户表达经常改写 |
| 标题或文档名搜索 | 高 | 低中 | 标题词面很强 |
| 概念解释 / 原理总结 | 中 | 高 | 语义相关更重要 |
| 多条件政策问题 | 中高 | 中高 | 两者都需要 |
OpenAI Retrieval 文档中也提供了混合检索相关的 ranking 配置:可以通过 ranking_options.hybrid_search 调整 embedding 语义匹配和 sparse 文本匹配的权重,并通过 score_threshold 控制返回结果的最低分数。具体产品参数会变化,设计上要把这些当成可评测、可回滚的检索策略,而不是一次性写死。
5.3 融合前后都要去重
同一个证据可能以多个形式出现:
- 同一 source 的相邻 chunk;
- 同一文档不同版本;
- FAQ 和政策正文重复;
- 多语言翻译内容重复;
- 摘要页和原文页重复。
融合后必须按 chunk_id、content_hash、source_id + locator 等字段去重。否则前 10 条看似很多,实际上全是同一段话的重复版本。
6. Candidate Pool:重排前要先给足候选
reranker 的前提是正确证据已经在候选池里。候选池太小,是很多 RAG 排序失败的根因。
一个稳妥的起点:
sparse top 80
dense top 80
metadata filter before retrieval
RRF fusion to top 80
dedupe and source cap to top 60
rerank top 60
context pack top 6-12 chunks
数字不是标准答案,但体现了一个原则:召回宽,重排窄,上下文更窄。
6.1 为什么不能只 rerank top 10
如果 sparse 和 dense 初排都不完美,正确证据可能排在第 18、33 或 47。只 rerank top 10,会让 reranker 看不到正确答案。
在延迟允许的情况下,建议至少 rerank 30-80 条。对于高价值、低频、长答案问题,可以 rerank 100 条以上;对于客服实时场景,可以用缓存、轻量 reranker 或分层 rerank 控制延迟。
6.2 候选池要保留解释字段
每个候选不仅要有文本,还要保留排序证据:
{
"chunk_id": "refund_policy#sec_4#chunk_02",
"source_id": "refund_policy_2026_03",
"sparse_rank": 3,
"dense_rank": 18,
"fusion_score": 0.0271,
"rerank_score": 0.84,
"matched_terms": ["E7782", "自动退款"],
"metadata": {
"version": "2026-03",
"status": "active",
"access_scope": ["customer_service"]
}
}
这些字段是调试和评测的生命线。线上出现错答时,如果日志里只有最终 prompt,很难知道是没召回、排错了,还是生成模型没用证据。
7. Reranker:把“像”变成“能支持答案”
初始召回判断的是“这个 chunk 和 query 是否相似”。reranker 应该更进一步:判断“这个 chunk 是否能直接支持回答这个 query”。
例如用户问:
命中 E7782 后能否自动退款?
候选 A:
自动退款适用于低风险订单,系统会在审核通过后按原支付渠道退款。
候选 B:
错误码 E7782 表示支付渠道返回风险拦截。命中该错误码后,退款申请必须进入人工审核,不得自动退款。
A 和 query 都包含“自动退款”,相似度可能很高。但 B 才是真正支持答案的证据。reranker 的价值就在这里。
7.1 常见 reranker 类型
| 类型 | 思路 | 优点 | 代价 |
|---|---|---|---|
| Cross-encoder reranker | 同时输入 query 和 chunk,输出相关性分数 | 精度强,适合精排 | 每个候选都要跑模型 |
| LLM reranker | 让 LLM 判断候选是否支持答案 | 可解释,能处理复杂条件 | 成本和延迟高,要防格式漂移 |
| Late interaction | 例如 ColBERT 风格的 token-level 交互 | 兼顾召回和细粒度匹配 | 系统实现更复杂 |
| 规则 reranker | 用实体、时间、版本、标题等规则加权 | 便宜、稳定、可控 | 覆盖不了复杂语义 |
工程上常见组合是:
RRF fusion -> rule boosts -> neural reranker -> final policy filters
规则不是为了替代模型,而是把确定性约束先处理掉。例如:
- active 版本优先;
- 用户所在产品线优先;
- 标题精确命中加权;
- 旧版本降权或过滤;
- 同一来源最多保留 2-3 条;
- 表格证据必须带行列标题。
7.2 reranker 的输出不要只存一个分数
如果使用 LLM 或可解释 reranker,建议输出结构化字段:
{
"chunk_id": "refund_policy#sec_4#chunk_02",
"relevance": 0.92,
"support": "direct",
"reason": "The chunk explicitly states that E7782 requires manual review and automatic refund is not allowed.",
"missing": []
}
其中 support 可以分成:
direct:直接回答;partial:回答一部分;background:只是背景;conflicting:与其他证据冲突;irrelevant:无关。
最终进上下文的应该优先是 direct 和高质量 partial,而不是所有“看起来相关”的材料。
8. Query Rewrite 与 Query Decomposition
用户 query 往往不是最适合检索的形式。检索前可以做 query rewrite,但要控制边界。
8.1 Query Rewrite 适合什么
适合:
- 口语化问题改写成标准术语;
- 省略上下文补全;
- 中英文术语映射;
- 拼写纠错;
- 把长问题压成检索关键词;
- 生成同义 query 做多路召回。
例如:
用户问题:海外订单退款为什么慢?
检索 query 1:跨境支付订单 退款 人工审核 到账时间
检索 query 2:海外订单 退款 1-5 个工作日 原支付渠道
检索 query 3:跨境退款 审核完成后多久到账
OpenAI Retrieval 文档中提供了 rewrite_query 一类能力,用于在检索前改写 query。无论使用托管能力还是自建能力,都要把 rewrite 结果记录到日志里,否则很难解释为什么召回了某些证据。
8.2 Query Decomposition 适合复杂问题
复杂问题经常包含多个子问题:
企业客户取消年付套餐时,退款金额怎么计算,是否会影响已开票金额?
这可能拆成:
- 年付套餐取消规则;
- 退款金额计算;
- 发票和财务处理;
- 企业客户是否有特殊条款。
每个子 query 单独召回,再融合和重排,通常比把原问题直接检索更稳定。
但 decomposition 有一个风险:它会引入模型臆测的子问题。建议把拆解结果和原始 query 一起记录,并在 reranker 阶段要求证据仍然支持原始问题,而不是只支持某个被改写后的问题。
9. 多样性、权限、版本和新鲜度
排序不是只看相关性。RAG 的证据上下文还要满足工程约束。
9.1 权限必须在检索前过滤
如果用户无权访问某个文档,最好在检索层就过滤掉,而不是召回后再让模型忽略。
必备字段:
tenant_idaccess_scopeowner_teamclassificationstatuseffective_atexpires_at
托管检索能力通常也会支持 metadata filtering。比如 OpenAI File Search 和 Retrieval 文档都强调可以基于文件或属性 metadata 做过滤。自建系统也应该把 metadata filter 设计成检索 API 的一等参数。
9.2 版本和状态要硬过滤
如果文档有 active / deprecated / draft 状态,默认只检索 active。不要把“新版本更高分”交给排序模型自然学会。
常见策略:
| 状态 | 默认处理 |
|---|---|
| active | 可召回 |
| deprecated | 默认过滤;用户明确问历史版本时才召回 |
| draft | 只对内部授权用户召回 |
| archived | 默认过滤 |
9.3 多样性控制避免上下文被同一来源占满
如果 top 10 全来自同一个长文档的相邻 chunk,模型可能看不到另一个关键来源。可以设置:
- 每个
source_id最多保留 2-3 个 chunk; - 同一章节相邻 chunk 合并或补全;
- 使用 MMR 类策略兼顾相关性和差异;
- 对冲突证据保留至少一条反例;
- 对表格证据保留完整行列上下文。
多样性不是为了“平均分配”,而是为了让上下文覆盖回答所需的不同证据角色。
10. 分数阈值与低置信度策略
很多团队只关心“召回了什么”,不关心“什么时候不该回答”。
实际产品里,低置信度问题必须有策略:
- 没有任何候选超过阈值:拒答或追问;
- 候选分数接近但互相冲突:说明冲突并请求确认;
- 只有背景证据,没有直接证据:给出保守回答;
- 需要用户身份、时间或产品线才能判断:追问;
- 命中旧版本而新版本缺失:提示无法确认最新规则。
阈值不要只用一个全局数字。不同 query 类型可以有不同阈值。
| Query 类型 | 阈值策略 |
|---|---|
| 错误码 / 条款号 | 要求精确命中或强支持 |
| 概念解释 | 可以接受多个 partial 证据 |
| 合规 / 法务 | 阈值更高,冲突时不强答 |
| 客服话术 | 允许引用政策后生成表达 |
| 数据事实 | 必须命中明确来源和时间 |
一个成熟的 RAG 系统,不是永远回答,而是知道什么时候回答、什么时候追问、什么时候拒答。
11. 一个最小混合检索示例
下面是一个不依赖具体数据库的最小排序框架。真实系统里,sparse_search 可以来自 Elasticsearch / OpenSearch / Vespa / PostgreSQL FTS,dense_search 可以来自向量库或托管检索,rerank 可以来自 cross-encoder、LLM 或自建模型。
from dataclasses import dataclass, field
from typing import Dict, Iterable, List, Optional
@dataclass
class Candidate:
chunk_id: str
source_id: str
text: str
metadata: Dict[str, str]
sparse_rank: Optional[int] = None
dense_rank: Optional[int] = None
fusion_score: float = 0.0
rerank_score: float = 0.0
reasons: List[str] = field(default_factory=list)
def rrf_fuse(
result_lists: Dict[str, List[Candidate]],
weights: Dict[str, float],
k: int = 60,
) -> List[Candidate]:
merged: Dict[str, Candidate] = {}
for channel, results in result_lists.items():
weight = weights.get(channel, 1.0)
for rank, item in enumerate(results, start=1):
if item.chunk_id not in merged:
merged[item.chunk_id] = item
candidate = merged[item.chunk_id]
candidate.fusion_score += weight / (k + rank)
if channel == "sparse":
candidate.sparse_rank = rank
elif channel == "dense":
candidate.dense_rank = rank
candidate.reasons.append(f"{channel}:rank={rank}")
return sorted(merged.values(), key=lambda x: x.fusion_score, reverse=True)
def apply_policy_filters(
candidates: Iterable[Candidate],
user_scope: str,
max_per_source: int = 3,
) -> List[Candidate]:
kept: List[Candidate] = []
source_counts: Dict[str, int] = {}
for item in candidates:
if item.metadata.get("status") != "active":
continue
if user_scope not in item.metadata.get("access_scope", "").split(","):
continue
if source_counts.get(item.source_id, 0) >= max_per_source:
continue
source_counts[item.source_id] = source_counts.get(item.source_id, 0) + 1
kept.append(item)
return kept
def rerank(query: str, candidates: List[Candidate]) -> List[Candidate]:
# Replace this stub with a cross-encoder, LLM reranker, or managed reranking API.
# Keep the interface stable so retrieval experiments are easy to compare.
query_terms = set(query.lower().split())
for item in candidates:
text_terms = set(item.text.lower().split())
overlap = len(query_terms & text_terms)
item.rerank_score = 0.7 * item.fusion_score + 0.3 * overlap
return sorted(candidates, key=lambda x: x.rerank_score, reverse=True)
def retrieve_for_rag(query: str, user_scope: str) -> List[Candidate]:
sparse = sparse_search(query, top_n=80)
dense = dense_search(query, top_n=80)
weights = choose_weights(query)
fused = rrf_fuse(
{"sparse": sparse, "dense": dense},
weights=weights,
k=60,
)
filtered = apply_policy_filters(fused[:100], user_scope=user_scope)
ranked = rerank(query, filtered[:60])
return ranked[:10]
这个例子刻意保留了几个接口:
choose_weights(query):按 query 类型调整 sparse / dense 权重;apply_policy_filters(...):把权限、版本、来源上限放在排序链路中;rerank(...):把精排能力和召回能力解耦;reasons:记录候选为什么被召回,便于调试。
真实系统上线前,还应该把每次请求的 query、rewrite query、filter、sparse rank、dense rank、fusion score、rerank score、最终引用都写入可检索日志。
12. 如何评测混合检索是否真的更好
第四篇讲过 RAG 评测,这里聚焦检索排序本身。
不要只看“感觉答案更好”。要把检索链路拆开比较:
| 实验组 | 目的 |
|---|---|
| sparse only | 关键词基线 |
| dense only | 向量基线 |
| sparse + dense fusion | 验证混合召回是否提升 recall |
| fusion + reranker | 验证精排是否提升 top-k |
| fusion + reranker + diversity | 验证上下文质量是否提升 |
| rewrite + fusion + reranker | 验证 query rewrite 是否有收益 |
12.1 指标分层
| 指标 | 看什么 | 典型用途 |
|---|---|---|
| Recall@50 | 正确证据是否进入候选池 | 诊断召回问题 |
| Recall@10 | 正确证据是否靠前 | 诊断初排和融合 |
| MRR | 第一个正确证据排第几 | 问答首证据质量 |
| nDCG@k | 多条证据排序质量 | 多证据答案 |
| Citation Support Rate | 最终引用是否支持答案 | 连接检索和生成 |
| Empty / Abstain Rate | 系统拒答频率 | 低置信度策略 |
| Latency p95 | 用户体验 | 线上 SLA |
| Cost per Query | 推理和检索成本 | 规模化成本 |
12.2 必须按 query 类型切分
总平均值会掩盖问题。至少按这些类型切:
- 精确实体类:错误码、SKU、条款号;
- 政策规则类:能不能、是否允许、怎么计算;
- 流程类:先做什么、后做什么;
- 对比类:A 和 B 有什么区别;
- 摘要类:总结某文档或某主题;
- 多跳类:需要多个来源组合;
- 低置信度类:知识库没有答案或权限不足。
一个系统可能总体 Recall@10 提升 3%,但错误码类下降 20%。如果不分桶,你会误以为优化成功。
12.3 评测样本从线上失败里来
初始 gold set 可以人工构造,但真正有价值的样本来自线上失败:
- 用户追问“不是这个意思”;
- 人工客服改写了答案;
- 用户点击了“没有帮助”;
- 引用被用户打开后没有支持答案;
- reranker 高分候选被人工判为无关;
- 模型回答“无法确认”但知识库其实有答案。
每周把失败样本加入评测集,再比较检索策略,就能形成真正的排序闭环。
13. 生产架构建议
一个可维护的 RAG 检索层可以拆成这些模块。
Client / Agent
-> Retrieval API
-> Query Analyzer
-> Metadata Policy Service
-> Sparse Index
-> Vector Index
-> Fusion Service
-> Reranker Service
-> Context Packer
-> Retrieval Logger
-> Generation API
-> Evaluation Pipeline
13.1 Retrieval API 要稳定
不要让业务代码直接拼多个数据库调用。建议提供一个稳定接口:
{
"query": "命中 E7782 后能否自动退款?",
"user": {
"tenant_id": "global",
"access_scope": ["customer_service"]
},
"filters": {
"status": "active",
"language": "zh-CN"
},
"strategy": "hybrid_rerank_v3",
"max_context_chunks": 8
}
返回:
{
"strategy": "hybrid_rerank_v3",
"rewritten_queries": [
"E7782 自动退款 人工审核",
"错误码 E7782 退款处理规则"
],
"contexts": [
{
"chunk_id": "refund_policy#sec_4#chunk_02",
"source_id": "refund_policy_2026_03",
"text": "错误码 E7782 表示支付渠道返回风险拦截。命中该错误码后,退款申请必须进入人工审核,不得自动退款。",
"score": 0.92,
"support": "direct",
"citation": {
"title": "退款政策 2026-03 版",
"section": "4.2",
"page": 4
}
}
]
}
这样生成层不需要知道底层是 Elasticsearch、向量库还是托管 file search。检索策略可以迭代,调用契约保持稳定。
13.2 策略要可版本化
每次检索策略调整都应该有版本号:
dense_only_v1hybrid_rrf_v1hybrid_rrf_rerank_v2hybrid_rrf_rerank_rewrite_v3
日志和评测都记录策略版本。否则线上表现变好或变坏时,你无法定位是哪次权重、过滤、reranker 或 chunk 变更造成的。
13.3 上下文打包是排序的一部分
最后放进 prompt 的不只是 top-k 文本。Context packing 要处理:
- 证据顺序:先直接证据,再背景证据;
- 引用格式:每条证据有 source、page、section;
- 相邻补全:必要时带前后 chunk;
- 冲突证据:明确标注不同版本或不同条件;
- token 预算:长表格、长段落要压缩但不能丢关键字段;
- 去重:重复证据不要浪费上下文窗口。
检索排序的终点不是列表,而是一个可被生成模型可靠使用的证据包。
14. 常见误区
14.1 误区一:只要 embedding 模型更强,就不需要关键词
embedding 再强,也不应该承担所有精确匹配任务。错误码、合同编号、字段名和条款号天然适合 sparse 检索。
14.2 误区二:把 raw BM25 分数和 cosine 分数直接相加
不同检索器分数尺度不同,直接相加会让某一侧支配排序。先用 RRF 这类基于排名的融合,或者做经过验证的分数归一化。
14.3 误区三:召回 top 5 直接给模型
top 5 适合最终上下文,不适合召回。召回要宽,重排要准,上下文要精。
14.4 误区四:reranker 输入太少
reranker 只看 top 10,很容易错过排在 20-50 的正确证据。先把候选池做宽,再让 reranker 工作。
14.5 误区五:权限和版本放到 prompt 里提醒
这会制造安全和一致性风险。权限、租户、版本、状态应该在检索层过滤。
14.6 误区六:只评测最终答案,不评测检索过程
最终答案错了,原因可能是没召回、排错、证据冲突、上下文打包错误,也可能是生成模型没遵守证据。只看答案会让修复方向变得模糊。
14.7 误区七:所有 query 用同一套权重
错误码查询和概念解释查询不应该使用同一套 sparse / dense 权重。至少要按 query 类型做简单路由。
15. 落地路线图
第 1 周:建立可观测基线
- 接入 sparse 和 dense 两条召回;
- 每条候选记录 sparse rank、dense rank、source、version、access scope;
- 建立 100-300 条人工 gold query;
- 跑 sparse only、dense only、hybrid 三组评测。
第 2 周:加入融合和过滤
- 实现 RRF 或托管 hybrid ranking;
- 加入 metadata filter;
- 加入 source cap 和去重;
- 按 query 类型调整 sparse / dense 权重;
- 输出 Recall@50、Recall@10、MRR、nDCG。
第 3 周:加入 reranker
- 选择 cross-encoder、LLM reranker 或托管 rerank 能力;
- rerank 30-80 条候选;
- 记录 rerank score 和 support 类型;
- 比较 hybrid 与 hybrid + rerank。
第 4 周:打通上下文和引用
- 把 rerank 后证据打包成结构化上下文;
- 接入引用与归因校验;
- 对低置信度问题设计拒答和追问;
- 建立线上失败样本回流流程。
第 5 周:优化延迟和成本
- 缓存热门 query 和 query rewrite;
- 对简单 query 跳过重 reranker;
- 使用分层 rerank;
- 控制每个来源和每个答案的 token 预算;
- 按业务价值设置不同 SLA。
16. 发布前自检清单
上线前至少检查这些问题。
召回
- sparse 和 dense 都可以单独运行;
- 错误码、SKU、条款号能被关键词稳定召回;
- 自然语言改写能被向量召回;
- 正确证据在 Recall@50 中表现稳定;
- chunk 标题路径进入索引文本。
融合
- 没有直接相加不同检索器 raw score;
- 使用 RRF 或经过验证的归一化;
- query 类型会影响 sparse / dense 权重;
- 融合后按 chunk_id、source、hash 去重;
- 记录每个候选来自哪些通道。
重排
- reranker 至少看到 30-80 条候选;
- rerank score 和支持类型写入日志;
- 评测 hybrid 与 hybrid + rerank 的差异;
- 低分候选不会进入最终上下文;
- 高风险场景有更严格阈值。
权限和版本
- tenant、access scope、status、version 在检索层过滤;
- deprecated 文档默认不召回;
- 用户明确问历史版本时才放开版本过滤;
- 文档权限变化后索引能及时更新;
- 日志不会泄漏无权访问内容。
上下文
- 每条上下文都有 citation;
- 相邻 chunk 补全不会引入无关内容;
- 同一来源不会挤占全部上下文;
- 冲突证据能被标记;
- 生成模型能看到足够直接证据。
评测
- 按 query 类型分桶;
- 同时看 Recall@k、MRR、nDCG、引用支持率;
- 记录 latency p50/p95 和成本;
- 线上失败样本能回流到评测集;
- 检索策略有版本号并可回滚。
17. 总结
RAG 的检索层不是一个向量库 top-k 参数,而是一条排序闭环。
Sparse retrieval 负责精确符号和词面证据,dense retrieval 负责语义改写和概念匹配,hybrid fusion 把多个通道变成候选池,reranker 再判断哪些证据真正支持用户问题。metadata filter、版本控制、权限、新鲜度、多样性和上下文打包,同样是排序质量的一部分。
一个实用原则是:
召回要宽,重排要准,上下文要精,评测要分桶。
如果系统只在“向量 top-k”上调参,问题会反复出现:召回不到、排错、引用不支持、旧版本混入、低置信度强答。把检索层工程化以后,RAG 才能从 demo 走向可维护的生产系统。
下一篇可以继续写:RAG 的低置信度与拒答策略怎么做:什么时候回答、追问和转人工。
参考资料
- OpenAI Developers, Retrieval guide, checked on 2026-06-21.
- OpenAI Developers, File search guide, checked on 2026-06-21.
- Nandan Thakur et al., BEIR: A Heterogeneous Benchmark for Zero-shot Evaluation of Information Retrieval Models, 2021.
- Omar Khattab and Matei Zaharia, ColBERT: Efficient and Effective Passage Search via Contextualized Late Interaction over BERT, 2020.
- Thibault Formal et al., SPLADE: Sparse Lexical and Expansion Model for First Stage Ranking, 2021.
- Rodrigo Nogueira, Zhiying Jiang, Ronak Pradeep, and Jimmy Lin, Document Ranking with a Pretrained Sequence-to-Sequence Model, 2020.
- Gordon V. Cormack, Charles L. A. Clarke, and Stefan Buettcher, Reciprocal Rank Fusion Outperforms Condorcet and Individual Rank Learning Methods, 2009.