IntermediateRAG & VectorsFree Guide

RAG Pipeline Explained — Retrieval-Augmented Generation

RAG grounds LLM answers in your own knowledge base by retrieving relevant document chunks at query time — making it the go-to architecture for enterprise AI.

What is it?

Retrieval-Augmented Generation (RAG) is an architecture that grounds LLM responses in a specific knowledge base by retrieving relevant documents at query time and injecting them into the prompt context. Instead of relying on parametric knowledge (memorized during training), the LLM reads retrieved text to formulate its answer — like an open-book exam instead of a closed-book one.

How it works

RAG has two phases:

  1. INDEXING (offline): Documents → chunk into 256–512 token pieces with 10–15% overlap → embed each chunk with an embedding model (e.g. text-embedding-3-small, 1536-dim vectors) → store vectors + metadata in a vector database (Pinecone, Chroma, pgvector, Weaviate).

  2. RETRIEVAL + GENERATION (online, per query): User query → embed with same model → cosine similarity search against stored vectors → retrieve top-k chunks (k=3–10) → assemble context → send to LLM with retrieved context in the prompt.

Critical: the embedding model used for indexing and querying MUST be identical — switching models invalidates the entire index.

Reranking: a cross-encoder reranker (e.g. Cohere Rerank) can rescore retrieved chunks for relevance before generation. Adds ~100ms latency but improves answer quality significantly.

Hybrid search combines dense (embedding) and sparse (BM25 keyword) retrieval. Dense catches semantic matches ('car' ↔ 'automobile'); sparse catches exact matches and rare terms. Reciprocal Rank Fusion (RRF) merges the two result lists.

Code example

from anthropic import Anthropic import chromadb client = Anthropic() vdb = chromadb.Client() collection = vdb.get_collection("docs") def rag_query(question: str) -> str: results = collection.query(query_texts=[question], n_results=3) context = "\n\n".join(results["documents"][0]) response = client.messages.create( model="claude-opus-4-5", max_tokens=512, messages=[{"role": "user", "content": f"Context:\n{context}\n\nQuestion: {question}\nAnswer based only on the context above."}] ) return response.content[0].text

Key concepts

document chunkingvector embeddingscosine similarity searchtop-k retrievalrerankinghybrid search (dense + sparse)Reciprocal Rank Fusion

Common mistakes

Using different embedding models for indexing vs querying. If you index with text-embedding-ada-002 and later switch to text-embedding-3-small, the vector spaces are incompatible — all retrieved results will be wrong.

Chunk size too large (>1024 tokens). A 2000-token chunk has an embedding that averages over all its content — the signal for any specific sentence gets diluted.

No overlap between chunks. If a key answer spans the boundary between chunk 3 and chunk 4, neither chunk alone contains the full answer. Use 10–15% overlap.

Trusting retrieval without verifying faithfulness. The LLM can still hallucinate content that is not in the retrieved chunks — instruct the model explicitly: 'Answer ONLY from the provided context.'

Not filtering low-similarity results. Always set a minimum similarity threshold (0.70–0.75 cosine) and tell the LLM 'no relevant documents found' when nothing passes the threshold.

Interview questions on this topic

Explain the difference between HNSW and IVF vector indexes. What are the speed, memory, and recall tradeoffs?

Practice answering these with structured learning → Start on CrackLab

Continue learning — explore the full AI Engineering curriculum

30+ structured topics from Python fundamentals to deploying production LLM systems. Free to start.

See all topics

Related free guides