Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Design production retrieval-augmented generation systems — the full ingest→chunk→embed→index→retrieve→rerank→assemble→generate pipeline, with concrete numbers on chunking, embeddings, hybrid search, reranking, query processing, indexing ops, context assembly, and the failure modes that wreck precision.
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-02 | ✗→✓ | ▲ Improved | 103% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 238% | 0% |
| case-10 | ✗→✓ | ▲ Improved | 374% | 0% |
| case-12 | ✗→✓ | ▲ Improved | 316% | 0% |
| case-13 | ✗→✓ | ▲ Improved | 237% | 0% |
RAG is search + prompt, not magic. The model can only answer from what retrieval puts in the context window, so 80% of RAG quality is the retrieval system and 20% is the prompt. Most "the LLM hallucinated" bugs are actually "retrieval returned garbage and the model dutifully summarized it." Build the pipeline so the right chunks land in context with citations, and tell the model to say "I don't know" when they don't. This skill is the concrete playbook: numbers, methods, tables, and the failure modes that cost precision.
Index time (offline, batch): ingest → chunk → embed → index. Query time (online, latency-bound): retrieve → rerank → assemble → generate.
| Stage | Job | Latency budget (typical) | |---|---|---| | Ingest | Parse source → clean text + metadata | offline | | Chunk | Split into retrievable units | offline | | Embed | Text → vector | offline (docs), ~10–50ms (query) | | Index | Store vectors + metadata for ANN | offline | | Retrieve | top-k candidates (dense + sparse) | 10–50ms | | Rerank | Cross-encoder reorders k→n | 50–300ms | | Assemble | Select, dedup, order, budget, cite | <5ms | | Generate | LLM answers from context | 1–10s (dominates) |
Do treat each stage as independently measurable and swappable. Don't optimize generation prompts before you've measured retrieval recall — you're polishing the 20%.
When RAG vs alternatives: RAG when knowledge is large, changing, private, or needs citations (docs, tickets, codebases, policies). Long-context stuffing when the whole corpus fits the window and is small/static — simpler, no index. Fine-tuning to teach style/format/behavior, not facts — fine-tuning bakes knowledge in stale and unverifiable; it's the wrong tool for "answer from current documents." Tool/SQL calling when the answer is a precise lookup or computation over structured data, not fuzzy text. Most production systems combine RAG (facts) + light fine-tuning or few-shot (format) + routing (§7) to the right one per query.
A chunk is the atomic unit of retrieval. Chunk wrong and the answer is split across two chunks that never co-retrieve, or buried in a 2000-token wall of noise. There is no universal best size; it's a recall/precision tradeoff against your content and embedding model's context window.
| Strategy | How | Use when | Cost | |---|---|---|---| | Fixed-size | N tokens, hard cut | Uniform prose, fast baseline | Splits mid-sentence/mid-table | | Recursive | Split on ¶→sentence→word, respecting separators | General default (most corpora) | Slightly more compute | | Semantic | Split where embedding similarity between adjacent sentences drops below a threshold | Topic-dense docs, mixed subjects | Embedding cost at index time | | Structural | Split by Markdown heading / HTML section / AST function | Docs, code, anything with structure | Needs a parser per format |
Size + overlap (start here, then tune):
Metadata per chunk (index this, filter and cite on it): source_id, doc_title, section_heading, url, page, created_at, updated_at, version, author/acl. Filtering on metadata at retrieval (WHERE tenant_id = X AND updated_at > …) is often a bigger precision win than any embedding tweak.
Parent-document / small-to-big: embed small chunks (high precision match) but return the parent (the section/page they belong to) to the LLM (full context). Index child→parent pointers; retrieve on children, hydrate parents at assembly. Best default for docs.
Contextual retrieval (Anthropic, 2024): before embedding each chunk, prepend a 50–100 token LLM-generated blurb situating it in the whole doc ("This chunk is from the Q3 2023 10-K, discussing revenue recognition…"). Anthropic reports it cuts failed retrievals ~35%, ~49% combined with BM25, ~67% combined with reranking. One-time cost; with prompt caching the per-chunk LLM cost is small (~$1/M tokens). Worth it whenever chunks lose meaning out of context (most do).
Worked sizing example (support-doc corpus, answers are 1–3 sentences):
Failure mode: the #1 RAG bug is bad chunking that splits one answer across two chunks — neither chunk alone scores high enough, so the model gets half an answer and confabulates the rest. Fix with overlap, structural splits, or small-to-big.
Ingest hygiene (before chunking): strip nav/boilerplate/HTML chrome, normalize whitespace, OCR or layout-parse PDFs (tables and multi-column break naive text extraction), and keep tables/code as intact units. Garbage in ingest is garbage in every downstream stage — and it's invisible until eval. Dedup identical docs at ingest, not at query time.
The embedding model turns text into the vector whose nearest neighbors are "relevant." Choosing it is choosing your recall ceiling.
Selection criteria:
Normalization: L2-normalize vectors so cosine similarity = dot product (faster, and most ANN indexes assume it). Do it once at index time and at query time. Mismatched normalization between index and query silently tanks recall.
Query vs document asymmetry: queries ("how do I reset my password?") and documents ("Password reset is available under Settings…") have different shapes. Asymmetric models (e5, BGE, instructor) want a prefix — query: vs passage:, or an instruction. Using the wrong prefix, or none, degrades recall measurably. Read the model card; this is the most common silent misconfiguration.
Cost: hosted ~$0.02–0.13 / 1M tokens (OpenAI text-embedding-3, Cohere, Voyage). Self-hosted (BGE, e5, Nomic, GTE) = GPU/CPU time, zero per-call. At millions of docs, self-hosting often wins; at low volume, hosted is simpler.
| Model | Dim | Context | Host | Notes | |---|---|---|---|---| | OpenAI text-embedding-3-small | 1536 (MRL-truncatable) | 8191 | hosted | Cheap default, strong general | | OpenAI text-embedding-3-large | 3072 (truncatable) | 8191 | hosted | Higher quality, 2× cost | | Cohere embed-v3 | 1024 | 512 | hosted | Strong multilingual, query/doc input types | | Voyage voyage-3 | 1024 | 32k | hosted | Long-context, code/finance variants | | BGE-M3 | 1024 | 8192 | self | Dense+sparse+multivector in one, multilingual | | e5-large-v2 | 1024 | 512 | self | Needs query:/passage: prefixes | | nomic-embed-text-v1.5 | 768 (MRL) | 8192 | self | Open, long-context, task prefixes |
Pick by your retrieval-task MTEB score on your data, then weigh dim/context/host against it — never by headline average alone.
Embedding drift — the upgrade trap: vectors from model v1 and model v2 live in different, incomparable spaces. You cannot mix them in one index. Upgrading the embedding model means re-embedding the entire corpus and rebuilding the index — a batch job, not a config flip. Plan for it: version your embeddings, keep raw text as source of truth, run old+new indexes in parallel during cutover, A/B before flipping. Failure mode: someone bumps the model name, new docs embed with v2, queries embed with v2, but old docs are still v1 — recall silently collapses for everything indexed before the change.
Exact nearest-neighbor (compare query to every vector) is O(N) — fine to ~100k vectors, death at millions. Approximate nearest neighbor (ANN) trades a few % recall for 10–1000× speed.
| Index | How | Strength | Tradeoff | |---|---|---|---| | HNSW | Navigable small-world graph, layered | Best recall/latency, default everywhere | High memory; slow/awkward deletes | | IVF | Cluster (k-means), search nearest cells | Lower memory, fast build | Needs training; tune nprobe for recall | | IVF-PQ | IVF + product quantization (compress vectors) | Massive corpora, low memory | Quantization loses precision | | Flat (exact) | Brute force | 100% recall, ground truth for eval | O(N), only small sets |
Tune the recall/latency knobs: HNSW ef_search (higher = better recall, slower) and M (graph degree); IVF nprobe (cells probed). Always benchmark recall@k against a Flat index on a held-out query set — don't guess.
Metadata filtering is non-negotiable in production (tenant isolation, ACLs, date ranges, doc type). Pre-filter (restrict candidate set before ANN — accurate but can be slow if filter is selective and fights the graph) vs post-filter (ANN then drop non-matching — fast but may return <k after filtering). Good vector DBs do filtered HNSW. Verify your DB doesn't silently degrade to post-filter and return too few results.
Vector DB choice: pgvector (you already run Postgres — start here; HNSW + SQL filters + transactions in one place), Qdrant / Weaviate / Milvus (purpose-built, scale + hybrid built in), Pinecone / Turbopuppy / managed (zero-ops, pay per vector), or Elasticsearch/OpenSearch (you already run it for search). Do start with pgvector unless you have >10M vectors or need built-in hybrid. Don't adopt a new infra component before you've outgrown the database you already operate.
Dense (semantic) retrieval matches meaning — great for paraphrase, synonyms, intent. It fails on exact tokens: product IDs (SKU-4417), error codes (ERR_0x80), acronyms, names, rare jargon, version numbers — because they barely move a semantic embedding. Sparse (lexical, BM25) matches exact terms — great for those, useless for paraphrase.
Run both, fuse. The standard fusion is Reciprocal Rank Fusion (RRF): score(d) = Σ 1/(k + rank_i(d)) over each retriever's ranked list, k≈60. RRF uses ranks, not raw scores, so it needs no score normalization across incompatible scales — robust and near-parameter-free.
dense_results = vector_search(query, top_k=50) # semantic
sparse_results = bm25_search(query, top_k=50) # lexical
fused = rrf(dense_results, sparse_results, k=60) # → top 50 by fused rank
# then rerank fused top-50 → top-n (§6)Why hybrid wins: dense-only quietly misses every query that hinges on an exact string; lexical-only misses every paraphrase. Hybrid covers both and consistently beats either alone on real, messy query mixes. Failure mode: dense-only RAG that "works in the demo" then fails the moment a user pastes an error code or part number — the answer exists in a chunk, but no semantic neighbor surfaced it.
Bi-encoders (your embedding model) encode query and doc separately — fast, but they never see them together. A cross-encoder reranker feeds [query, doc] jointly through a transformer and scores relevance with full cross-attention. Far more accurate, far too slow to run over the whole corpus — so you run it on the top-k candidates only.
candidates = hybrid_search(query, top_k=50-100) # cheap, high recall
reranked = cross_encoder.rank(query, candidates) # expensive, high precision
context = reranked[:5-10] # top-n to the LLMms-marco-MiniLM-L-6-v2 for a fast local baseline).The funnel, with numbers: retrieve k=50–100 (cheap, ANN, ~20ms) → rerank to n=3–10 (cross-encoder, ~100ms) → generate (~3s). Widening k costs the retriever almost nothing but gives the reranker more chances to surface a buried gem; the reranker is what makes a wide-but-noisy candidate set safe to pass downstream. Tune k up until reranked precision stops improving, then stop.
The user's raw query is often a bad search query (vague, multi-part, full of pronouns, or just badly phrased). Transform it first.
| Technique | What | When | |---|---|---| | Rewriting | LLM cleans/normalizes the query; resolves "it/that" from chat history into standalone form | Conversational RAG (mandatory — "what about the second one?" is unsearchable raw) | | Expansion | Add synonyms / related terms | Sparse retrieval, jargon mismatch | | HyDE | LLM writes a hypothetical answer, embed that (answers look like docs, not questions), search with it | Zero-shot / asymmetric mismatch; costs one LLM call | | Multi-query | LLM generates 3–5 query variations, retrieve each, union + dedup | Improves recall on ambiguous queries | | Decomposition | Split a compound question into sub-questions, retrieve per sub, combine | Multi-hop ("compare A's revenue to B's") | | Routing | Classify query → pick index/tool/filter (docs vs code vs SQL vs "no retrieval needed") | Multiple sources; avoid retrieving when the model already knows |
Conversational rewrite example (why it's mandatory):
History: "How do I export my data?" → "It's under Settings → Export."
User: "Can I schedule that?"
Raw embed of "Can I schedule that?" → matches calendars, meetings — wrong.
Rewritten: "Can I schedule a recurring data export in Settings?" → correct chunks.Do always rewrite in multi-turn chat — follow-ups are unsearchable without history. Don't stack every technique blindly; each adds latency and LLM cost. Add one, measure recall, keep if it helps.
A RAG index is not write-once. Sources change; the index must track them or it serves stale, deleted, or wrong content.
doc_id. On change, delete all old chunks for that doc, then re-chunk and re-insert — don't append (you'll serve both versions). Diff by content hash to skip unchanged docs.indexed_at; for time-sensitive corpora set a TTL and re-index or expire. Surface document age to the model so it can hedge on old data.| Change cadence | Strategy | Mechanism | |---|---|---| | Static / rarely (policies, manuals) | Full rebuild on release | CI job, versioned index | | Daily–weekly (docs, KB) | Scheduled incremental | Cron + content-hash diff upsert | | Minutes (tickets, chat, prices) | Event-driven real-time | CDC / webhook → queue → upsert worker | | Deletes (any cadence) | Tombstone + compaction | Soft-delete by doc_id, scheduled compaction |
Failure mode: stale index — a doc was updated or deleted in the source weeks ago, but its old chunks still retrieve, so RAG confidently cites information that no longer exists. No prompt fixes this; the indexing pipeline must.
Retrieval found candidates; assembly decides what enters the prompt and in what order. This stage is cheap to run and easy to get wrong.
source, title, url, section alongside each chunk and instruct the model to cite by ID. Citations are how users (and your evals) verify the answer wasn't hallucinated — non-negotiable for trust.system + query + (n chunks) + reserved output must fit the window with margin. Budget explicitly; truncate by reranked relevance, never by arbitrary cutoff. Leave room for the answer.Generation prompt — make grounding and abstention explicit:
Answer ONLY from the <context> below. Each fact must trace to a
chunk; cite its [id]. If the context does not contain the answer,
reply exactly: "I don't have that in my sources." Do not use prior
knowledge. Do not guess.
<context>
[doc-12 §Billing] Refunds are processed within 5–7 business days...
[doc-08 §Billing] To request a refund, open Settings → Billing...
</context>
Question: {query}The instruction to abstain is what converts weak retrieval into an honest "I don't know" instead of a confident fabrication. Pair it with the §9 relevance threshold so the empty-context branch actually fires.
Failure modes here: (1) retrieving irrelevant chunks and the model hallucinates an answer from them — looks grounded, is wrong; (2) no "I don't know" path, so empty/weak retrieval still produces a fabricated answer; (3) lost-in-the-middle burying the one good chunk in position 6 of 10.
| Pattern | Answers | Build cost | Query cost | Reach for it when | |---|---|---|---|---| | Flat hybrid+rerank | "What does the doc say about X?" | Low | 1 retrieval | Default — start here | | Graph RAG | "How are A and B connected?" / global summary | High (entity extraction) | Traverse + vector | Relationships > passages | | Agentic RAG | Ambiguous, gap-filling, iterative | Medium | Many LLM calls | Query needs reflection/retry | | Multi-hop | "CEO of the firm that bought X" | Medium | k retrievals chained | Chained evidence required |
Do exhaust hybrid + rerank + good chunking before adding graph/agentic complexity — they fix most failures at a fraction of the cost. Don't build Graph RAG because it's fashionable; build it because your questions are relational.
RAG fails silently — a wrong answer looks identical to a right one. Build evals before tuning. Measure retrieval and generation separately so you know which half is broken.
| Symptom | Root cause | Fix | |---|---|---| | Answer is half-right / cut off | Chunking split it across chunks | Overlap, structural/small-to-big chunks | | Misses exact IDs, codes, acronyms | Dense-only retrieval | Add BM25 → hybrid + RRF (§5) | | Relevant doc retrieved but ranked low | No reranking | Add cross-encoder rerank (§6) | | Cites deleted/outdated content | Stale index | Tombstones, re-index, source sync (§8) | | Confident answer, wrong facts | Irrelevant context forced in | Relevance threshold + "I don't know" (§9) | | Always answers, even with no data | No abstention path | Instruct + threshold to say "I don't know" | | Recall collapsed after model upgrade | Embedding drift (mixed vector spaces) | Re-embed whole corpus, rebuild index (§3) | | Good chunk ignored by model | Lost in the middle | Reorder most-relevant first/last (§9) | | Conversational follow-ups fail | Raw query not standalone | Query rewriting with history (§7) |
The one rule: retrieval quality is the ceiling on RAG quality. Measure it, make it hybrid, rerank it, keep it fresh, and let the model abstain when it's empty.
Other measured skills in the registry, with their headline benchmark lift.