文章

Contextual Retrieval 实战:让 RAG Chunk 带着上下文被检索

系统讲解 Contextual Retrieval 的原理、索引改造、混合检索、重排与上线检查,帮助工程团队降低 RAG 漏召回并提升答案可追溯性。

Contextual Retrieval 实战:让 RAG Chunk 带着上下文被检索

背景:RAG 的问题常常不是模型不会答,而是证据没有被找回来

很多 RAG 系统上线初期看起来很有效:把文档切成 chunk,生成 embedding,放进向量库,查询时取 Top-K,再交给大模型回答。这个流程能快速做出原型,但一旦进入真实业务文档,就会暴露一个很硬的问题:chunk 在切分后失去了原文上下文

例如,一个 chunk 里只有「本方案适用于高级版客户,试运行期为 30 天」,它在原文中可能属于「企业客户续费政策 / 2026 版 / 华南区域」。如果检索阶段只看到这句话本身,向量模型未必能把它和「广州企业版客户续费试运行规则」这样的查询稳定关联起来。结果不是模型生成能力差,而是候选证据一开始就没有进入上下文窗口。

Contextual Retrieval 解决的是这个切分副作用。它不是替代 RAG,也不是单纯换一个更大的 embedding 模型,而是在索引阶段给每个 chunk 补一段轻量上下文,让它在被单独检索时仍然带着「来自哪份文档、哪个章节、讨论什么对象、和哪些约束相关」的语义线索。

核心原理:先让 chunk 变得可理解,再让检索器排序

传统 RAG 通常这样处理文档:

原始文档 → 切分 chunk → 生成 embedding → 向量检索 → Top-K → LLM 生成

Contextual Retrieval 会把索引流程前移一步:

原始文档 → 切分 chunk → 为 chunk 生成上下文说明 → 上下文化 chunk → embedding / BM25 索引 → 混合检索 → rerank → LLM 生成

这里的关键不是把整篇文档塞进每个 chunk,而是生成一段短上下文,例如:

该片段来自《企业客户续费政策 2026》,属于「试运行与适用范围」章节,说明高级版客户在华南区域续费时的 30 天试运行规则。

原始 chunk: 本方案适用于高级版客户,试运行期为 30 天。

这样做会同时改善两类检索信号:

  • Contextual Embeddings:embedding 输入不再只是孤立 chunk,而是「上下文说明 + 原始 chunk」。向量表示会包含更完整的实体、主题和章节语义。
  • Contextual BM25:BM25 依赖词项匹配。上下文说明把标题、产品名、区域、版本号、章节名等关键词补进索引文本后,精确词查询、编号查询和业务术语查询更容易命中。

这也是它和「扩大 chunk size」的区别。扩大 chunk size 会把更多文本混在一起,可能提升召回,也可能引入噪声;Contextual Retrieval 更像是给每个 chunk 加一个简短索引摘要,目标是补足语义定位,而不是把更多正文强塞进候选证据。

工程落地:推荐采用四段式检索链路

1. 文档解析与稳定切分

不要一开始就让 LLM 生成上下文。先把文档结构处理好:标题、层级、页码、表格标题、段落编号、发布日期、版本号、权限范围都要进入结构化元数据。

建议每个 chunk 至少保存这些字段:

{
  "doc_id": "policy-renewal-2026",
  "doc_title": "企业客户续费政策 2026",
  "section_path": ["续费规则", "试运行与适用范围"],
  "chunk_id": "policy-renewal-2026#0038",
  "chunk_text": "本方案适用于高级版客户,试运行期为 30 天。",
  "page": 12,
  "version": "2026-01",
  "source_uri": "internal://docs/policy-renewal-2026"
}

这些元数据有两个作用:一是参与上下文生成;二是用于检索结果回溯、权限过滤和灰度验证。没有稳定的 doc_idchunk_id 和版本号,后续很难判断一次回答到底引用了哪一版证据。

2. 为每个 chunk 生成短上下文

上下文生成要克制。它的目标是解释 chunk 在原文中的位置,不是重写 chunk,也不是补充模型猜测。

可以使用下面这类提示词生成上下文:

你将看到一份文档的标题、章节路径、邻近段落和一个目标片段。
请用 1-2 句话说明该片段在整篇文档中的作用,必须只基于已给内容,不要添加外部信息。
输出应包含:主题、适用对象、关键约束、必要的版本或章节线索。
不要改写原始片段,不要扩展未出现的结论。

生成结果建议限制在 50-120 个中文字符。过短可能无法补足语义,过长会增加 embedding 成本、BM25 噪声和索引体积。

最终用于索引的文本可以分成两个字段,而不是简单拼接:

{
  "context_text": "该片段来自企业客户续费政策 2026 的试运行章节,说明高级版客户的适用范围与试运行周期。",
  "chunk_text": "本方案适用于高级版客户,试运行期为 30 天。"
}

