RAG Systems: A Deep Dive into Retrieval-Augmented Generation
A comprehensive guide to RAG systems — covering architecture, embedding models, vector databases, retrieval strategies, reranking, chunking, evaluation, and production deployment

Contents
- What is RAG and Why Does It Matter?
- The RAG Architecture — An End-to-End Overview
- Document Loading and Preprocessing
- Chunking Strategies — The Most Underrated RAG Decision
- Embedding Models — Turning Text into Vectors
- Vector Databases — Storing and Searching at Scale
- Retrieval Strategies — Beyond Naive Top-k
- Reranking — Precision After Recall
- Context Assembly and Prompt Engineering
- Evaluation — Measuring What Actually Matters
- Production Deployment and Scaling
- Advanced Techniques and What's Next
- Conclusion
What is RAG and Why Does It Matter?
Large language models are trained on static snapshots of the internet. The moment training ends, the model's knowledge freezes. Ask GPT-4 about an event that happened last week and it will either refuse to answer or — more dangerously — hallucinate a plausible-sounding but entirely fabricated response. This is the fundamental knowledge problem with LLMs, and it has three distinct dimensions
First, training cutoffs mean that any information created after the model's data collection window is simply unknown to the model. Second, hallucination means that when a model lacks knowledge, it often generates confident-sounding falsehoods rather than admitting ignorance. Third, private and proprietary data — your company's internal documentation, customer records, legal contracts, product specifications — never appears in public training data at all
Retrieval-Augmented Generation (RAG) solves all three problems with a single architectural insight: instead of baking knowledge into model weights, retrieve it at inference time. When a user asks a question, the system first searches a knowledge base for the most relevant documents, then injects those documents into the LLM's prompt as context, and finally asks the model to answer based on that retrieved evidence
The term was coined by Patrick Lewis et al. in their landmark 2020 paper Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks (Facebook AI Research / arXiv:2005.11401). Lewis et al. demonstrated that a retrieval-augmented model outperformed pure parametric models on open-domain QA benchmarks including Natural Questions, TriviaQA, and WebQuestions — without any task-specific fine-tuning
The core value proposition of RAG is compelling:
- No retraining required. Update the knowledge base and the model immediately has access to new information
- Always up-to-date. Index new documents as they are created; the model's effective knowledge horizon is the last index update, not the training cutoff
- Auditable and attributable. Every answer can be traced back to the specific source chunks that informed it, enabling citation, verification, and trust
- Cost-effective. Fine-tuning a 70B parameter model costs tens of thousands of dollars. Updating a vector index costs cents

The RAG Architecture — An End-to-End Overview
A RAG system operates in two distinct phases that are designed, deployed, and scaled independently
Offline Indexing Phase
The offline indexing phase runs asynchronously, typically as a batch job or a streaming pipeline triggered by document updates. It transforms raw source documents into a searchable vector index:
- Document loading — Raw documents are ingested from their source systems: S3 buckets, SharePoint, databases, web crawlers, APIs
- Preprocessing — Documents are cleaned: HTML tags stripped, whitespace normalized, tables converted to structured text, metadata extracted
- Chunking — Long documents are split into smaller, semantically coherent segments. This is the most consequential decision in the entire pipeline
- Embedding — Each chunk is passed through an embedding model that converts it into a dense vector representation capturing its semantic meaning
- Storing — Chunk text, embeddings, and metadata are written to a vector database that supports approximate nearest neighbor (ANN) search
Online Retrieval Phase
The online retrieval phase runs synchronously at inference time, typically within a latency budget of 200–500ms:
- Query embedding — The user's query is embedded using the same model used during indexing
- ANN search — The query vector is compared against all stored chunk vectors using an approximate nearest neighbor algorithm (HNSW, IVF, etc.) to retrieve the top-k most similar chunks
- Optional reranking — A more expensive cross-encoder model rescores the top-k candidates for precision
- Context assembly — Retrieved chunks are formatted and injected into the LLM prompt
- LLM generation — The model generates a response grounded in the retrieved context
The two phases interact at the vector database: the offline phase writes to it, the online phase reads from it. This separation is what makes RAG systems scalable — you can update the knowledge base without touching the inference path, and you can scale the inference path without re-indexing

