A chunk that reads "the penalty rises to 10% in that case" embeds terribly — which penalty, which case, which contract? Contextual retrieval fixes this at index time: for each chunk, an LLM writes a short context sentence situating it within its source document, and you embed context + chunk together.
Tier: Advanced · Workflow area: RAG data pipelines
In Anthropic's published measurements, contextual embeddings alone cut top-20 retrieval failures by roughly 35% (and combined with BM25 and reranking, the stack reached ~67%). The catch is cost — one LLM call per chunk — which batch processing and prompt caching turn from prohibitive to routine. This playbook builds the enrichment stage and proves the lift on your golden set.
Baseline first. Record current golden-set fail@20 from your pb-33/pb-34 pipeline. Contextual enrichment is an increment; without the baseline you cannot prove it earned its token bill.
Write the enrichment prompt so the whole document is cacheable. The pattern: full document first (cached across all its chunks), then the chunk, then the instruction. With prompt caching, the document tokens are paid once per document, not once per chunk — this is what makes the technique affordable.
Generate contexts with structured output. Local-first note: enrichment quality tracks model quality, so this is the one pipeline stage where a frontier hosted model is usually worth it; a strong local model via Ollama/vLLM is the fallback for restricted data.
import anthropic
from pydantic import BaseModel
class ChunkContext(BaseModel):
context: str # 1-2 sentences situating the chunk in the document
client = anthropic.Anthropic()
def enrich(document: str, chunk: str) -> str:
response = client.messages.parse(
model="claude-opus-4-8",
max_tokens=200,
thinking={"type": "adaptive"},
messages=[{
"role": "user",
"content": [
{"type": "text", "text": f"<document>\n{document}\n</document>",
"cache_control": {"type": "ephemeral"}},
{"type": "text", "text":
f"<chunk>\n{chunk}\n</chunk>\n"
"Write 1-2 sentences situating this chunk within the document "
"(what document, which section/party/product, what period). "
"Answer with only the context."},
],
}],
output_format=ChunkContext,
)
return response.parsed_output.context
Expected output per chunk: something like "From the 2025 Master Services Agreement between Acme and Beta Corp, Section 7 (Late Delivery), covering penalty escalation terms." — short, factual, front-loadable.
Run it as a batch job on the pb-30 rails. Enrichment is embedding-generation's twin: same work-queue table (add context and enriched_at columns), same idempotent re-runs, same DLQ. For full-corpus passes, submit through the provider's batch API (~50% discount, hours-scale turnaround) rather than synchronous calls. Pre-compute the cost from token counts before launching: docs × doc_tokens (cached rate) + chunks × (chunk_tokens + ~60 output tokens).
Embed context + "\n" + chunk into a shadow collection. Store the original chunk text for display and the enriched text only as the embedding input (and in the BM25 leg too — contextual BM25 is part of the published gains). Never overwrite the live index; this is a versioned cutover (pb-31 model_version discipline, pb-36 mechanics).
Measure the lift. The metric that proves this step worked: golden-set fail@20 (and recall@5 post-rerank), enriched versus baseline, same queries, same pipeline otherwise.
Expected shape:
baseline hybrid+rerank : fail@20 = 6.0% · recall@5 = 0.84
contextual + same : fail@20 = 4.0% · recall@5 = 0.89
Also spot-audit 30 generated contexts against their source documents for factual drift — a context that misdescribes the chunk poisons retrieval more efficiently than no context at all.
Enrichment output is LLM-generated data entering your index — treat it as untrusted input. Validate length (reject > 3 sentences), reject contexts referencing entities absent from the document, and sample-audit every batch; a hallucinated context is an index-poisoning bug with a long half-life.
Enrichment sends full documents to the LLM — the maximum-exposure operation in the entire RAG pipeline — so for client data it requires explicit written approval covering the provider, retention terms, and document classes, with ACL-restricted material excluded from the batch. Where approval isn't granted, run the same prompt through a client-approved or self-hosted model (vLLM/Ollama with a strong open-weights model) inside their boundary and accept a smaller lift, measured by the same shadow-index eval. Either way, the generated contexts become part of the client's index and inherit its access controls, and the pre-launch cost estimate should be signed off — a full-corpus enrichment pass is the largest single LLM spend most RAG projects ever make.