The five most common design mistakes in RAG systems, why they hurt, and what to do instead.
What it looks like: Chunks set to 100–200 tokens because "smaller = more precise retrieval."
Why it hurts:
The fix: Start at 512 tokens with 10% overlap. Measure Recall@5. Only reduce if you find evidence that large chunks are diluting relevance with unrelated material.
# Anti-pattern
chunks = split_by_tokens(document, chunk_size=128, overlap=0)
# Better
chunks = split_by_tokens(document, chunk_size=512, overlap=64)
# Even better: use semantic boundaries
chunks = split_by_paragraph_or_section(document, max_tokens=512)
What it looks like: Wrapping every RAG call in a ReAct loop because "agents are more powerful."
Why it hurts:
The fix: Measure Recall@5 and answer accuracy on a probe set. If Naive RAG already achieves 0.85+ recall and 0.80+ answer accuracy, don't add an agent. Agents are for cases where retrieval scope is unknown at query time — if you know the retrieval pattern, encode it in a static or modular system.
Decision: Do you need an agent?
Is the retrieval scope unknown at query time? ──► No ──► Don't use an agent.
Is the query multi-hop and the hops are conditional on retrieved content? ──► No ──► Don't use an agent.
Does the answer require comparing results across many separate retrievals? ──► No ──► Use fan-out instead.
──► Yes ──► Use an agent.
What it looks like: Adding a cross-encoder reranker to every retrieval call, regardless of query type.
Why it hurts:
The fix: Add reranking only when dense retrieval is measurably the bottleneck. Signs you need a reranker:
# Anti-pattern: always rerank
def retrieve(query):
results = vector_db.search(query, k=5)
return reranker.rerank(query, results, top_k=5) # wastes 400ms when k is already small
# Better: rerank only when initial pool is large
def retrieve(query, use_reranker=False):
k_initial = 20 if use_reranker else 5
results = vector_db.search(query, k=k_initial)
if use_reranker:
return reranker.rerank(query, results, top_k=5)
return results
What it looks like: Storing all tenants' documents in the same vector namespace with metadata.tenant_id filtering.
Why it hurts:
tenant_id can return too few results after filteringtenant_id filter exposes all tenants' dataThe fix: Per-tenant namespace isolation for different-sensitivity tenants; per-tier namespace isolation for same-sensitivity with different SLAs.
# Anti-pattern: shared index with filter
def retrieve_naive(query, tenant_id):
return vector_db.search(
query,
k=5,
filter={"tenant_id": {"$eq": tenant_id}}, # dangerous if omitted
)
# Better: per-tenant namespace
def retrieve_safe(query, tenant_id):
return vector_db.search(
query,
k=5,
namespace=f"tenant:{tenant_id}", # server-side isolation, can't be omitted
)
What it looks like: Deploying RAG changes (new chunking, new embedding model, new reranker) without a golden eval set.
Why it hurts:
The fix: Maintain a labeled probe set of 50–200 queries with known relevant passages. Run it in CI before every deployment. Block if Recall@5 drops more than 2%.
# Minimum viable eval harness
def run_eval(retrieval_fn, probe_set: list[dict], k: int = 5) -> dict:
recall_hits = 0
for sample in probe_set:
results = retrieval_fn(sample["query"], k=k)
result_ids = {r["doc_id"] for r in results}
if sample["relevant_doc_id"] in result_ids:
recall_hits += 1
recall_at_k = recall_hits / len(probe_set)
return {"recall_at_k": recall_at_k, "k": k, "n_queries": len(probe_set)}
# In CI:
# metrics = run_eval(new_retrieval_fn, PROBE_SET)
# assert metrics["recall_at_k"] >= BASELINE_RECALL - 0.02, f"Recall regression: {metrics}"
| Anti-Pattern | Root Cause | Fix |
|---|---|---|
| Over-chunking | "Smaller = better" without measurement | Start at 512 tokens; measure first |
| Premature agentification | "Agents are powerful" without need | Measure if you need multi-hop first |
| Reranking everything | "Rerankers improve quality" always | Only add when dense is the bottleneck |
| One index for all tenants | Simplicity over security | Per-tenant namespace isolation |
| No probe set | "We'll know if it's bad" | 50-query golden set in CI before day 1 |