Document Loading and Preprocessing
The quality of a RAG system is bounded by the quality of its input data. Garbage in, garbage out applies with particular force here because retrieval errors compound: a poorly preprocessed document produces poor embeddings, which produce poor retrieval, which produces a hallucinated or unfaithful answer
Source Document Types
Real-world RAG systems must handle a heterogeneous mix of document formats:
- PDFs — The most common enterprise format. PDFs can be text-based (easy) or scanned images (requiring OCR). Multi-column layouts, footnotes, and embedded figures require special handling
- HTML pages — Web content contains navigation menus, ads, and boilerplate that must be stripped before the actual content can be extracted
- Markdown — Common in developer documentation and wikis. Relatively clean but requires handling of code blocks, tables, and front matter
- DOCX — Microsoft Word documents with complex formatting, tracked changes, and embedded objects
- Databases — Structured data that must be serialized to text, often as natural-language descriptions of rows or as formatted tables
- APIs — Live data sources (Confluence, Notion, Jira, Salesforce) that require authenticated connectors and incremental sync logic
Tooling
LangChain provides a comprehensive library of document loaders covering over 100 source types, from PyPDFLoader and WebBaseLoader to ConfluenceLoader and GitLoader. LlamaIndex offers a similar ecosystem through its Reader abstraction, with particularly strong support for structured data sources. Unstructured.io is a specialized preprocessing library that handles complex document layouts, extracts tables as structured data, and performs OCR on image-heavy PDFs — it is the industry standard for enterprise document ingestion
Preprocessing Steps
Effective preprocessing involves several transformations:
- HTML stripping — Remove all markup tags, keeping only the visible text content. Libraries like
BeautifulSoupandtrafilaturahandle this well - Whitespace normalization — Collapse multiple spaces, remove zero-width characters, normalize Unicode
- Table handling — Convert HTML or PDF tables to Markdown table syntax or natural-language descriptions so they survive chunking intact
- Metadata extraction — Capture document-level metadata: title, URL, author, publication date, section headers, document type. This metadata is stored alongside the chunk and enables powerful filtered retrieval later
Clean text dramatically improves downstream retrieval quality. In practice, investing a week in preprocessing quality typically yields larger retrieval improvements than spending the same time tuning embedding models
Chunking Strategies — The Most Underrated RAG Decision
If you could only optimize one thing in a RAG pipeline, it should be chunking. The chunking strategy determines what unit of text gets embedded, retrieved, and injected into the prompt. Get it wrong and no amount of embedding model tuning or reranking will save you
The fundamental tradeoff is between precision and context: small chunks (128–256 tokens) embed a narrow, focused concept and deliver precise retrieval but lose surrounding context; large chunks (1024–2048 tokens) embed a broad topic with rich context but produce blurry embeddings that are harder to match against specific queries
Fixed-Size Chunking with Overlap
Fixed-size chunking splits documents into segments of exactly N tokens (or characters), with an overlap of M tokens between consecutive chunks. The overlap ensures that sentences spanning a chunk boundary appear in at least one chunk in their entirety. This is the simplest strategy and a reasonable baseline. Its weakness is that it ignores document structure entirely — a chunk boundary might fall in the middle of a sentence, a table, or a code block
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.document_loaders import TextLoader
# Load a document
loader = TextLoader("technical_docs.txt")
documents = loader.load()
# Split with fixed size and overlap
splitter = RecursiveCharacterTextSplitter(
chunk_size=512,
chunk_overlap=64,
length_function=len,
separators=["\n\n", "\n", ". ", " ", ""]
)
chunks = splitter.split_documents(documents)
print(f"Split {len(documents)} document(s) into {len(chunks)} chunks")
for i, chunk in enumerate(chunks[:3]):
print(f"\nChunk {i+1} ({len(chunk.page_content)} chars):")
print(chunk.page_content[:200] + "...")
print(f"Metadata: {chunk.metadata}")RecursiveCharacterTextSplitter is smarter than a naive fixed-size splitter: it tries to split on paragraph breaks first, then sentence breaks, then word breaks, only falling back to character-level splitting as a last resort
Sentence-Based and Paragraph-Based Chunking
Sentence-based chunking uses a sentence boundary detector (spaCy, NLTK, or a regex heuristic) to split on sentence boundaries, then groups sentences into chunks of approximately the target size. This guarantees that no sentence is split across chunks. Paragraph-based chunking treats each paragraph (double newline) as a natural chunk boundary. This works well for well-structured documents like Wikipedia articles or technical documentation but produces highly variable chunk sizes
Semantic Chunking
Semantic chunking embeds each sentence individually, then identifies topic shift points by measuring the cosine distance between consecutive sentence embeddings. When the distance exceeds a threshold, a new chunk begins. This produces chunks that are semantically coherent — each chunk covers exactly one topic — at the cost of requiring an embedding pass over the entire document during indexing
Hierarchical / Parent-Child Chunking
Hierarchical chunking maintains two levels of granularity simultaneously. Small child chunks (128 tokens) are embedded and used for retrieval — their narrow focus makes them highly precise. When a child chunk is retrieved, the system returns its parent chunk (512–1024 tokens) to the LLM instead, providing rich surrounding context. This is sometimes called the small-to-big retrieval pattern
Late Chunking
Late chunking (introduced by Jina AI) embeds the entire document first using a long-context embedding model, then pools the token-level embeddings into chunk-level representations. Because the token embeddings are computed with full document context, each chunk embedding captures its meaning within the broader document — solving the context loss problem of standard chunking

