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_id、chunk_id 和版本号,后续很难判断一次回答到底引用了哪一版证据。
2. 为每个 chunk 生成短上下文
上下文生成要克制。它的目标是解释 chunk 在原文中的位置,不是重写 chunk,也不是补充模型猜测。
可以使用下面这类提示词生成上下文:
你将看到一份文档的标题、章节路径、邻近段落和一个目标片段。
请用 1-2 句话说明该片段在整篇文档中的作用,必须只基于已给内容,不要添加外部信息。
输出应包含:主题、适用对象、关键约束、必要的版本或章节线索。
不要改写原始片段,不要扩展未出现的结论。
生成结果建议限制在 50-120 个中文字符。过短可能无法补足语义,过长会增加 embedding 成本、BM25 噪声和索引体积。
最终用于索引的文本可以分成两个字段,而不是简单拼接:
{
"context_text": "该片段来自企业客户续费政策 2026 的试运行章节,说明高级版客户的适用范围与试运行周期。",
"chunk_text": "本方案适用于高级版客户,试运行期为 30 天。"
}
向量检索可以使用 context_text + chunk_text,BM25 可以分别给 chunk_text 和 context_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_id、chunk_id、section_path、version - 上下文说明是否只基于原文生成,没有引入外部猜测
-
context_text和chunk_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 onlyindex_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 被召回。
参考资料
- Anthropic Engineering, “Introducing Contextual Retrieval”, 2024-09-19:https://www.anthropic.com/engineering/contextual-retrieval
- Anthropic Claude Cookbook, “Enhancing RAG with contextual retrieval”, 2024-09-13:https://platform.claude.com/cookbook/capabilities-contextual-embeddings-guide
- OpenAI API Docs, “Vector embeddings”:https://developers.openai.com/api/docs/guides/embeddings
- Elasticsearch Reference, “Reciprocal rank fusion”:https://www.elastic.co/docs/reference/elasticsearch/rest-apis/reciprocal-rank-fusion
- Cohere Docs, “Cohere’s Rerank Model”:https://docs.cohere.com/docs/rerank
- LlamaIndex Documentation, “Node Postprocessor”:https://developers.llamaindex.ai/python/framework/module_guides/querying/node_postprocessors/
- Fariba Afrin Irany, Sampson Akwafuo, “A Hybrid Retrieval and Reranking Framework for Evidence-Grounded Retrieval-Augmented Generation”, 2026-05-03:https://arxiv.org/abs/2605.01664
- Meftun Akarsu et al., “From BM25 to Corrective RAG: Benchmarking Retrieval Strategies for Text-and-Table Documents”, 2026-04-02:https://arxiv.org/abs/2604.01733