← Back to Index

Pattern: RAG Anti-Patterns

The five most common design mistakes in RAG systems, why they hurt, and what to do instead.


Anti-Pattern 1: Over-Chunking

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)

Anti-Pattern 2: Premature Agentification

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.

Anti-Pattern 3: Reranking Everything

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

Anti-Pattern 4: One Index for All Tenants

What it looks like: Storing all tenants' documents in the same vector namespace with metadata.tenant_id filtering.

Why it hurts:

The 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
    )

Anti-Pattern 5: No Probe Set = Flying Blind

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}"

Summary Table

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