RAG Explained for Developers

Retrieval-Augmented Generation — ground your AI in real data

AI & DevelopmentJuly 8, 202618 min readBy Keyur Patel

LLMs hallucinate. They confidently state incorrect facts, invent API endpoints that don't exist, and cite documentation that was never written. This isn't a bug that will be fixed with the next model release — it's a fundamental property of how generative models work. They predict plausible text, not truthful text.

RAG (Retrieval-Augmented Generation) is the most practical solution to this problem. Instead of relying on the model's training data alone, you retrieve relevant documents from your own knowledge base and feed them into the context window. The model generates answers grounded in real, verifiable data. It's the difference between asking someone to answer from memory versus giving them the reference material first.

I've built RAG systems for internal documentation search, customer support bots, and code-aware developer tools. Here's a complete guide covering architecture, implementation, and the real-world gotchas you'll encounter.

What Is RAG?

Retrieval-Augmented Generation combines two capabilities: retrieval (finding relevant information) and generation (producing natural language responses). Instead of the model relying entirely on its training data, RAG gives it access to external knowledge at inference time.

The concept is simple: before generating a response, search your knowledge base for relevant documents and include them in the prompt. The model then uses those documents as source material rather than generating from memory.

Without RAG:
User: "What's our refund policy for enterprise customers?"
LLM: [makes something up based on general training data]

With RAG:
User: "What's our refund policy for enterprise customers?"
System: [retrieves actual policy document from knowledge base]
LLM: [generates answer grounded in the real policy document]
     "Enterprise customers can request a full refund within 30 days..."
     Source: internal-docs/refund-policy-v3.md

Why RAG? Comparing Approaches

There are three main approaches to giving an LLM access to custom knowledge. Each has different tradeoffs:

ApproachRAGFine-TuningLarge Context
Data freshnessReal-time updatesRequires retrainingCurrent (in-context)
CostLow (embeddings cheap)High (training runs)High (token costs)
Setup complexityMediumHighLow
ScalabilityMillions of docsLimited by trainingLimited by window
Hallucination riskLow (grounded)MediumLow (if relevant)
Source attributionYes (track sources)NoManual
Best forKnowledge bases, docsBehavior changesSmall, fixed datasets

RAG wins for most production applications because knowledge changes frequently, you need source attribution, and the approach scales to millions of documents without retraining. Fine-tuning is better when you need to change how the model responds (tone, format, reasoning style) rather than what it knows.

How RAG Works: The Pipeline

Every RAG system follows the same five-step pipeline. Understanding each step helps you optimize the system as a whole.

RAG Pipeline:

┌─────────────────────────────────────────────────────────┐
│  1. QUERY         User asks a question                  │
│     ↓                                                    │
│  2. EMBED         Convert question to vector            │
│     ↓                                                    │
│  3. SEARCH        Find similar vectors in database      │
│     ↓                                                    │
│  4. RETRIEVE      Pull matching document chunks         │
│     ↓                                                    │
│  5. GENERATE      LLM answers using retrieved context   │
└─────────────────────────────────────────────────────────┘

Indexing Phase (offline, done once):
Documents → Chunk → Embed → Store in Vector DB

Query Phase (real-time, per request):
Question → Embed → Search → Retrieve → Augment Prompt → Generate

The indexing phase processes your documents ahead of time. The query phase happens in real-time for each user request. Keeping these separate is important — indexing can be slow and expensive, but queries need to be fast.

RAG Architecture Components

Vector Database

A vector database stores document embeddings and enables fast similarity search. When you query with a question's embedding, it returns the most semantically similar documents — even if they don't share exact keywords with the query.

Unlike traditional keyword search (which matches exact words), vector search understands meaning. "How do I authenticate?" matches documents about "login flow" and "bearer tokens" because they're semantically related.

Embeddings

