Retrieval-Augmented Generation (RAG)
Chunk, index, retrieve, fuse, rerank, generate with citations and grade each stage. Python from the lessons.
- Concepts and terms
- Tasks and commands
- Annotated code
- Diagrams
- Defaults and numbers
- Troubleshooting
- Questions and tips
End-to-end RAG pipeline#
One query: Pipeline.query#
Tasks in code#
| Task | Code |
|---|---|
| Fixed window chunks | fixed_window(doc_id, |
| Chunk at headings | structural_markdown(doc_id, |
| Score a chunker | eval_recall(chunker, |
| Fuse ranked lists | rrf(rankings, |
| Retrieve through a rewriter | retrieve_with_rewriter(query, |
| Rerank candidates | rerank(model, |
| Graded ranking score | ndcg_at_k(retrieved, |
| Run the gated demo | python3 code/main.py exits 1 on a FAIL |
From 19.64 code/main.py, 19.65 code/main.py, 19.67 code/main.py, 19.68 code/main.py, 19.69 code/main.py
Hybrid retrieval with RRF#
retriever = HybridRetriever()BM25Index + DenseIndexfor d in CORPUS: retriever.add(d)indexes both modalitiesresult = retriever.search(q, k_each=5, k_out=5)result["bm25"], result["dense"], result["fused"]three ranked listsbm25 = BM25Index(k1=1.5, b=0.75)title tokens count 3 timesidf = math.log((n - df + 0.5) / (df + 0.5) + 1.0)stays positivescore[doc.doc_id] += w * (1.0 / (k + rank + 1))rank starts at 0From 19.65 code/main.py
Assemble and query the pipeline#
p = Pipeline()chunker, index, rewriter, rerankerp.ingest(CORPUS)recursive split, target 400p.train_reranker_on(TRAIN_TRIPLES)fits the small cross-encoderresult = p.query("<question>")result.answersentences plus anchorsresult.citationslist of doc_id:chunk_indexresult.rewriter_strategyhyde, multiquery or decomposeresult.latency_msrewrite, retrieve, rerank, generateFrom 19.69 code/main.py
Defaults and numbers#
| Setting | Default | Note |
|---|---|---|
BM25 k1 | 1.5 | Lesson 11.07 code uses 1.2 |
BM25 b | 0.75 | 0 ignores length, 1 full normalization |
RRF k | 60 | Rank 1 adds 1/61 |
| Chunk size | 256-512 | Tokens, with 50-token overlap |
| Chunks per query | 5 to 10 | 2,500-5,000 tokens of context |
| Rerank pool N | 50 to 200 | Cross-encoder keeps the top K |
top_n, top_k | 12 | Pipeline keeps 5 after rerank |
REFUSE_THRESHOLD | 0.05 | Below it the answer is "I do not know." |
From 19.65 code/main.py, Lesson 19.65, Lesson 11.06, Lesson 11.07, Lesson 19.66, 19.69 code/main.py
Chunking strategies#
| Command | What it does |
|---|---|
recursive_split | Prose without structure. Strongest single baseline |
structural_markdown | Markdown, RFC and API docs. Cuts at headings |
sentence_chunks | Long single-topic paragraphs. Target 500 |
semantic_chunks | Short mixed-topic paragraphs. Threshold 0.6 |
fixed_window | A control only. Cuts mid-symbol in code |
| Parent-document | Search small children, return the parent. Dedupe parents |
| Contextual retrieval | Prepend 50-100 words of LLM context to each chunk |
| Late chunking | Embed the whole doc per token, then pool per chunk |
Key formulas#
- Cosine
- \cos(a, b) = \frac{a \cdot b}{\lVert a \rVert \, \lVert b \rVert}, from -1 to 1.
- BM25 term
- \mathrm{IDF}(t) \frac{tf \, (k_1 + 1)}{tf + k_1 (1 - b + b \, |d| / \mathrm{avgdl})}
- RRF
- \mathrm{RRF}(d) = \sum_R \frac{1}{k + \mathrm{rank}_R(d)}
- Precision@k
- gold docs in the top k, divided by k.
- Recall@k
- gold docs in the top k, divided by all gold docs.
- MRR
- mean of 1 / rank of the first relevant doc.
- DCG gain
- \frac{2^{rel} - 1}{\log_2(i + 2)} for position i from 0.
- nDCG@k
- DCG divided by the DCG of the ideal order. From 0 to 1.
From Lesson 11.06, Lesson 11.07, Lesson 19.68, 19.68 code/main.py
Troubleshooting#
| Symptom | Cause | Fix |
|---|---|---|
| Low recall@k and precision@k | Chunk cut the answer, or retriever misses it | Fix boundaries or add a modality |
| Good recall@k, low MRR | Right chunk not at position 1 | Add a cross-encoder reranker |
| High MRR, low faithfulness | Generator invents content | Force cite or refuse in the prompt |
| High faithfulness, low relevance | Grounded but off-topic answer | Rewrite the query |
Exact code E-4021 missed | Embedding treats it as noise | Add BM25 and fuse with RRF |
| Rerank shows no lift | N equals K | Set N to at least 3 times K |
| HyDE drops the right doc | Invented identifier in the passage | Cap length, weight BM25 lower |
| 40-token fragments | Semantic chunks without a floor | Enforce min_tokens |
From Lesson 19.68, Lesson 11.07, Lesson 19.66, Lesson 19.67, Lesson 05.23
Abbreviations#
- RAG
- Retrieval-Augmented Generation. Retrieve, put in the prompt, generate.
- BM25
- Best Matching 25. Keyword ranking with IDF and length normalization.
- RRF
- Reciprocal Rank Fusion. Sums 1/(k + rank) over ranked lists.
- HyDE
- Hypothetical Document Embeddings. Search with a fake answer.
- MRR
- Mean Reciprocal Rank. Mean of 1/rank of the first hit.
- nDCG
- Normalized Discounted Cumulative Gain. Graded ranking quality.
- Qrels
- gold labels. Queries with gold docs and answers.
- Bi / cross-encoder
- embeds query and doc apart, or reads them together.
Questions#
- Why RRF, not score mixing?
- BM25 scores are unbounded. Ranks compare across modalities.
- Why rerank only the top N?
- A cross-encoder is 100-1000x slower than a bi-encoder.
- Why quote recall@k?
- The generator can drop a bad chunk. It cannot use a chunk it never saw.
- Why does a wrong HyDE answer help?
- The retriever uses its words, not its facts.
- RAG or fine-tuning?
- RAG for facts. Fine-tuning for style, tone or a reasoning pattern.
From Lesson 19.65, Lesson 11.07, Lesson 19.68, Lesson 19.67, Lesson 11.06
Tips#
- Start with recursive 512 tokens. Measure recall@5 on a 50-query eval set.
- Tell the model to use only the context and to say "I don't know."
- Pin the bi-encoder, cross-encoder and N together.
- Judge with a different model family than the generator.
- Slice qrels by query class: literal, paraphrased, multi-topic.
- Tier the eval: 20 queries in CI, 200 nightly, 2000 weekly.