上下文检索(Contextual Retrieval):让 RAG 不再丢失语境
传统 RAG 在分块与向量化时容易丢失文档级上下文,导致检索 recall 不足。Anthropic 提出的 Contextual Retrieval 在嵌入与建 BM25 索引前,用 LLM 为每个块生成简短「情境说明」并拼在块前,然后使用 Contextual Embeddings + Contextual BM25。本文基于Anthropic 原文梳理原理、实现要点与 Prompt Cach
摘要:传统 RAG 在分块与向量化时容易丢失文档级上下文,导致检索 recall 不足。Anthropic 提出的 Contextual Retrieval 在嵌入与建 BM25 索引前,用 LLM 为每个块生成简短「情境说明」并拼在块前,使 Contextual Embeddings + Contextual BM25 可将 top-20 检索失败率降低约 49%,结合 Reranking 可进一步降至约 67%。本文基于 Anthropic 原文 梳理原理、实现要点与 Prompt Caching 成本控制,并给出基于 LangChain 与 LlamaIndex 的落地示例。
关键词:RAG · 检索增强生成 · Contextual Retrieval · 上下文检索 · Contextual Embeddings · BM25 · Reranking · Prompt Caching · 向量检索 · LangChain · LlamaIndex
1. 为什么需要「带语境的」检索?
要让大模型在具体业务场景中有用,往往需要为其注入领域知识:客服机器人需要企业产品与政策知识,法律分析助手需要大量判例与法规,代码助手需要项目代码库与文档。这类知识通常通过 检索增强生成(Retrieval-Augmented Generation, RAG) 注入:从知识库中检索相关片段,拼接到用户提问前,从而扩展模型的「可见」上下文。
传统 RAG 的常见问题是:在将文档编码为向量或建立索引时,会丢失「这段文字属于谁、在什么情境下」的上下文,导致检索阶段经常找不到真正相关的片段。Anthropic 提出的 Contextual Retrieval(上下文检索) 通过两种子技术——Contextual Embeddings(上下文嵌入) 和 Contextual BM25(上下文 BM25)——在保持可扩展性的前提下,显著降低「该召回却没召回」的比例,从而直接提升下游任务表现。
根据其公开实验,该方法可使 top-20 检索失败率降低约 49%,若再结合 Reranking(重排序),失败率可进一步降低约 67%,对生产级 RAG 系统具有明确参考价值。
2. RAG 简要回顾:从单次提示到大规模知识库
2.1 何时可以不用 RAG?
若知识库规模小于约 20 万 token(约 500 页材料),一种简单且有效的做法是:直接把整份知识库放进提示词,无需做分块、向量化与检索。配合 Prompt Caching(提示缓存),重复使用的长提示可被缓存,在多次请求间复用,从而显著降低延迟与成本(Anthropic 的缓存方案可带来约 2 倍以上延迟优化与最高约 90% 的成本节省)。在知识体量可控时,应优先评估「全量上下文 + 缓存」是否已满足需求。
2.2 知识库变大后的标准做法:RAG
当知识库超出单次上下文窗口,就需要可扩展的检索方案。典型 RAG 流程包括:
- 分块:将语料切分为较小文本块(通常几百 token 以内);
- 向量化:用 Embedding 模型 将每个块编码为向量,以表示语义;
- 存储与检索:将向量存入向量数据库,查询时按与 query 的语义相似度检索最相关的若干块,并拼接到生成模型的输入中。
语义向量擅长捕捉「意思相近」的表述,但在精确词句、专有名词、编号等场景容易失灵。因此业界常同时使用 BM25(Best Matching 25) 做词法匹配:基于 TF-IDF 思想,对词频做饱和、并考虑文档长度,适合「错误码 TS-999」「法规第 N 条」这类需要字面匹配的查询。
常见融合方式为:
- 对语料分块后,同时构建 TF-IDF/BM25 索引 和 Embedding 向量;
- 用 BM25 按词法相似度取 top 块;
- 用 Embedding 按语义相似度取 top 块;
- 用 Rank Fusion 等方式合并、去重;
- 将 top-K 块送入大模型生成回答。
这样既保留语义理解,又补足精确匹配能力。但上述流程有一个根本性问题:分块与编码过程会破坏「这段话在整篇文档中的位置与含义」这一上下文。