Embedding Models — Turning Text into Vectors
An embedding model is a neural network that maps a piece of text to a fixed-dimensional vector in a high-dimensional semantic space. Texts with similar meanings are mapped to nearby points; texts with different meanings are mapped to distant points. This geometric structure is what makes semantic search possible
Bi-Encoders and Dense Retrieval
The dominant architecture for retrieval embeddings is the bi-encoder: the query and each document are encoded independently into vectors, and similarity is computed as the dot product or cosine similarity between them. Because document vectors can be precomputed and stored, retrieval reduces to a single matrix multiplication — extremely fast at scale. This is called dense retrieval because the vectors are dense (all dimensions are non-zero)
Sparse Retrieval and Hybrid Search
Sparse retrieval methods like BM25 represent documents as sparse term-frequency vectors. BM25 is a classical information retrieval algorithm that scores documents based on term overlap with the query, adjusted for document length and term frequency saturation. It is extremely fast, requires no GPU, and handles exact keyword matches perfectly — something dense embeddings often struggle with. SPLADE (Sparse Lexical and Expansion Model) is a learned sparse retrieval model that combines the keyword-matching strength of BM25 with the semantic generalization of dense embeddings
Hybrid retrieval runs both dense and sparse retrieval in parallel and merges the results using Reciprocal Rank Fusion (RRF). RRF consistently outperforms either method alone on most real-world corpora and should be the default starting point for any production RAG system
Top Embedding Models
text-embedding-3-large(OpenAI) — 3072 dimensions, state-of-the-art on MTEB, available via API. The go-to choice when cost is not a constraintembed-v3(Cohere) — 1024 dimensions, strong multilingual support, available via API with input-type specification (search_documentvs.search_query)BGE-large-en-v1.5(BAAI) — 1024 dimensions, open-source, top-performing open model on MTEB English leaderboard. Excellent for self-hosted deploymentsE5-mistral-7b-instruct(Microsoft) — 4096 dimensions, instruction-tuned, highest quality open-source embedding model but requires significant GPU memory
from sentence_transformers import SentenceTransformer
import numpy as np
# Load the BGE-large model
model = SentenceTransformer('BAAI/bge-large-en-v1.5')
# Documents to encode
documents = [
"RAG systems retrieve relevant documents at inference time.",
"Vector databases store embeddings for approximate nearest neighbor search.",
"Chunking strategy is the most important decision in a RAG pipeline.",
]
# BGE models benefit from a query instruction prefix
query = "What is the most important decision when building a RAG system?"
query_with_instruction = (
f"Represent this sentence for searching relevant passages: {query}"
)
# Encode with normalization for cosine similarity via dot product
doc_embeddings = model.encode(documents, normalize_embeddings=True)
query_embedding = model.encode([query_with_instruction], normalize_embeddings=True)
# Compute cosine similarities
scores = np.dot(query_embedding, doc_embeddings.T)[0]
ranked = sorted(zip(scores, documents), reverse=True)
for score, doc in ranked:
print(f"{score:.4f}: {doc}")The BEIR (Benchmarking IR) benchmark evaluates retrieval models across 18 heterogeneous datasets spanning biomedical, legal, financial, and general-domain text. Always benchmark your embedding model on a domain-representative sample of your own data — MTEB/BEIR rankings do not always transfer to specialized domains. Higher-dimensional embeddings generally encode more information but require more storage and slower ANN search. OpenAI's text-embedding-3-large supports Matryoshka Representation Learning (MRL), allowing you to truncate embeddings to 256 or 512 dimensions with only a small quality penalty