Embeddings are numerical representations of text that capture semantic meaning. Similar texts produce similar vectors. The embedding model converts both your documents (during indexing) and queries (at search time) into the same vector space.

// Conceptual view of embeddings
embed("How do I reset my password?")  → [0.12, -0.34, 0.78, ...] // 1536 dimensions
embed("Password recovery process")    → [0.11, -0.32, 0.76, ...] // Very similar!
embed("What's for lunch today?")      → [-0.45, 0.67, -0.21, ...] // Very different

// Similarity is measured by cosine distance between vectors
cosineSimilarity(query, doc1) = 0.94  // Highly relevant
cosineSimilarity(query, doc2) = 0.23  // Not relevant

Chunking

Documents must be split into chunks before embedding. Chunk size matters: too large and you retrieve irrelevant content alongside relevant content. Too small and you lose context that makes the chunk meaningful.

Retrieval

The retrieval step finds the top-K most relevant chunks for a given query. Advanced systems add re-ranking (a second pass that scores results for relevance), metadata filtering (only search documents from a specific source), and hybrid search (combining vector similarity with keyword matching).

Generation

The final step constructs a prompt containing the retrieved context and the user's question, then sends it to the LLM. The system prompt instructs the model to answer only from the provided context and to cite sources.

Step-by-Step Implementation (Node.js)

Here's a complete RAG implementation using OpenAI embeddings and Pinecone as the vector database:

Step 1: Document Indexing

import OpenAI from 'openai';
import { Pinecone } from '@pinecone-database/pinecone';
import { readFileSync, readdirSync } from 'fs';

const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
const pinecone = new Pinecone({ apiKey: process.env.PINECONE_API_KEY });
const index = pinecone.index('knowledge-base');

// Chunk documents into smaller pieces
function chunkDocument(text, metadata, chunkSize = 500, overlap = 50) {
  const sentences = text.split(/(?<=[.!?])\s+/);
  const chunks = [];
  let current = [];
  let currentLength = 0;

  for (const sentence of sentences) {
    const words = sentence.split(/\s+/).length;
    if (currentLength + words > chunkSize && current.length > 0) {
      chunks.push({
        text: current.join(' '),
        metadata: { ...metadata, chunkIndex: chunks.length }
      });
      // Keep last few sentences for overlap
      const overlapSentences = current.slice(-2);
      current = [...overlapSentences, sentence];
      currentLength = current.join(' ').split(/\s+/).length;
    } else {
      current.push(sentence);
      currentLength += words;
    }
  }
  if (current.length > 0) {
    chunks.push({ text: current.join(' '), metadata: { ...metadata, chunkIndex: chunks.length } });
  }
  return chunks;
}

// Embed and store chunks
async function indexDocuments(docsDir) {
  const files = readdirSync(docsDir).filter(f => f.endsWith('.md'));
  
  for (const file of files) {
    const content = readFileSync(`${docsDir}/${file}`, 'utf-8');
    const chunks = chunkDocument(content, { source: file, indexed: new Date().toISOString() });
    
    // Batch embed (max 2048 inputs per request)
    const embedResponse = await openai.embeddings.create({
      model: "text-embedding-3-small",
      input: chunks.map(c => c.text)
    });
    
    // Upsert to Pinecone
    const vectors = chunks.map((chunk, i) => ({
      id: `${file}-chunk-${i}`,
      values: embedResponse.data[i].embedding,
      metadata: { ...chunk.metadata, text: chunk.text }
    }));
    
    await index.upsert(vectors);
    console.log(`Indexed ${chunks.length} chunks from ${file}`);
  }
}

Step 2: Query and Retrieval

async function retrieveContext(question, topK = 5) {
  // Embed the question
  const queryEmbedding = await openai.embeddings.create({
    model: "text-embedding-3-small",
    input: question
  });
  
  // Search vector database
  const results = await index.query({
    vector: queryEmbedding.data[0].embedding,
    topK,
    includeMetadata: true
  });
  
  // Return chunks with relevance scores
  return results.matches.map(match => ({
    text: match.metadata.text,
    source: match.metadata.source,
    score: match.score
  }));
}