向量检索可以使用 context_text + chunk_text,BM25 可以分别给 chunk_textcontext_text 设置不同权重。通常原始 chunk 权重应高于上下文说明,避免「说明文本」压过原文证据。

3. 第一阶段召回采用混合检索

不要只依赖向量检索。生产 RAG 里,很多问题包含编号、专有名词、表格字段、政策版本、接口名、错误码、产品代码,这些内容恰好是 BM25 的强项。

推荐第一阶段同时跑两路:

query
 ├─ dense retriever:语义召回,适合改写、同义表达、长问题
 └─ sparse retriever:BM25 / lexical,适合关键词、编号、术语、版本号

 rank fusion:RRF 或带权融合

 candidate set:例如 50-200 个候选 chunk

如果团队不想调大量权重,可以从 Reciprocal Rank Fusion(RRF) 开始。RRF 不要求 BM25 分数和向量相似度在同一尺度上,工程上更容易稳定落地。等有了离线评估集,再考虑学习排序或带权融合。

4. 第二阶段使用 reranker 提升 Top-N 精度

第一阶段的目标是尽量别漏掉正确证据,第二阶段才是把最有用的证据排到前面。这就是 reranker 的位置。

Reranker 通常读取 query 和候选 chunk,对每个 query-chunk pair 给相关性分数。它比单向量相似度更贵,但只处理第一阶段召回出的几十到几百个候选,成本可控。

hybrid candidates top 100

reranker(query, candidates)

reranked top 5-12

LLM answer with citations

实际落地时,top_n 不要盲目设大。Top-N 太小会漏掉多跳问题需要的补充证据,Top-N 太大会增加生成阶段噪声。可以按问题类型配置:

问题类型建议 Top-N
事实查询5-8
制度解释8-12
多文档对比12-20

适用场景:不是所有 RAG 都需要上下文化索引

Contextual Retrieval 最适合以下场景:

  • 企业制度、合同、规范文档:这类文档依赖章节层级和适用范围。单个 chunk 往往只说规则本身,不说规则属于哪类客户、哪一版政策、哪个区域、哪个例外条件。
  • 技术文档与代码库问答:函数片段、配置项、错误码说明经常离不开模块名、文件路径和调用链。给 chunk 补上「来自哪个模块、解决什么问题、与哪个接口相关」,会显著改善检索可解释性。
  • 表格和半结构化资料:表格切分后容易丢失表头、单位、适用年份和维度。上下文说明可以补充「这一行属于哪个表、指标单位是什么、行列含义是什么」。
  • 多版本知识库:当同一政策、同一 API、同一产品有多个版本时,版本号和生效时间必须进入上下文。否则向量检索很容易把旧版本和新版本混在一起。

不太适合的情况也要明确:如果知识库很小,可以直接放入长上下文;如果文档本身每段都很完整,上下文化收益可能有限;如果查询主要是精确数据库过滤,而不是自然语言问答,优先做结构化查询,不要强行 RAG 化。

常见误区

误区一:把上下文生成当成摘要生成

上下文不是摘要。摘要追求概括全文,而 Contextual Retrieval 只需要说明当前 chunk 的定位。生成太抽象的摘要会损害检索,因为它可能丢掉原文里的编号、实体和约束条件。

误区二:只评估最终回答,不评估检索

很多团队只看最终答案是否正确,这会把问题混在一起:可能是检索漏召回,可能是 reranker 排错,可能是 prompt 没约束引用,也可能是模型生成时幻觉。上线前应拆开评估:

检索 Recall@K → Rerank MRR / nDCG → 证据覆盖率 → 最终答案准确率 → 引用一致性

只有这样才能知道系统应该调 chunk、调 fusion、调 reranker,还是调回答 prompt。

误区三:上下文说明越长越好

上下文太长会带来三类副作用:索引成本上升、关键词噪声上升、召回结果变得更像「章节摘要」而不是原始证据。建议把上下文当成索引辅助字段,而不是正文替代品。

误区四:忽略权限和版本过滤

上下文化索引可能把文档标题、客户名称、区域等敏感信息写入索引字段。生产环境必须在检索前做权限过滤,不能等检索结果出来后再过滤。版本过滤也一样:先限定可用版本,再做召回和排序。

误区五:认为 reranker 可以修复所有漏召回

Reranker 只能重排候选集合,不能重排没有被召回的证据。如果第一阶段 Top-100 没有正确 chunk,reranker 没有机会补救。因此第一阶段召回应优先保守,宁可候选多一些,再交给 reranker 精排。

上线检查清单

