Back to guides
Architecture·May 25, 2026·Updated September 2, 2026·14 min read

RAG implementation guide — production retrieval-augmented generation with Claude and Gemini

Build a production RAG system that actually works — not just a 100-line demo. Covers chunking strategy, embedding model choice, retrieval ranking, prompt structure, hallucination control, and how to keep monthly costs in three figures even at 10,000 queries/day.

Last reviewed on .

Most RAG demos die in production. They work on the 10-question demo deck and break the moment users ask anything novel. This guide is the architecture and patterns that hold up: chunking that respects semantic boundaries, hybrid retrieval that catches both fuzzy and literal queries, prompt structure that prevents hallucination, and cost discipline that keeps the bill flat as you scale.

The five decisions that matter

  1. Chunking strategy — affects retrieval quality more than the model
  2. Embedding model — picks the recall ceiling
  3. Retrieval strategy — vector alone is not enough
  4. Prompt structure — decides whether the model hallucinates
  5. Generation model + caching — sets the unit economics

1. Chunking — boring beats clever

Don't overthink this. Recursive character splitter with 1000-1500 characters per chunk and 100-200 character overlap. Tries to break on paragraph boundaries first, then sentences, then words. Stable across domains:

chunk.py
# Recursive character splitter with overlap — the boring choice that wins
from langchain_text_splitters import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(
    chunk_size=1200,
    chunk_overlap=200,
    separators=["\n\n", "\n", ". ", " ", ""],
)
chunks = splitter.split_text(document)
# 1200 chars ≈ 300-400 tokens — fits 5+ chunks in a Sonnet context window
# with room for system prompt + question + answer

Things that look smarter but rarely help: sentence-aware models, markdown-aware splitters, sliding window with embeddings. They're not bad — they're marginally better at huge effort. Spend that effort on retrieval instead.

2. Embedding model — text-embedding-3-large is the default

text-embedding-3-large hits the multilingual + accuracy sweet spot for most production use cases. On Kunavo it's about $0.10/1M tokens — embedding 1M chunks costs ~$25, a rounding error compared to the LLM bill. Re-embed when you change the chunking strategy or the model itself; not when content updates (just add new chunks).

Exceptions where smaller embeddings work: pure English short-text retrieval (FAQs, product cards). For those text-embedding-3-smallis 4x cheaper with negligible quality loss. For multilingual or technical content, stay on large.

3. Retrieval — pure vector loses to hybrid

Vector similarity is great at fuzzy semantic matching ("how do I cancel" → "subscription cancellation policy"). It's terrible at literal terms ("SKU-A92837" or "order #4729"). The fix is hybrid: BM25 keyword search in parallel, then reciprocal rank fusion:

retrieve.py
# Hybrid retrieval: vector similarity + BM25 keyword scoring
# Pure vector misses literal keywords (product SKUs, IDs, dates)
def retrieve(question: str, k: int = 5) -> list[dict]:
    q_embed = embed([question])[0]
    vector_hits = vector_db.similarity_search(q_embed, k=k * 2)
    keyword_hits = bm25_search(question, k=k * 2)

    # Reciprocal rank fusion — simple, robust
    scores: dict[str, float] = {}
    for rank, hit in enumerate(vector_hits):
        scores[hit["id"]] = scores.get(hit["id"], 0) + 1 / (rank + 60)
    for rank, hit in enumerate(keyword_hits):
        scores[hit["id"]] = scores.get(hit["id"], 0) + 1 / (rank + 60)

    top_ids = sorted(scores, key=scores.get, reverse=True)[:k]
    return [chunk_by_id[i] for i in top_ids]

This is the single biggest quality lift after picking a decent embedding model. Track recall@5 on a held-out 100-question eval — if it goes from 70% to 90% after adding BM25, you just removed a third of the "I don't know" failures.

4. Prompt structure — three patterns that prevent hallucination

answer.py
# Final RAG prompt structure — three sections, citations enforced
SYSTEM_PROMPT = """You answer based exclusively on the supplied Context.
- Cite the [doc_id] for each factual claim.
- If the context doesn't answer the question, say "I don't have that
  information" — do not extrapolate or use general knowledge.
- Be concise. No throat-clearing."""