Vector Databases — Storing and Searching at Scale
A vector database is a data store optimized for storing high-dimensional vectors and performing approximate nearest neighbor (ANN) search over them at low latency. It is the central component of the RAG architecture — the bridge between the offline indexing phase and the online retrieval phase
ANN Algorithms
- HNSW (Hierarchical Navigable Small World) — A graph-based ANN algorithm that builds a multi-layer proximity graph. Delivers excellent query-time performance (sub-millisecond for millions of vectors) with high recall. The default algorithm in most vector databases
- IVF (Inverted File Index) — Clusters vectors into Voronoi cells during indexing. At query time, only the nearest cells are searched. More memory-efficient than HNSW but requires a training step
- PQ (Product Quantization) — Compresses vectors by splitting them into sub-vectors and quantizing each independently. Dramatically reduces memory footprint (8–32x) at the cost of some recall. Often combined with IVF as IVF-PQ
Vector Database Comparison
Pinecone is a fully managed, serverless vector database. It handles infrastructure, scaling, and replication automatically. Strong metadata filtering, hybrid search support, and a generous free tier make it the fastest path to production. The tradeoff is vendor lock-in and cost at scale
Qdrant is an open-source vector database written in Rust, offering exceptional performance and a rich filtering API. It supports hybrid search natively, runs on-premise or in the cloud, and has a managed cloud offering. The recommended choice for teams that want control without sacrificing performance
Weaviate is an open-source vector database with a GraphQL API and built-in support for hybrid search, multi-tenancy, and module-based vectorization
Chroma is a lightweight, embeddable vector database designed for rapid prototyping. It runs in-process with no external dependencies, making it ideal for development and small-scale deployments. Not recommended for production at scale
pgvector is a PostgreSQL extension that adds vector storage and ANN search to a standard Postgres database. If your application already uses Postgres, pgvector eliminates the need for a separate vector store. Performance lags behind dedicated vector databases at very large scale but is entirely adequate for corpora under ~10M vectors
Milvus is a cloud-native, open-source vector database designed for billion-scale deployments. It supports multiple ANN algorithms, GPU acceleration, and a distributed architecture. The most powerful open-source option for extreme scale
Choose your vector database based on: scale (number of vectors and queries per second), operational model (managed vs. self-hosted), hybrid search support (critical for production quality), metadata filtering capabilities, latency SLAs, and total cost of ownership

Retrieval Strategies — Beyond Naive Top-k
Naive top-k retrieval — embed the query, find the k nearest chunks — is a reasonable baseline but leaves significant quality on the table. Several advanced strategies address its limitations
Maximum Marginal Relevance (MMR)
MMR balances relevance and diversity. Instead of returning the k most similar chunks (which are often near-duplicates of each other), MMR iteratively selects chunks that are both relevant to the query and dissimilar to already-selected chunks. This is particularly valuable when the knowledge base contains many near-duplicate documents
HyDE — Hypothetical Document Embeddings
HyDE (Gao et al., arXiv:2212.10496) addresses a fundamental mismatch: queries are short and sparse, while documents are long and dense. Their embedding distributions are different, making direct query-document similarity imprecise. HyDE's solution: use an LLM to generate a hypothetical answer to the query — a plausible document that would answer the question. Embed the hypothetical answer (not the original query) and use that embedding for retrieval. Because the hypothetical answer is in the same distribution as real documents, the embedding similarity is much more accurate
from sentence_transformers import SentenceTransformer
from openai import OpenAI
client = OpenAI()
model = SentenceTransformer('BAAI/bge-large-en-v1.5')
def hyde_retrieve(query: str, vector_db, top_k: int = 10):
"""Retrieve using Hypothetical Document Embeddings (HyDE)."""
# Step 1: Generate a hypothetical answer using an LLM
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{
"role": "system",
"content": (
"Write a detailed, factual paragraph that directly answers "
"the following question. Write as if you are an expert "
"writing a technical document."
)
},
{"role": "user", "content": query}
],
max_tokens=256,
temperature=0.0,
)
hypothetical_answer = response.choices[0].message.content
# Step 2: Embed the hypothetical answer (not the original query)
hyp_embedding = model.encode(
[hypothetical_answer],
normalize_embeddings=True
)[0]
# Step 3: Query the vector DB with the hypothetical embedding
results = vector_db.search(
query_vector=hyp_embedding.tolist(),
top_k=top_k
)
return results, hypothetical_answer
# Example usage
query = "How does HNSW indexing work in vector databases?"
results, hyp = hyde_retrieve(query, vector_db=my_vector_db)
print(f"Hypothetical answer used for retrieval:\n{hyp}\n")
print(f"Retrieved {len(results)} documents")Multi-Query and Parent-Child Retrieval
Multi-query retrieval uses an LLM to generate N paraphrases of the original query, runs retrieval for each, and merges the result sets (deduplicating by chunk ID). This addresses the sensitivity of embedding-based retrieval to exact phrasing — different phrasings of the same question often retrieve different relevant chunks. Parent-child retrieval embeds small child chunks for precision but returns their larger parent chunks to the LLM for context, implemented by storing a parent_id metadata field on each child chunk