索引侧检查

  • 每个 chunk 是否有稳定 doc_idchunk_idsection_pathversion
  • 上下文说明是否只基于原文生成,没有引入外部猜测
  • context_textchunk_text 是否分字段存储,便于调权与回滚
  • embedding 输入是否记录生成模型、维度、时间和版本
  • BM25 字段权重是否可配置
  • 是否支持按租户、权限、生效日期和版本过滤

检索侧检查

  • 是否同时保留 dense 与 sparse 检索结果
  • fusion 方法是否可解释,RRF 参数是否记录
  • reranker 的输入候选数量、输出 top_n 是否可配置
  • 是否记录每次命中的 chunk_id、rank、score、retriever 来源
  • 是否能回放一次查询的完整检索链路

评估侧检查

  • 是否有覆盖真实业务问法的评估集
  • 是否单独评估 Recall@K、MRR、nDCG
  • 是否区分单文档问题、多文档问题、版本敏感问题、表格问题
  • 是否对旧索引与新索引做 A/B 对比
  • 是否抽检生成答案与引用 chunk 的一致性

运行侧检查

  • 是否监控 embedding 成本、索引体积和构建耗时
  • 是否监控检索延迟、rerank 延迟和总 TTFT
  • 是否支持索引灰度发布与快速回滚
  • 是否保留原始 chunk,不允许只保存上下文化后的文本
  • 是否能按 doc_id 或 version 批量重建索引

一个可落地的最小实现方案

如果团队已有基础 RAG,可以按三步改造,不必一次重构全部链路。

第一步:只增加上下文字段

保留现有 chunk 切分方式,为每个 chunk 生成 context_text,并新增索引版本:

  • index_v1:chunk_text only
  • index_v2:context_text + chunk_text

先离线对比 Recall@10 和人工命中率。如果没有明显提升,不要急着上线;先检查上下文生成质量和 chunk 切分策略。

第二步:引入 BM25 + RRF

在原向量召回之外增加 BM25 检索,并用 RRF 合并结果:

from collections import defaultdict

def rrf_fuse(result_lists, k=60):
    scores = defaultdict(float)
    for results in result_lists:
        for rank, item in enumerate(results, start=1):
            scores[item.chunk_id] += 1.0 / (k + rank)
    return sorted(scores.items(), key=lambda x: x[1], reverse=True)

这段逻辑看起来简单,但工程上很有价值:它避免了直接混合 BM25 分数和向量相似度分数导致的尺度问题。

第三步:只对融合后的候选做 rerank

不要对全库 rerank。先取融合后的 Top-100,再让 reranker 输出 Top-8 或 Top-12。这样既保留召回空间,又能控制延迟。

dense top 80 + bm25 top 80

RRF fusion top 100

reranker top 8

answer with citations

如果 reranker 成本较高,可以按问题类型触发:短事实问题默认不开,制度解释、多文档对比、低置信度查询才打开。

FAQ

Contextual Retrieval 会不会增加幻觉?

会有这个风险,主要来自上下文生成阶段。如果上下文说明包含原文没有的信息,后续检索和回答都会被污染。控制办法是限制生成输入,只允许使用文档标题、章节路径、邻近段落和原始 chunk;同时保留原始 chunk,在最终回答时引用原文而不是引用上下文说明。

它和 query rewriting、HyDE 是什么关系?

它们作用在不同阶段。Contextual Retrieval 改造的是索引侧,让文档 chunk 更容易被找回;query rewriting 和 HyDE 改造的是查询侧,让用户问题更适合检索。生产系统可以组合使用,但要分别评估收益,避免每一层都引入模型调用,最后延迟和成本失控。

是否应该把 context_text 展示给最终用户?

一般不建议直接展示。context_text 是检索辅助字段,不是权威原文。对用户展示时应优先展示原始 chunk、标题、页码、版本和来源链接。上下文说明可以作为内部调试信息,帮助工程师判断为什么某个 chunk 被召回。

参考资料

常见问题

Contextual Retrieval 和普通向量检索的核心区别是什么?
普通向量检索通常只对切分后的 chunk 本身做 embedding;Contextual Retrieval 会在索引前为每个 chunk 补充文档级上下文,让 chunk 在脱离原文时仍保留标题、章节、实体和业务语义。
做了 Contextual Retrieval 后还需要 BM25 和 reranker 吗?
通常仍然需要。上下文化 chunk 提升候选召回质量,BM25 保留关键词和编号匹配能力,reranker 负责在候选集合中做更精细的 query-chunk 相关性排序。
这套方案最容易出问题的地方是什么?
最常见的问题是上下文生成过长、污染 chunk 原意、索引版本不可追溯,以及只看最终回答准确率而不单独评估检索阶段的 Recall、MRR 和证据覆盖率。
Contextual Retrieval 会不会增加幻觉?
会有风险,主要来自上下文生成阶段。控制办法是限制生成输入只允许使用文档标题、章节路径、邻近段落和原始 chunk,并在最终回答时引用原文而非上下文说明。