def answer(question: str) -> dict:
    chunks = retrieve(question, k=5)
    context = "\n\n---\n\n".join(
        f"[doc:{c['id']}] {c['text']}" for c in chunks
    )
    resp = client.chat.completions.create(
        model="claude-sonnet-4-6",
        messages=[
            {"role": "system", "content": [{
                "type": "text",
                "text": SYSTEM_PROMPT,
                "cache_control": {"type": "ephemeral"},
            }]},
            {"role": "user", "content": f"# Context\n{context}\n\n# Question\n{question}"},
        ],
        max_tokens=600,
    )
    return {
        "text": resp.choices[0].message.content,
        "sources": [c["id"] for c in chunks],
    }

Three non-negotiables in the system prompt:

  • Cite sources — make the model attach [doc:42]to each claim. If the cited ID isn't real, you caught a hallucination
  • Refuse explicitly — "if context doesn't answer, say I don't have that information." Without this the model completes from world knowledge
  • Concise output — short answers correlate with accurate answers. Max 600 tokens is a good production default

5. Generation model + caching — the cost layer

Claude Sonnet 4.6 is the default. Gemini 2.5 Flash is the cheap fallback if you're cost-pressed. Always wrap the system prompt in cache_control — the system prompt is the same every call, so caching cuts that portion to 10% of the input rate (Anthropic) or 20% (Gemini).

Realistic per-query cost at production scale (5K context, 500 output, caching on):

ModelCost per queryAt 10,000 queries/dayQuality vs Sonnet
claude-sonnet-4-6~$0.007~$70/dayBaseline
claude-haiku-4-5~$0.001~$10/day~85%, 4× cheaper
gemini-2-5-flash~$0.0008Similar, 5× cheaper

At 10,000 queries/day → $70/day on Sonnet, $10/day on Haiku. Pick Sonnet for high-stakes use cases (customer-facing, legal, medical), Haiku for internal/bulk processing.

What breaks in production (and how to catch it)

  • Distribution drift: training corpus stops matching real user questions. Sample 100 queries/week, check recall@5 manually
  • Stale embeddings: source docs updated but embeddings not refreshed. Track index size vs source size weekly
  • Fake citations: model invents doc IDs that look real. Validate every [doc:N] against the actual list of retrieved IDs before rendering
  • Latency cliffs: vector DB blows up past a million vectors. Use HNSW indexing, partition by tenant if multi-tenant

A 4-week production roadmap

  1. Week 1: prototype with 100 docs, 10 test questions, manual eval
  2. Week 2: scale to full corpus, build hybrid retrieval, write 100-question eval set
  3. Week 3: tune chunking + system prompt iterating on the eval, ship to internal users
  4. Week 4: monitoring, cost budget, public release with cite-source UI

FAQ

What chunk size should a production RAG system use?

1,000–1,500 characters per chunk with 100–200 characters of overlap, split with a recursive character splitter that breaks on paragraph boundaries first, then sentences, then words. Sentence-aware models, markdown-aware splitters and sliding windows are marginally better at much higher effort — that effort pays back better spent on retrieval.

Which embedding model should a RAG pipeline use?

text-embedding-3-large is the default for multilingual and technical content. It costs about $0.10 per 1M tokens on Kunavo, so embedding 1M chunks costs roughly $25 — a rounding error next to the LLM bill. For pure English short-text retrieval such as FAQs and product cards, text-embedding-3-small is 4× cheaper with negligible quality loss.

Is vector search alone enough for RAG retrieval?

No. Vector similarity handles fuzzy semantic matching well but fails on literal terms such as a SKU or an order number. Running BM25 keyword search in parallel and merging with reciprocal rank fusion typically moves recall@5 from about 70% to about 90% — the single biggest quality lift after picking a decent embedding model.

How do you stop a RAG system from hallucinating?

Three patterns in the system prompt: require a [doc:N] citation on every factual claim, instruct the model to say it does not have the information when the context does not answer, and keep answers short — max 600 output tokens is a good production default. Then validate every cited id against the retrieved set before rendering; an id that is not real is a caught hallucination.

How much does a production RAG query cost?

At 5K context and 500 output tokens with prompt caching on: about $0.007 per query on Claude Sonnet 4.6, about $0.001 on Claude Haiku 4.5 (4× cheaper, roughly 85% of the quality) and about $0.0008 on Gemini 2.5 Flash. At 10,000 queries a day that is about $70/day on Sonnet and about $10/day on Haiku.