Reranking — Precision After Recall
Retrieval is a recall problem: get as many relevant documents as possible into the candidate set. Reranking is a precision problem: from that candidate set, identify the truly relevant documents and discard the noise. This two-stage retrieval paradigm is the standard architecture for high-quality RAG systems
Why Cross-Encoders Cannot Be Used for First-Stage Retrieval
A cross-encoder takes the query and a document concatenated together as input and outputs a single relevance score. Because it sees both texts simultaneously, it can model fine-grained interactions between query terms and document terms — far more accurate than a bi-encoder's dot product. The problem: a cross-encoder must process every query-document pair independently. With a corpus of 10 million chunks, that means 10 million forward passes per query — completely infeasible at inference time. Cross-encoders are therefore restricted to reranking a small candidate set (typically 20–100 documents) retrieved by the fast bi-encoder first
Top Reranking Models
- Cohere Rerank API — Managed API, state-of-the-art quality, supports 100+ languages. The easiest path to production-quality reranking
BGE-reranker-large(BAAI) — Open-source cross-encoder, excellent quality, self-hostable on a single GPU- Flashrank — Ultra-lightweight reranker designed for CPU inference. Adds minimal latency and is ideal for cost-sensitive deployments
Reranking 20 candidates with Cohere Rerank adds approximately 50–150ms of latency. For most applications, this is an acceptable cost for the precision improvement. Skip reranking when: latency budget is under 100ms total, the corpus is small and well-structured, or the query distribution is narrow and predictable
import cohere
co = cohere.Client("YOUR_COHERE_API_KEY")
# Assume we retrieved 20 candidate chunks from the vector DB
query = "What are the tradeoffs between HNSW and IVF indexing algorithms?"
candidate_documents = [
"HNSW builds a hierarchical graph structure for fast approximate search...",
"IVF partitions the vector space into Voronoi cells...",
"Product quantization compresses vectors by splitting into sub-vectors...",
"Pinecone uses a proprietary indexing algorithm optimized for cloud deployment...",
"HNSW offers excellent query-time performance with high recall at the cost of memory...",
] + [f"Candidate document {i}" for i in range(6, 21)]
# Rerank with Cohere
rerank_response = co.rerank(
model="rerank-english-v3.0",
query=query,
documents=candidate_documents,
top_n=5, # Return only the top 5 after reranking
return_documents=True,
)
print("Reranked results:")
for result in rerank_response.results:
print(f" Rank {result.index + 1} | Score: {result.relevance_score:.4f}")
print(f" Text: {result.document.text[:100]}...\n")
Context Assembly and Prompt Engineering
Once the relevant chunks have been retrieved and reranked, they must be assembled into a prompt that the LLM can use effectively. This step is more consequential than it appears
Assembly Strategies
- Stuff strategy — Concatenate all retrieved chunks into a single context block and pass them to the LLM in one call. Simple and fast. Works well when the total context fits comfortably within the model's context window
- Map-reduce — For large document sets, process each chunk independently (map step), then synthesize the extracted information (reduce step). More expensive but handles arbitrarily large context
- Refine chain — Process chunks sequentially, iteratively refining an initial answer using each subsequent chunk. Produces high-quality answers but is slow and expensive
The Lost-in-the-Middle Problem
Liu et al. (2023) demonstrated that LLMs exhibit strong position bias in long contexts: they attend most strongly to information at the beginning and end of the context window, and systematically underweight information in the middle. This has a direct implication for RAG: place the most relevant chunks at the beginning or end of the context block, not in the middle
Token Budget Management
Every token of retrieved context reduces the space available for the model's reasoning and response. A practical token budget for a RAG prompt: system prompt (~200 tokens), retrieved context (1,500–3,000 tokens, or 3–6 chunks of 512 tokens each), user query (~50–200 tokens), and response budget (500–1,000 tokens) — well within the 8,192-token context window of most production models
def build_rag_prompt(context: str, question: str):
system_prompt = """You are a precise and faithful question-answering assistant.
You will be given a set of retrieved context passages and a question.
Your task is to answer the question based ONLY on the information in the provided context.
Rules:
- Answer only from the context. Do not use prior knowledge.
- If the context does not contain enough information to answer the question,
say: "I don't have enough information in the provided context to answer this question."
- Cite the source of your answer when possible.
- Be concise and precise. Do not pad your answer.
- Do not make up facts, statistics, or citations."""
user_message = f"""Context passages:
---
{context}
---
Question: {question}
Answer:"""
return system_prompt, user_message
# Example usage
retrieved_chunks = [
"[Source: RAG paper, p.3] The retriever uses Maximum Inner Product Search (MIPS)...",
"[Source: RAG paper, p.5] The generator is initialized from BART-large...",
]
context = "\n\n".join(retrieved_chunks)
question = "What retrieval algorithm does the original RAG paper use?"
system, user = build_rag_prompt(context, question)
print("System prompt:")
print(system)
print("\nUser message:")
print(user)Evaluation — Measuring What Actually Matters
Evaluating a RAG system is significantly harder than evaluating a standard NLP model. There is no single metric that captures system quality. A system can retrieve perfectly but generate poorly, or generate fluently but unfaithfully. You need to measure the retrieval and generation stages independently
The RAGAS Framework
RAGAS (Retrieval-Augmented Generation Assessment, arXiv:2309.15217) provides four complementary metrics that together give a complete picture of RAG system quality:
- Context Recall — What fraction of the information needed to answer the question is present in the retrieved context? Measures retrieval completeness. Low context recall means the retriever is missing relevant documents
- Context Precision — What fraction of the retrieved context is actually relevant to the question? Measures retrieval noise. Low context precision means the retriever is returning irrelevant chunks that distract the LLM
- Answer Faithfulness — Is every claim in the generated answer supported by the retrieved context? Measures hallucination. Low faithfulness means the LLM is generating information not present in the context
- Answer Relevance — Does the generated answer actually address the question asked? Measures response quality. Low answer relevance means the LLM is producing tangential or incomplete responses
from ragas import evaluate
from ragas.metrics import (
context_recall,
context_precision,
faithfulness,
answer_relevancy,
)
from datasets import Dataset
# Build your evaluation dataset
eval_data = {
"question": [
"What ANN algorithm does HNSW use?",
"What is the purpose of reranking in RAG?",
"How does HyDE improve retrieval quality?",
],
"answer": [
"HNSW uses a hierarchical graph structure for approximate nearest neighbor search.",
"Reranking improves precision by using a cross-encoder to rescore the top-k candidates retrieved by the bi-encoder.",
"HyDE generates a hypothetical answer to the query and embeds that instead of the raw query, closing the distribution gap between queries and documents.",
],
"contexts": [
["HNSW builds a multi-layer proximity graph...", "The graph structure enables logarithmic search time..."],
["Cross-encoders score query-document pairs jointly...", "Reranking adds 50-150ms of latency..."],
["HyDE embeds a hypothetical answer rather than the query...", "This closes the query-document distribution gap..."],
],
"ground_truth": [
"HNSW is a graph-based ANN algorithm.",
"Reranking uses a cross-encoder to improve precision after bi-encoder retrieval.",
"HyDE generates and embeds a hypothetical answer to bridge the query-document gap.",
],
}
dataset = Dataset.from_dict(eval_data)
# Run RAGAS evaluation
results = evaluate(
dataset=dataset,
metrics=[
context_recall,
context_precision,
faithfulness,
answer_relevancy,
],
)
print(results)
# Output: {'context_recall': 0.92, 'context_precision': 0.87,
# 'faithfulness': 0.95, 'answer_relevancy': 0.91}Building a Golden Evaluation Set
A golden evaluation set is a curated collection of (question, ground-truth answer, relevant document IDs) triples that represent the query distribution your system will face in production. Build it by sampling 100–500 representative queries from production logs (or generating them synthetically using an LLM), having domain experts annotate the correct answers and relevant source documents, and versioning the evaluation set alongside your code so that metric changes are attributable to specific system changes
LLM-as-Judge and Online Evaluation
For metrics like faithfulness and answer relevance, RAGAS uses an LLM (typically GPT-4) as the judge. The LLM is prompted to evaluate whether a claim is supported by the context, or whether an answer addresses the question. This approach scales to arbitrary output formats and correlates well with human judgments. Offline evaluation on a golden set measures potential quality; online evaluation measures actual quality in production. Instrument your RAG system to log queries, retrieved contexts, and generated answers. Sample a fraction for human review. Track metric trends over time — a drop in faithfulness score often signals index staleness or a distribution shift in incoming queries