Step 3: Augmented Generation

async function ragQuery(question) {
  // Retrieve relevant context
  const context = await retrieveContext(question);
  
  // Build augmented prompt
  const contextText = context
    .map((c, i) => `[Source ${i + 1}: ${c.source}]\n${c.text}`)
    .join('\n\n---\n\n');
  
  // Generate answer grounded in retrieved documents
  const response = await openai.chat.completions.create({
    model: "gpt-4o",
    temperature: 0.2,
    messages: [
      {
        role: "system",
        content: `You are a helpful assistant that answers questions based on the provided context documents.

Rules:
- Answer ONLY based on the provided context
- If the context doesn't contain the answer, say "I don't have information about that in my knowledge base"
- Cite sources using [Source N] format
- Be concise and direct
- Never make up information not present in the context`
      },
      {
        role: "user",
        content: `Context Documents:\n\n${contextText}\n\n---\n\nQuestion: ${question}`
      }
    ]
  });
  
  return {
    answer: response.choices[0].message.content,
    sources: context.map(c => c.source),
    relevanceScores: context.map(c => c.score)
  };
}

// Usage
const result = await ragQuery("How do I configure SSO for our application?");
console.log(result.answer);    // Grounded answer citing specific docs
console.log(result.sources);   // ["sso-setup-guide.md", "auth-config.md"]

Alternative: Using pgvector (PostgreSQL)

-- Setup pgvector extension
CREATE EXTENSION IF NOT EXISTS vector;

-- Create table for document chunks
CREATE TABLE document_chunks (
  id SERIAL PRIMARY KEY,
  content TEXT NOT NULL,
  source VARCHAR(255),
  chunk_index INTEGER,
  embedding vector(1536), -- OpenAI embedding dimensions
  created_at TIMESTAMP DEFAULT NOW()
);

-- Create index for fast similarity search
CREATE INDEX ON document_chunks 
  USING ivfflat (embedding vector_cosine_ops)
  WITH (lists = 100);

-- Query: find top 5 most similar chunks
SELECT content, source, 
  1 - (embedding <=> $1::vector) as similarity
FROM document_chunks
WHERE 1 - (embedding <=> $1::vector) > 0.7  -- minimum threshold
ORDER BY embedding <=> $1::vector
LIMIT 5;

Chunking Strategies

How you split documents has a massive impact on retrieval quality. There's no one-size-fits-all approach — the best strategy depends on your content type.

Fixed-Size Chunking

Split text every N tokens/words with overlap between chunks. Simple to implement and works reasonably well for uniform content like documentation.

// Fixed-size chunking with overlap
function fixedChunk(text, size = 400, overlap = 50) {
  const words = text.split(/\s+/);
  const chunks = [];
  for (let i = 0; i < words.length; i += size - overlap) {
    chunks.push(words.slice(i, i + size).join(' '));
  }
  return chunks;
}
// Pro: Simple, predictable chunk sizes
// Con: May split mid-sentence or mid-concept

Semantic Chunking

Split at natural boundaries — paragraphs, sections, headers. Each chunk represents a complete thought or concept. Better for structured content with clear sections.

