Most RAG systems are built without instrumentation and tuned by anecdote. This playbook builds the opposite: a deliberately simple pipeline with retrieval metrics wired in from the first query — and, before that, a measured decision on whether long-context stuffing beats retrieval for your corpus at your query volume.
Tier: Core · Workflow area: RAG & Retrieval Engineering
Every later retrieval upgrade (pb-22 through pb-26) is judged against this baseline.
langchain or llama-index (either), pgvector/qdrant/faiss (all free/self-hostable), ragas, sentence-transformers.count_tokens over a sample × doc count) — needed for the stuffing-vs-RAG math.Decision gate: long-context vs retrieval. Compute three numbers: corpus tokens, expected queries/month, freshness cadence. Then run the comparison empirically on 20 questions:
Rules of thumb the numbers usually confirm: corpora under ~200k tokens with low query volume → stuffing + caching wins on simplicity and often accuracy; large corpora, high QPS, per-user ACLs, or minute-level freshness → retrieval wins on cost and latency. Cache-read pricing (~0.1×) changes the arithmetic — model it, don't assume. Record the decision and the numbers; revisit quarterly.
Build the eval set before the pipeline. For each of the 30–50 questions, record: the answer (or key facts), and the source passages that support it (doc ID + span). This grounding set powers retrieval metrics forever after.
Assemble the naive baseline. Deliberately boring: recursive character splitting at ~450 tokens with 15% overlap (the strong 2026 default — pb-22 tunes this properly), bge-small-en-v1.5 embeddings, cosine top-5, answer with a cited-generation prompt. Resist every upgrade urge; upgrades come later, measured.
Instrument retrieval separately from generation. Log, per query: retrieved chunk IDs + scores, and compute against the grounding set:
# recall@k: did we retrieve any gold passage?
recall_at_k = mean(any(c in gold[q] for c in retrieved[q][:k]) for q in questions)
# MRR: how high did the first gold passage rank?
Retrieval recall@5 is the ceiling on your whole system — if the passage isn't retrieved, no generation model can save you.
Score end-to-end with RAGAS. Component metrics tell you where it breaks:
from ragas import evaluate
from ragas.metrics import faithfulness, answer_relevancy, context_precision, context_recall
from datasets import Dataset
ds = Dataset.from_dict({"question": qs, "answer": answers,
"contexts": contexts, "ground_truth": truths})
report = evaluate(ds, metrics=[faithfulness, answer_relevancy,
context_precision, context_recall])
Use a cheap-tier model as the RAGAS judge and pin its version. Read the four numbers as a diagnosis: low context_recall → retrieval problem (pb-22/23/24); high recall + low faithfulness → generation/grounding problem; low context_precision → you're retrieving noise.
Freeze the baseline scorecard. One table: recall@5, MRR, the four RAGAS metrics, p95 latency, cost/query. This is the number every subsequent change must beat — commit it next to the code.
"Instrumented from day one" means the eval set exists before the pipeline does. Retrofitting metrics onto a tuned system tells you where you ended up, never which changes mattered.
claude-opus-4-8) with cache_control on the corpus prefix; verify cache hits via usage.cache_read_input_tokens before trusting the cost model.