3. 传统 RAG 的「上下文缺失」问题
分块通常按长度或段落切分,单个块内往往缺少「是谁、何时、何主题」的说明。例如在 SEC 财报语料中,某块内容为:
- “The company’s revenue grew by 3% over the previous quarter.”
若用户问的是 「ACME Corp 在 2023 年 Q2 的收入增速是多少?」,该块本身没有出现公司名、也没有时间信息,仅靠这一段文字做 Embedding 或 BM25,很难在检索阶段被正确匹配或排序到前列。也就是说,检索阶段依赖的是「裸块」的表示,而裸块丢失了文档级上下文,导致 recall 不足、下游回答质量受限。
4. Contextual Retrieval:给每个块加上「情境说明」
Contextual Retrieval 的核心思路是:在向量化和建 BM25 索引之前,先为每个块生成一段简短的、块专属的「情境说明」,并拼在该块前面,再用「情境 + 原块」做 Embedding 和 BM25 索引。这样,检索时匹配到的不再是孤立的句子,而是「带说明的片段」。

4.1 具体做法示例
仍以 SEC 财报为例:
- 原始块:
"The company's revenue grew by 3% over the previous quarter." - 上下文化块:
"This chunk is from an SEC filing on ACME corp's performance in Q2 2023; the previous quarter's revenue was $314 million. The company's revenue grew by 3% over the previous quarter."
前半句由模型根据整篇文档生成,把「公司、时间、上一季度收入」等关键情境补全;后半句是原块。这样,无论是语义检索(Embedding)还是词法检索(BM25),都更容易命中「ACME」「Q2 2023」「revenue」等关键信息。
两种子技术对应关系为:
- Contextual Embeddings:对「情境 + 原块」做向量化并检索;
- Contextual BM25:对「情境 + 原块」建 BM25 索引并检索。
二者可单独使用,也可同时使用;实验表明同时使用时收益最大。
4.2 与其它「带上下文」方案的区分
Anthropic 文中提到,此前已有一些为块增加上下文的思路,例如:为块加通用文档摘要、假设性文档嵌入(HyDE)、基于摘要的索引等。他们在实验中尝试或评估后,收益有限或表现一般。Contextual Retrieval 的差异在于:用 LLM 为每个块生成短小、块专属的情境描述,且与 Prompt Caching 结合后可低成本规模化,而不是简单加一层通用摘要或假设文档。
4.3 实现要点:批量生成情境
显然无法人工为成千上万个块写情境。Anthropic 的做法是用 Claude 3 Haiku 等模型,对「整篇文档 + 当前块」一次性生成简短情境。示例 prompt 如下(保留其英文结构便于复现):
<document>
{{WHOLE_DOCUMENT}}
</document>
Here is the chunk we want to situate within the whole document
<chunk>
{{CHUNK_CONTENT}}
</chunk>
Please give a short succinct context to situate this chunk within the overall document for the purposes of improving search retrieval of the chunk. Answer only with the succinct context and nothing else.
生成的情境一般约 50–100 token,只拼在块前,不改变块内原文。预处理流程可概括为:
- 对文档分块;
- 对每个块,将「整文档 + 该块」送入上述 prompt,得到情境文本;
- 将「情境 + 原块」作为新文本,分别做 Embedding 与 BM25 索引;
- 检索时仍用用户 query 在向量库与 BM25 索引中检索,得到 top-K 块(此时存储与返回的可以是「情境 + 原块」或按需只返回原块)。
4.4 用 Prompt Caching 控制成本
需要先澄清一点:Prompt Caching 并不会减少你每次请求时“发送”的 token 数量。在为每个块生成情境时,你仍然会对每一个块构造并发送完整 prompt(整篇文档 + 该块 + 指令),也就是说,从客户端角度看,请求次数和每次请求的输入 token 数都没有变少。
成本与延迟的优化发生在服务端,机制大致如下:
-
重复前缀的识别与缓存
同一篇文档会对应很多个块,因此很多次请求的 prompt 都共享同一段「长前缀」(即<document>…</document>里的整篇文档)。Anthropic 在服务端会识别出这段重复内容,对其做缓存(存储的是该段内容的 KV cache 等内部表示及哈希,而非明文),并在后续请求中复用,而不是每次都重新做完整计算。 -
差异化计费带来成本下降
- 首次出现:这段文档对应的 token 按「缓存写入」计费(例如约为基础输入价格的 1.25× 或 2×,视缓存时长而定)。
- 后续命中:同一段文档在后续请求中被命中时,按「缓存读取」计费。Anthropic 对缓存读取的定价约为基础输入价格的 10%(即 0.1×)。
因此,虽然你「发送」的 token 总数没变,但计费时:整篇文档在第一次请求后,在后续所有针对同一文档的块上,都只按 0.1× 计费,从而显著拉低单次请求和整体预处理成本。
-
延迟
因为重复前缀不再每次重新计算,首 token 延迟也会下降(Anthropic 公开数据中,长 prompt 场景下可有约 2× 以上加速)。 -
与情境生成成本的对应关系
文中给出的参考数据是:在 800 token 块、8k token 文档、约 50 token 指令、每块约 100 token 情境的设定下,每百万文档 token 的一次性情境生成成本约 1.02 美元。这一数字正是在「同一文档被多块复用、文档部分大量以缓存读取计费」的前提下得到的;若没有 Prompt Caching,同一文档在每条请求里都按全价计费,总成本会高得多。因此,在实现时需按文档维度组织请求(同一文档的块连续或成批调用),并按照 Anthropic 的 prompt caching 文档 在请求中正确标记可缓存区间(如对<document>块设置cache_control),才能让服务端稳定命中缓存、实现上述成本与延迟收益。
4.5 实践建议与工程细节
- 分块策略:块大小、边界(按句/按段/按小节)以及块间重叠都会影响检索效果,需要针对领域做调优。
- Embedding 模型:Contextual Retrieval 在多种 Embedding 上均有效,但不同模型收益不同,可优先尝试 Gemini、Voyage 等。
- 情境 prompt:上述通用 prompt 已能带来明显提升;若领域有固定术语、实体或仅在其它文档中才出现的定义,可在 prompt 中加入领域说明或术语表,以进一步提升情境质量。
- 送入模型的格式:若最终传入模型的是「情境 + 原块」,建议在 prompt 中明确区分「情境」与「正文」,便于模型更好利用。
- 评估:检索指标(Recall@K、MRR 等)与最终任务指标(准确率、用户满意度)都应做持续评估,以决定是否启用 Contextual Retrieval、是否加 Reranking 以及 K 的取值。
5. 通过 LangChain 实现上下文检索
LangChain 并未内置与 Anthropic 完全同名的「Contextual Retrieval」组件,但可以用文档转换 + 自定义处理在嵌入前为每个块补足情境,从而复现同一思路。核心是:先分块,再对每个块用 LLM 生成情境并拼到内容前,最后对「情境 + 原块」做 Embedding 与检索;如需与 BM25、Reranker 结合,可再叠加社区或自建检索器。
5.1 依赖与思路
典型依赖包括:langchain、langchain-community、向量库(如 chromadb 或 faiss-cpu)、Embedding 与 LLM 的对应包(如 langchain-anthropic、langchain-openai)。流程可概括为:
- 用
DocumentLoader加载文档,用RecursiveCharacterTextSplitter(或TokenTextSplitter)分块; - 对每个块,将「整文档 + 该块」送入 LLM,得到简短情境(50–100 token);
- 构造新文档:
page_content = 情境 + "\n\n" + 原块,保留metadata便于溯源; - 用 Embedding 模型对上述新文档做向量化并写入向量库;
- 使用
VectorStoreRetriever做语义检索;若需 BM25,可用langchain_community.retrievers.BM25Retriever,再自己做融合或交给EnsembleRetriever。
5.2 情境生成与文档转换示例
下面示例用「整文档 + 单块」调用 LLM 生成情境,并返回用于嵌入的文档列表(每项为「情境 + 原块」)。实际使用时需按文档粒度组织:同一文档的多个块共享同一份 whole_doc,以利后续做 Prompt Caching。
from langchain_core.documents import Document
from langchain_core.prompts import ChatPromptTemplate
from langchain_anthropic import ChatAnthropic
CONTEXT_PROMPT = """<document>
{whole_document}
</document>
Here is the chunk we want to situate within the whole document:
<chunk>
{chunk_content}
</chunk>
Please give a short succinct context to situate this chunk within the overall document for the purposes of improving search retrieval. Answer only with the succinct context and nothing else."""
def add_context_to_chunk(chunk: Document, whole_document: str, llm) -> Document:
"""为单个块生成情境并拼到内容前,返回新 Document(用于嵌入)。"""
prompt = ChatPromptTemplate.from_messages([("human", CONTEXT_PROMPT)])
chain = prompt | llm
context = chain.invoke({
"whole_document": whole_document,
"chunk_content": chunk.page_content,
}).content.strip()
new_content = f"{context}\n\n{chunk.page_content}"
return Document(page_content=new_content, metadata={**chunk.metadata, "context": context})
# 对同一文档的所有块批量处理(whole_document 可缓存)
def contextualize_documents(chunks: list[Document], whole_document: str, llm) -> list[Document]:
return [add_context_to_chunk(c, whole_document, llm) for c in chunks]
5.3 与向量库、检索链衔接
将上下文化后的文档列表送入向量库并构建 Retriever,即可在 RAG 链中使用;若使用 Cohere 等 Reranker,可用 ContextualCompressionRetriever 包装已有 Retriever,在检索后再做一次基于 query 的压缩/重排。
from langchain_community.vectorstores import Chroma
from langchain_openai import OpenAIEmbeddings
# 假设 docs_contextualized 为上下文化后的 Document 列表
embedding = OpenAIEmbeddings()
vectorstore = Chroma.from_documents(
documents=docs_contextualized,
embedding=embedding,
collection_name="contextual_retrieval",
)
retriever = vectorstore.as_retriever(search_kwargs={"k": 20})
# 可选:与 BM25 融合(需先对 docs_contextualized 建 BM25 索引)
# from langchain_community.retrievers import BM25Retriever
# bm25 = BM25Retriever.from_documents(docs_contextualized, k=20)
# from langchain.retrievers import EnsembleRetriever
# ensemble = EnsembleRetriever(retrievers=[retriever, bm25], weights=[0.5, 0.5])
5.4 小结(LangChain)
- 在 LangChain 中实现上下文检索,本质是在嵌入前用 LLM 为每个块生成情境并改写 Document 的
page_content。 - 可按文档维度组织调用(每文档一份
whole_document),以便接入 Anthropic 等提供的 Prompt Caching,控制成本。 - 与 BM25、Reranker 结合时,对「情境 + 原块」建的索引与向量库一致,检索阶段无需改 query,仅需在构造 Document 时保证写入的是上下文化后的文本。
6. 通过 LlamaIndex 实现上下文检索
LlamaIndex 对 Anthropic 的 Contextual Retrieval 有原生支持:既提供高层封装 DocumentContextExtractor,也提供可直接复现论文流程的 Cookbook 示例(含 Contextual Embedding + Contextual BM25 + Reranker)。下面分两种用法简要说明。
6.1 使用 DocumentContextExtractor(推荐入门)
DocumentContextExtractor 会在文档索引流水线中,为每个节点(chunk) 基于整篇文档调用 LLM 生成一段情境,并将情境写入节点 metadata(默认 key 为 "context");若在构建索引时把「情境 + 正文」一起作为节点的可检索内容,即得到 Contextual Embeddings。需要文档级访问,因此要配合 Docstore:先把完整文档放入 docstore,再在 transformations 里先分块、再跑 Context Extractor。
安装与基础依赖示例:
pip install llama-index llama-index-readers-file llama-index-embeddings-huggingface llama-index-llms-openai
# 若需 BM25 + Reranker:llama-index-retrievers-bm25 llama-index-postprocessor-cohere-rerank
构建带上下文抽取的索引:
from llama_index.core import VectorStoreIndex, StorageContext
from llama_index.core.node_parser import TokenTextSplitter
from llama_index.core.storage.docstore.simple_docstore import SimpleDocumentStore
from llama_index.core.extractors import DocumentContextExtractor
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
from llama_index.llms.openai import OpenAI
# 文档库与嵌入模型
docstore = SimpleDocumentStore()
embed_model = HuggingFaceEmbedding(model_name="BAAI/bge-small-en-v1.5")
llm = OpenAI(model="gpt-4o-mini")
storage_context = StorageContext.from_defaults(docstore=docstore)
# 先加载文档并加入 docstore(DocumentContextExtractor 需要从 docstore 取整文档)
documents = SimpleDirectoryReader(input_dir="./data").load_data()
storage_context.docstore.add_documents(documents)
# 分块 + 为每块生成情境(结果在 metadata["context"])
text_splitter = TokenTextSplitter(separator=" ", chunk_size=256, chunk_overlap=10)
context_extractor = DocumentContextExtractor(
docstore=docstore,
max_context_length=128000,
llm=llm,
oversized_document_strategy="warn",
max_output_tokens=100,
key="context",
prompt=DocumentContextExtractor.SUCCINCT_CONTEXT_PROMPT,
)
index = VectorStoreIndex.from_documents(
documents=documents,
storage_context=storage_context,
embed_model=embed_model,
transformations=[text_splitter, context_extractor],
)
retriever = index.as_retriever(similarity_top_k=20)
DocumentContextExtractor 会把生成的情境写入节点 metadata;若希望嵌入与检索的文本是「情境 + 原块」,需在节点进入向量索引前,将 node.text 设为 metadata["context"] + "\n\n" + node.text,或使用在 Cookbook 中给出的「先得到带 context 的 nodes,再显式用 context+text 建索引」的方式。
6.2 Cookbook 式实现:Contextual Embedding + BM25 + Reranker
LlamaIndex Contextual Retrieval Cookbook 完整复现了「为每块生成情境 → 对情境+块做 Embedding 与 BM25 → 融合后 Rerank」的流程,适合需要 BM25 与 Reranker 的生产配置。核心步骤包括:
- 分块:如
SentenceSplitter(chunk_size=1024, chunk_overlap=200)得到nodes。 - 为每块生成情境:对每个 node,用 LLM + 整文档 + 该块内容,调用与 Anthropic 博客一致的 prompt,将结果写入
node.metadata["context"];然后构造用于检索的文本:contextual_text = metadata["context"] + "\n\n" + node.text,用该文本新建节点或覆盖node.text,得到nodes_contextual。 - Contextual Embedding:对
nodes_contextual建VectorStoreIndex,得到 embedding retriever。 - Contextual BM25:对
nodes_contextual建BM25Retriever.from_defaults(...)。 - 融合 + Rerank:自定义或使用
EnsembleRetriever合并两种检索结果,再通过CohereRerank等取 top-K 送入生成模型。
Cookbook 中使用了 Anthropic 的 prompt caching(在请求头中设置 anthropic-beta: prompt-caching-2024-07-31),同一文档的多个块共享缓存,以降低情境生成成本。
6.3 小结(LlamaIndex)
- 快速落地:用
DocumentContextExtractor+Docstore+VectorStoreIndex.from_documents(..., transformations=[splitter, context_extractor])即可得到「带情境的」索引;注意让嵌入内容包含「情境 + 原块」。 - 完整流程:参考官方 Contextual Retrieval Cookbook,实现 Contextual Embeddings + Contextual BM25 + Reranker,并与评估脚本(如
RetrieverEvaluator、hit_rate / MRR)结合做效果验证。
7. 小结
- 传统 RAG 在分块与编码时容易丢失「这段话属于谁、在什么情境下」的上下文,导致检索 recall 不足。
- Contextual Retrieval 通过在每块前添加由 LLM 生成的块专属情境,再对「情境 + 原块」做 Contextual Embeddings 与 Contextual BM25,可显著降低 top-20 检索失败率(文中约 49%),并结合 Reranking 进一步降至约 67%。
- 实现上依赖「整文档 + 当前块」的短情境生成 prompt,以及 Prompt Caching 控制成本;分块、Embedding 与 Reranker 的选型需结合业务做评估与迭代。
更多推荐


所有评论(0)