// Semantic chunking: split by markdown headers
function semanticChunk(markdown) {
  const sections = markdown.split(/^#{1,3}\s+/m);
  return sections
    .filter(s => s.trim().length > 50) // Skip tiny fragments
    .map(section => {
      // Include the header in the chunk for context
      const lines = section.split('\n');
      return lines.join('\n').trim();
    });
}
// Pro: Preserves complete ideas and context
// Con: Variable chunk sizes, some chunks may be too large

Recursive Chunking

Start with large separators (double newlines, headers), then recursively split any chunk that's still too large using progressively smaller separators (single newlines, sentences, words). This is what LangChain's RecursiveCharacterTextSplitter uses.

// Recursive chunking: progressively split large chunks
function recursiveChunk(text, maxSize = 500, separators = ['\n\n', '\n', '. ', ' ']) {
  if (text.split(/\s+/).length <= maxSize) return [text];
  
  const separator = separators[0];
  const parts = text.split(separator);
  const chunks = [];
  let current = '';
  
  for (const part of parts) {
    if ((current + part).split(/\s+/).length > maxSize) {
      if (current) chunks.push(current.trim());
      // Recursively split if still too large
      if (part.split(/\s+/).length > maxSize && separators.length > 1) {
        chunks.push(...recursiveChunk(part, maxSize, separators.slice(1)));
      } else {
        current = part;
      }
    } else {
      current += separator + part;
    }
  }
  if (current.trim()) chunks.push(current.trim());
  return chunks;
}
// Pro: Best balance of preserving context and controlling size
// Con: More complex implementation

Vector Databases Compared

Choosing a vector database depends on your scale, infrastructure, and team preferences:

DatabaseTypeBest ForPricingKey Feature
PineconeManaged cloudFast prototyping, production SaaSFree tier + pay per useServerless, zero ops
WeaviateSelf-hosted / cloudComplex queries, hybrid searchOpen-source + cloud optionGraphQL API, modules
pgvectorPostgreSQL extensionTeams already using PostgresFree (part of your DB)No new infrastructure
QdrantSelf-hosted / cloudHigh performance, filteringOpen-source + cloudRust-based, fast
ChromaDBEmbedded / self-hostedLocal development, small scaleFree, open-sourceSimple API, Python-native
MilvusSelf-hosted / cloudLarge-scale enterpriseOpen-source + Zilliz cloudBillion-vector scale

My recommendation for most teams: start with pgvector if you already use PostgreSQL (zero new infrastructure). Move to Pinecone or Qdrant when you hit scale limits or need specialized features like hybrid search or advanced filtering. Use ChromaDB for local prototyping and testing.

RAG Evaluation Metrics

Building a RAG system is step one. Knowing whether it actually works well requires measurement. Here are the key metrics:

Retrieval Precision

What percentage of retrieved chunks are actually relevant to the question?

relevant_retrieved / total_retrieved

Target: > 0.8 (80% of retrieved docs should be relevant)

Retrieval Recall

What percentage of all relevant documents were actually retrieved?

relevant_retrieved / total_relevant

Target: > 0.7 (find at least 70% of relevant docs)

Faithfulness

Does the generated answer only use information from the retrieved context? No hallucinated facts?

claims_supported_by_context / total_claims

Target: > 0.9 (90%+ of claims should be grounded)

Answer Relevance

Does the generated answer actually address the user's question?

LLM-judged relevance score (0-1)

Target: > 0.85

Context Utilization

How much of the retrieved context is actually used in the answer?

context_sentences_used / total_context_sentences

Target: > 0.5 (avoid retrieving irrelevant chunks)

Tools like RAGAS, DeepEval, and LangSmith can automate these evaluations. Start by building a test set of 50-100 question/answer pairs with known correct sources, then measure how your system performs against them.

Common RAG Mistakes

Chunks too large or too small

Large chunks (2000+ words) retrieve irrelevant content alongside relevant content. Tiny chunks (50 words) lose context needed to understand the content.

✓ Fix: Start with 300-500 words per chunk with 50-word overlap. Adjust based on retrieval precision metrics.

No overlap between chunks

Without overlap, information at chunk boundaries gets split across two chunks and neither chunk has the full context.

✓ Fix: Use 10-20% overlap between consecutive chunks. This ensures boundary content appears in at least one complete chunk.

Retrieving too few or too many chunks

Too few (1-2) risks missing relevant information. Too many (20+) floods the context with noise and overwhelms the model.

✓ Fix: Start with 5 chunks. Add a re-ranking step to filter the top results by relevance score before passing to the LLM.

No metadata filtering

Searching the entire knowledge base for every query when you could narrow results by source, date, or category first.

✓ Fix: Store metadata (source, date, category, access level) with each chunk. Filter by metadata before semantic search.

Using the wrong embedding model for your content

Generic embedding models may not capture domain-specific nuances. Code, legal text, and medical content have different semantic structures.

✓ Fix: Test multiple embedding models on your specific content. Consider domain-specific models for specialized applications.

Not handling 'I don't know' cases

When retrieved context doesn't actually answer the question, the model may hallucinate an answer anyway rather than admitting uncertainty.

✓ Fix: Set a minimum relevance score threshold. If no results pass (e.g., score < 0.7), return 'I don't have information about that' instead of generating.

Best Practices

Start simple — basic chunking + embeddings + top-5 retrieval works surprisingly well for most use cases

Add overlap between chunks (10-20%) to avoid losing information at boundaries

Use hybrid search (vector + keyword) for better recall on exact terms, names, and IDs

Implement a re-ranking step to filter retrieved chunks before passing to the LLM

Store source metadata and return citations — users need to verify AI-generated answers

Set relevance score thresholds and handle low-confidence queries gracefully

Build an evaluation dataset early — you can't improve what you don't measure

Keep your indexing pipeline idempotent so you can re-index without duplicates when documents change

Frequently Asked Questions

What is RAG in AI?

RAG (Retrieval-Augmented Generation) combines information retrieval with text generation. Instead of relying solely on training data, the model retrieves relevant documents from an external knowledge base and uses them to generate grounded, accurate responses.

When should I use RAG vs fine-tuning?

Use RAG when you need up-to-date information, have frequently changing data, or want source attribution. Use fine-tuning when you need to change the model's behavior, tone, or teach it specialized reasoning patterns that don't change often.

What is a vector database?

A vector database stores data as high-dimensional numerical vectors (embeddings) and enables fast similarity search. It returns semantically similar documents even if they don't share exact keywords with the query.

How do embeddings work in RAG?

Embeddings convert text into numerical vectors that capture semantic meaning. Both documents and queries are embedded into the same space, allowing similarity search to find relevant content regardless of exact wording.

What is chunking in RAG?

Chunking splits documents into smaller pieces before embedding. Good chunking ensures each piece is self-contained and meaningful. Common strategies include fixed-size with overlap, semantic chunking by section, and recursive splitting.

How many chunks should I retrieve for a RAG query?

Typically 3-10 chunks work well. Start with 5 and adjust based on answer quality. Fewer chunks mean higher relevance concentration; more chunks provide broader coverage but risk diluting quality.

Can RAG eliminate hallucinations completely?

RAG significantly reduces hallucinations by grounding responses in real documents, but doesn't eliminate them entirely. The model can still misinterpret retrieved content. Evaluation metrics like faithfulness help measure grounding quality.

What embedding model should I use for RAG?

OpenAI's text-embedding-3-small offers good quality-to-cost ratio for most applications. For higher quality, use text-embedding-3-large. Open-source alternatives like sentence-transformers work well for self-hosted deployments.

Related Articles

Conclusion

RAG is the most practical way to build AI applications that are grounded in facts rather than trained guesses. The architecture is straightforward: embed your documents, store them in a vector database, retrieve relevant chunks at query time, and let the LLM generate answers from real source material.

The key insight is that RAG separates what the model knows (reasoning ability, language understanding) from what it can access (your specific knowledge base). This means you get the power of a frontier LLM applied to your own data without expensive fine-tuning or hoping the model memorized your docs during training.

Start simple. Basic chunking with OpenAI embeddings and a vector database handles most use cases. Add complexity — re-ranking, hybrid search, metadata filtering — only when your evaluation metrics show you need it. And always measure: a RAG system without evaluation is just a more expensive search engine.