Production Deployment and Scaling
Moving a RAG prototype to production requires addressing a set of engineering concerns that do not appear in development: latency, cost, reliability, and observability
Semantic Caching
Semantic caching stores the results of previous RAG queries and serves cached responses for semantically similar future queries. Unlike exact-match caching (which only hits on identical queries), semantic caching embeds incoming queries and checks for near-duplicate queries in a cache index. A cosine similarity threshold (typically 0.95) determines whether a cache hit is declared. Semantic caching can reduce LLM API costs by 30–60% in production systems with repetitive query patterns, and reduces p50 latency to near-zero for cached queries
Monitoring
A production RAG system requires instrumentation across four dimensions:
- Retrieval hit rate — What fraction of queries return at least one chunk above the relevance threshold? A declining hit rate signals index staleness
- Latency (p50/p95) — Track end-to-end latency and per-stage latency (embedding, ANN search, reranking, LLM generation) separately. p95 latency is the metric that matters for user experience
- Faithfulness score drift — Run a sample of production query-answer pairs through the RAGAS faithfulness metric daily. A declining score signals that the LLM is hallucinating more, often due to context quality degradation
- Context window utilization — Track the average number of tokens consumed by retrieved context. Approaching the context window limit degrades generation quality
Query Routing and Guardrails
A query router classifies incoming queries and routes them to the appropriate handler: simple factual queries go to the standard pipeline, complex multi-hop queries go to an agentic pipeline, out-of-scope queries receive a guardrail response without an LLM call, and sensitive queries receive additional safety filtering before retrieval. Production RAG systems also need input and output guardrails to detect prompt injection attempts, jailbreaks, off-topic queries, PII, and toxic content
Incremental Index Updates
The knowledge base must be kept current. Design your indexing pipeline for incremental updates: monitor source systems for new or modified documents, re-chunk and re-embed only changed documents (not the entire corpus), use soft deletes to mark outdated chunks as inactive rather than deleting them immediately, and maintain a blue-green index deployment so that a new index can be validated before traffic is switched over

Advanced Techniques and What's Next
GraphRAG (Microsoft Research, arXiv:2404.16130) builds a knowledge graph from the document corpus, extracting entities and relationships. At query time, it traverses the graph to retrieve not just semantically similar chunks but also structurally related information — enabling multi-hop reasoning that flat vector retrieval cannot support. Particularly powerful for complex analytical queries over large document collections
Self-RAG (Asai et al., arXiv:2310.11511) trains a model to decide when to retrieve (not every query needs retrieval), what to retrieve, and how to critique its own generated output for faithfulness. The model generates special reflection tokens ([Retrieve], [IsRel], [IsSup], [IsUse]) that control the retrieval and generation process dynamically
FLARE (Forward-Looking Active REtrieval) triggers retrieval proactively during generation: when the model's next-token probability falls below a threshold (indicating uncertainty), it pauses, formulates a retrieval query based on what it was about to say, retrieves relevant documents, and continues generation with the new context. This enables iterative, context-aware retrieval rather than a single retrieval step
Agentic RAG replaces the fixed retrieval pipeline with an LLM agent that can decide which tools to call, in what order, and how many times. The agent might call a vector DB, a web search API, a SQL database, and a calculator in sequence to answer a complex query. This is the direction the field is moving for enterprise applications
Multimodal RAG extends the retrieval paradigm to images, audio, and video. Multimodal embedding models (CLIP, ImageBind) embed both text and images into a shared vector space, enabling retrieval of relevant images, diagrams, and charts alongside text passages. Critical for domains like medical imaging, engineering documentation, and e-commerce
Conclusion
Building a production-grade RAG system requires making a series of interconnected engineering decisions, each of which has a significant impact on the quality, latency, and cost of the final system. The key decisions, in order of impact:
- Chunking strategy — Start with recursive character splitting at 512 tokens with 64-token overlap. Upgrade to semantic or hierarchical chunking once you have evaluation data showing where retrieval fails
- Embedding model —
BGE-large-en-v1.5is the best open-source starting point. Switch totext-embedding-3-largeif quality is paramount and cost is not a constraint - Retrieval strategy — Default to hybrid search (dense + BM25 with RRF). Add HyDE for query-document distribution mismatch. Add multi-query for broad, ambiguous queries
- Reranking — Always add a reranking stage. Cohere Rerank is the fastest path;
BGE-reranker-largefor self-hosted deployments - Evaluation framework — Instrument RAGAS from day one. Build a golden evaluation set before you start optimizing. You cannot improve what you do not measure
- Production hardening — Add semantic caching, query routing, incremental index updates, and faithfulness monitoring before going to production
The pragmatic starting architecture: recursive character splitting at 512 tokens → BGE-large embeddings → Qdrant for vector storage → hybrid search with RRF → Cohere Rerank → GPT-4o with a faithfulness-enforcing system prompt → RAGAS evaluation. This stack is open-source-friendly, self-hostable, and has been validated in production at scale
Finally: RAG is not a one-time build. Query patterns shift, knowledge bases grow, and model capabilities improve. Continuous evaluation — running your golden set weekly, sampling production queries for human review, and tracking metric trends — is what separates a RAG system that stays reliable from one that quietly degrades. Build the evaluation infrastructure first, and let it guide every subsequent optimization decision
Key takeaways
- RAG eliminates the need to retrain or fine-tune models when knowledge changes by retrieving external documents at inference time instead
- Chunking strategy is the highest-leverage single decision in a RAG pipeline — poor chunking degrades retrieval quality regardless of how good the embedding model is
- Hybrid search combining dense embeddings and sparse BM25 outperforms either method alone on most real-world corpora and should be the default starting point
- Reranking with a cross-encoder model boosts precision significantly and adds only 50–150ms of latency at most practical candidate set sizes
- RAGAS provides four independent evaluation dimensions — context recall, context precision, answer faithfulness, and answer relevance — and all four must be tracked separately to diagnose specific failure modes
- Production RAG systems require semantic caching, incremental index updates, and faithfulness monitoring to remain reliable as the knowledge base grows and query patterns shift
Questions this answers
What is RAG and how is it different from fine-tuning?
RAG (Retrieval-Augmented Generation) retrieves external documents at inference time and injects them into the prompt, so the model answers from that retrieved context rather than from memorized weights. Fine-tuning bakes knowledge directly into model parameters through additional training. RAG is preferred when knowledge changes frequently, must be auditable, or is proprietary; fine-tuning is preferred when you want to change model behavior, style, or domain expertise rather than expand its factual knowledge
What is the best chunking strategy for RAG?
There is no universally best chunking strategy — it depends on your documents and query distribution. Semantic chunking generally outperforms fixed-size chunking for prose-heavy documents by splitting on natural topic boundaries. Hierarchical chunking works well when you need both precise retrieval and broad surrounding context. Start with recursive character splitting at 512–1024 tokens with 10–20% overlap, then measure retrieval recall on a golden evaluation set before optimizing further
Which vector database should I use for production RAG?
For most teams, Qdrant or Weaviate offer the best balance of performance, features, and operational simplicity. Use pgvector if you are already running PostgreSQL and your vector count is under a few million. Use Pinecone if you need a fully managed service with no infrastructure to operate. Avoid in-memory databases like Chroma in production — they are excellent for prototyping but lack the durability and reliability guarantees production systems require
How do I evaluate a RAG system?
Use the RAGAS framework to measure four dimensions: context recall (did retrieval surface the relevant chunks?), context precision (were retrieved chunks mostly relevant?), answer faithfulness (does the answer stay within the retrieved context?), and answer relevance (does the answer address the question?). Build a golden evaluation set of 50–100 question-answer pairs from your real documents and run RAGAS metrics after every significant pipeline change
What is reranking and when should I use it?
Reranking is a second-stage step that takes the top-k candidates from vector search and scores each one jointly with the query using a cross-encoder transformer model, which attends to both simultaneously and produces far more accurate relevance scores than the bi-encoder used for retrieval. Use reranking whenever answer quality matters more than raw latency — it typically adds 50–200ms and improves precision by 10–30% in practice. Skip it for real-time streaming applications with strict sub-100ms end-to-end SLA requirements
Sources
- Retrieval-Augmented Generation for Knowledge-Intensive NLP TasksarXiv / Facebook AI Research
- BEIR: A Heterogeneous Benchmark for Zero-shot Evaluation of Information Retrieval ModelsarXiv
- Precise Zero-Shot Dense Retrieval without Relevance Labels (HyDE)arXiv
- Lost in the Middle: How Language Models Use Long ContextsarXiv
- RAGAS: Automated Evaluation of Retrieval Augmented GenerationarXiv
- Self-RAG: Learning to Retrieve, Generate, and Critique through Self-ReflectionarXiv
- From Local to Global: A Graph RAG Approach to Query-Focused SummarizationarXiv / Microsoft Research