Retrieval-Augmented Generation: A Practitioner's Guide to RAG Architecture

Published May 12, 2026

RAG retrieval pipeline architectureA user query is embedded and used to retrieve candidate chunks from a vector store, those candidates are reranked, the top results are attached to the prompt as context, and the LLM generates a grounded answer.User QueryEmbed + RetrieveRerankLLM + ContextGrounded Answervector store lookup happens between embed and rerank
A production RAG pipeline: retrieve, rerank, then generate with the retrieved context attached.

What Retrieval-Augmented Generation Actually Is

In one line

Retrieval-augmented generation (RAG) fetches a handful of relevant pieces of your own data at query time and puts them in the prompt, so the model answers from your facts instead of from whatever it memorised during training.

Retrieval-augmented generation is a pattern, not a product: at query time, you retrieve a handful of relevant pieces of your own data and insert them into the prompt before the LLM generates its answer. The term comes from Patrick Lewis and colleagues’ 2020 paper, Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks, which combined a pretrained sequence-to-sequence model with a neural retriever over a Wikipedia index and showed it beat purely parametric models on open-domain QA while producing more specific, factual answers. Six years later the core idea hasn’t changed, even though the tooling around it - embeddings, vector databases, rerankers - has matured a lot.

The reason RAG stuck as the default pattern for “make an LLM answer questions about our stuff” is that it separates two concerns that used to be tangled together: what the model knows how to do (reason, write, summarize) versus what it knows about (your product docs, your codebase, your support history). Fine-tuning bakes facts into weights, which means every time your knowledge base changes, you’re looking at a retraining cycle. RAG keeps facts in an index you can update in seconds. That’s the whole pitch, and it holds up in production more often than not.

Why Retrieval-Augmented Generation Matters in Production

I’ve shipped RAG systems for legal document review, internal engineering knowledge bases, and customer support deflection, and in every one of those, the client’s actual problem was never “we need an LLM.” It was “we need answers grounded in a specific, changing body of truth, with a way to check where the answer came from.” That’s a narrower and more useful framing than “add AI chat to the product.”

A few concrete reasons RAG earns its complexity over just prompting a bigger model from memory:

  • Currency. Your data changes daily; retraining a model doesn’t happen daily. Re-indexing a new document takes seconds.
  • Traceability. You can show the user which chunks the answer came from, which matters enormously for anything regulated or customer-facing - “here’s the paragraph in the contract” beats “trust me.”
  • Cost control. You pay to encode and index documents once, then pay per query for a handful of retrieved chunks - not for stuffing an entire corpus into every prompt.
  • Reduced (not eliminated) hallucination. Grounding the model in retrieved text measurably cuts down on fabrication versus asking it to answer from parametric memory alone, though it does not make hallucination impossible - the model can still misread or overgeneralize from what it retrieved.

None of that means RAG is free. It adds a retrieval subsystem with its own failure modes, its own latency budget, and its own evaluation surface, which I’ll get into below.

RAG vs. Fine-Tuning vs. Prompt Engineering vs. Long Context

“Should we use RAG or fine-tune?” is the single most common question I get on a first call, and it’s usually a false binary. There are four ways to get an LLM to behave correctly on your domain, they solve different problems, and production systems routinely use more than one.

Approach What it changes Update cost Best for Breaks down when
Prompt engineering The instructions and examples in context Instant, free Behaviour, format, tone; small fixed reference material The knowledge needed exceeds what you can paste in
RAG What the model sees at inference time Seconds - re-index one document Answering over a large, changing body of your own facts, with citations Facts needed are tiny and static, or change second-to-second
Fine-tuning The model’s weights Hours to days, plus a retraining pipeline Consistent output format, domain tone, latency/cost via a smaller model You’re trying to inject facts - this is the classic misuse
Long context Nothing; you just paste everything Instant Corpora small enough to fit, where simplicity wins Cost per query, and accuracy degradation as context grows

The rule I actually apply: fine-tuning changes how a model behaves, RAG changes what it knows about. If your complaint is “it doesn’t know our Q3 pricing,” that is never a fine-tuning problem. If your complaint is “it won’t stop writing in bullet points,” that is never a retrieval problem. Meta’s own guidance on fine-tuning Llama models makes the same split - fine-tuning is framed around adapting behaviour and domain style, with retrieval as the tool for external knowledge.

On cost, the comparison is less about headline numbers than about shape. Fine-tuning is a large up-front cost plus a recurring retraining cost every time your knowledge changes, and it buys you cheaper inference (shorter prompts, possibly a smaller model). RAG is a near-zero setup cost plus a permanent per-query tax, because every retrieved chunk is billed as input tokens on every single request. If your corpus changes weekly, RAG wins on total cost without it being close. If your corpus is frozen and your query volume is enormous, that calculus can genuinely flip.

The Four Levels of RAG Maturity

Yunfan Gao and colleagues’ survey Retrieval-Augmented Generation for Large Language Models: A Survey organises the field into three paradigms, and practice has since added a fourth. This is a useful ladder for figuring out where a given system actually sits:

The four levels of RAG maturity
  1. Naive RAGChunk, embed, retrieve top-k by cosine similarity, stuff into the prompt. The tutorial version, and where most stalled projects are stuck.
  2. Advanced RAGAdds pre- and post-retrieval steps: query rewriting, metadata filtering, hybrid search, reranking. Everything in the architecture section below lives here, and it is where the bulk of real quality gains come from.
  3. Modular RAGRetrieval becomes a swappable component in a larger pipeline, routing between multiple indexes or search strategies depending on the query.
  4. Agentic RAGRetrieval is no longer a fixed pipeline stage but a tool an agent decides to call, possibly several times, reformulating its query between attempts.

Most teams who tell me “RAG didn’t work for us” built level 1 and concluded the pattern was broken. It usually wasn’t.

RAG Architecture: From Query to Grounded Answer

The flow shown in the diagram above - user query, embed and retrieve, rerank, LLM plus context, grounded answer - is the shape almost every production RAG system takes, whether it’s backed by Pinecone, Weaviate, or plain Postgres. The interesting engineering decisions all live inside those boxes: how you chunk documents before embedding them, how you search the vector store, and whether you rerank before handing chunks to the model. I’ll walk through each.

Chunking Strategies That Actually Affect Retrieval Quality

Chunking is the least glamorous part of RAG architecture and the part that determines your ceiling on retrieval quality more than model choice or vector database choice ever will. The mistake I see most often is picking a fixed token count (say, 512 tokens) and splitting blindly, which routinely severs a sentence - or an entire idea - across two chunks, so neither chunk retrieves well on its own.

One page, both failure modes. Here is the shape of a refund clause I have indexed on real client corpora:

Refunds. Orders can be returned within 7 days with proof of purchase. Refunds are processed in 3 business days. During sale periods, refund processing takes up to 7 business days. Members return free of charge.

Chunked too big, the whole policy page becomes one 2,000-word chunk covering refunds, delivery, membership and opening hours. Its embedding is an average of four unrelated topics, so it sits near nothing in particular and a refund question never retrieves it.

Chunked too small, split one sentence per chunk, “refunds are processed in 3 business days” retrieves cleanly - and the sale-period exception, now a separate chunk, does not. The model answers “3 business days”, cites its source, and is wrong for six weeks of the year.

The second failure is the dangerous one, because nothing visibly breaks. The retrieval succeeded, the citation is real, and the answer is still incomplete.

What I do in practice:

  • Chunk by structure first, size second. Split on headings, paragraphs, or code function boundaries, then only fall back to a hard token limit for oversized sections. A markdown document should split on ## boundaries before it splits on token count.
  • Keep chunks in the low hundreds of tokens, with overlap. Small enough that a single chunk is topically coherent, with roughly 10-20% overlap between adjacent chunks so an idea that straddles a boundary still shows up whole in at least one chunk.
  • Attach context the chunk would otherwise lose. A chunk pulled from page 40 of a contract doesn’t know it’s a contract, or which section it’s in. Anthropic’s Contextual Retrieval writeup addresses exactly this: prepending a short (50-100 token) LLM-generated summary of the chunk’s place in the document before embedding it. In their internal benchmark, each layer compounds on the last:
Top-20 retrieval failure rate, Anthropic contextual retrieval benchmark
Naive chunking + embeddings
5.7%
+ contextual embeddings
3.7%
+ BM25 hybrid search
2.9%
+ reranking pass
1.9%
A 67% relative reduction over the naive baseline, and note where the steps are largest. This is the most convincing chunking-quality data point I have seen published, and it matches what I see qualitatively on client corpora: the reranking pass usually buys you more than any embedding model swap.

Vector Database Embeddings: Picking a Store

An embedding is just a vector - a list of floats - produced by a model trained so that semantically similar text ends up close together in that vector space. OpenAI’s embeddings documentation describes the standard retrieval recipe plainly: embed the query and the documents, rank documents by cosine similarity to the query, and use a vector database once you’re searching over more than a handful of documents rather than computing similarity by hand.

The store you pick matters less than people assume, but it’s not a non-decision:

  • Postgres + pgvector is my default when the client already runs Postgres and the corpus is in the low millions of chunks. You get vector similarity search (with HNSW or IVFFlat indexing, exact or approximate), inside the same transactional system as the rest of your application data - no separate service to operate, and you can join a vector search against relational filters (tenant ID, permissions, date ranges) in a single query.
  • Pinecone or Weaviate earn their keep when you need managed hybrid search out of the box, multi-tenant namespace isolation at real scale, or query throughput that would mean babysitting a database cluster. Both ship hybrid search as a first-class feature, which brings me to the next section.

Hybrid Search & Reranking: Why Vector Search Alone Isn’t Enough

A vector database returns the nearest chunk, never the right one. It measures distance in embedding space; it does not reason about your question. Three distinct misses follow from that, and they have different fixes:

Miss 1 - the vocabulary gap. The user asks “can I get a refund?” The policy chunk says “money is returned”; a delivery-charges chunk happens to use the literal word “refund” and wins on keyword overlap. Fix: hybrid search. Pure vector search is excellent at paraphrase and synonym matching and surprisingly bad at exact strings - error codes, part numbers, proper nouns, anything where the literal characters matter more than the meaning. Running a sparse keyword search (typically BM25) alongside the dense vector search and fusing the two ranked lists covers each method’s blind spot with the other’s strength.

Miss 2 - the question is too thin to locate anything. “7 days from when?” is four words with almost no semantic content, so its embedding sits near everything and close to nothing. Fix: query rewriting - a cheap model expands the question using conversation history before it is ever embedded. This is also what makes follow-up questions work at all.

Miss 3 - the answer lives in a chunk the question never points at. The user asks about their refund; the sale-period exception applies, but they have no idea the exception exists, so nothing in their wording aims at it. There is no clean retrieval fix for this one. You cannot phrase a query around a rule you do not know about. The real answer is letting the model search again after seeing the first result - which is the agentic RAG pattern, covered below.

Pinecone’s hybrid search docs frame it as combining “a keyword signal with a semantic signal in a single query,” either by storing dense and sparse vectors in one index and weighting them client-side, or by querying two indexes and merging results. Weaviate’s hybrid search exposes this as a single alpha parameter: alpha = 1.0 is pure vector search, alpha = 0.0 is pure BM25F keyword search, and anything in between blends the two - Weaviate’s default fusion method (Relative Score Fusion since v1.24) combines the two using their actual similarity scores rather than just rank position.

Reranking is the second lever, and it’s the one I underused early in my career because it looks like an optional nice-to-have. It isn’t. The pattern, as Pinecone’s reranking docs describe it, is: retrieve a wider candidate set from your vector/hybrid search (say, top 20-50), then pass the query and every candidate through a dedicated cross-encoder reranking model that scores each pair for actual semantic relevance, and keep only the top handful after that. A cross-encoder sees the query and the candidate together, which lets it catch relevance signals that a dense bi-encoder embedding - which encodes the query and the document completely independently - structurally cannot. Given how much of the Contextual Retrieval failure-rate improvement I cited above came specifically from adding the reranking step, I now treat rerank-before-generate as close to non-negotiable for anything beyond a demo.

RAG Examples: A Minimal Pipeline in TypeScript

Here’s the shape of a production-lean RAG pipeline in Node/TypeScript - chunking, embedding and indexing, then hybrid retrieval with reranking before the generation call. I’m using pgvector here because it’s the setup I reach for most often with clients who don’t need a dedicated vector database yet.

Indexing a document:

import { Pool } from "pg";
import OpenAI from "openai";

const db = new Pool({ connectionString: process.env.DATABASE_URL });
const openai = new OpenAI();

interface Chunk {
  text: string;
  sectionTitle: string;
  documentTitle: string;
}

async function embedAndStore(chunk: Chunk, documentId: string) {
  // Prepend lightweight context so the chunk is coherent on its own -
  // the "contextual retrieval" pattern, cheap when the summary is short.
  const contextualized = `${chunk.documentTitle} - ${chunk.sectionTitle}\n\n${chunk.text}`;

  const { data } = await openai.embeddings.create({
    model: "text-embedding-3-small",
    input: contextualized,
  });

  await db.query(
    `INSERT INTO chunks (document_id, text, section_title, embedding)
     VALUES ($1, $2, $3, $4)`,
    [documentId, chunk.text, chunk.sectionTitle, JSON.stringify(data[0].embedding)]
  );
}

Hybrid retrieval (vector similarity + full-text search) with a reranking pass before the chunks ever reach the LLM:

interface RetrievedChunk {
  id: string;
  text: string;
  score: number;
}

async function retrieve(query: string, topK = 6): Promise<RetrievedChunk[]> {
  const { data } = await openai.embeddings.create({
    model: "text-embedding-3-small",
    input: query,
  });
  const queryEmbedding = JSON.stringify(data[0].embedding);

  // Pull a wider candidate set from both vector similarity and keyword search,
  // then let the reranker - not raw cosine distance - make the final call.
  const { rows: candidates } = await db.query(
    `SELECT id, text,
            1 - (embedding <=> $1) AS vector_score,
            ts_rank_cd(to_tsvector('english', text), plainto_tsquery($2)) AS keyword_score
     FROM chunks
     WHERE embedding <=> $1 < 0.5
        OR to_tsvector('english', text) @@ plainto_tsquery($2)
     ORDER BY vector_score DESC
     LIMIT 30`,
    [queryEmbedding, query]
  );

  return rerank(query, candidates, topK);
}

async function rerank(
  query: string,
  candidates: { id: string; text: string }[],
  topK: number
): Promise<RetrievedChunk[]> {
  // Call out to a cross-encoder reranking model (Cohere Rerank, Pinecone's
  // hosted reranker, or a self-hosted cross-encoder all work here).
  const scored = await rerankApi.score({ query, documents: candidates.map((c) => c.text) });
  return scored
    .sort((a, b) => b.score - a.score)
    .slice(0, topK)
    .map((s) => ({ id: candidates[s.index].id, text: candidates[s.index].text, score: s.score }));
}

And the generation step, which is where the whole pipeline pays off or falls apart depending on how disciplined you are about what goes in the prompt:

async function answer(query: string): Promise<string> {
  const chunks = await retrieve(query);

  const context = chunks
    .map((c, i) => `[${i + 1}] ${c.text}`)
    .join("\n\n");

  const response = await anthropic.messages.create({
    model: "claude-sonnet-5",
    max_tokens: 1024,
    system:
      "Answer using only the numbered context below. Cite sources inline as [1], [2], etc. " +
      "If the context doesn't contain the answer, say so explicitly - do not guess.",
    messages: [{ role: "user", content: `Context:\n${context}\n\nQuestion: ${query}` }],
  });

  return response.content[0].type === "text" ? response.content[0].text : "";
}

That last system prompt line - “if the context doesn’t contain the answer, say so explicitly” - is doing more work than any architectural choice above it. A retrieval system that returns nothing relevant is a known, debuggable failure. A model that confidently fabricates an answer from irrelevant retrieved context is a silent one, and it’s the failure mode that erodes user trust fastest.

If your RAG pipeline is one tool among several that an autonomous agent calls - rather than the entire application - the retrieval and reranking logic above slots in as a single tool definition; I cover the surrounding orchestration, planning, and tool-calling loop in the agents guide. It’s also common to expose a retrieval tool like this over MCP so multiple clients (an IDE, a chat app, an internal agent) can share one retrieval backend without reimplementing it - see the MCP guide for that pattern.

Failure Modes I See in Production RAG Systems

Retrieval returns the wrong chunks and nobody notices until a customer complains. This is the most common failure by far, and it’s almost always a chunking or query-mismatch problem, not a model problem. Teams debug this by staring at the LLM’s output when they should be staring at what got retrieved. Log every retrieved chunk alongside every answer from day one.

The “lost in the middle” problem. Even with generous context windows, LLMs don’t weight every token in the prompt equally - content buried in the middle of a long context gets less attention than content near the start or end. Retrieving 20 chunks and dumping all of them into the prompt unranked is worse than retrieving and reranking down to the 4-6 that actually matter. This is also why “just use a bigger context window and skip retrieval” doesn’t scale the way it sounds like it should - more tokens in context isn’t free, and accuracy degrades as token count grows, a pattern Anthropic’s own context window documentation calls “context rot.”

Stale indexes. A document gets updated in the source system and nobody re-embeds it, so the RAG system confidently serves an outdated chunk. Treat re-indexing as a first-class pipeline step with monitoring, not a manual afterthought triggered by someone remembering to run a script.

Chunk boundaries that sever the answer. I covered this above, but it’s worth repeating as a failure mode: a numeric table split mid-row, or a procedure split mid-step, retrieves as two nonsensical fragments instead of one useful chunk.

Over-trusting the reranker or the embedding model on domain-specific jargon. General-purpose embedding models are trained on broad web text and can genuinely underperform on dense internal jargon - legal defined terms, internal product codenames, medical abbreviations. When I’ve seen this, hybrid search (which falls back to exact keyword matching) closed most of the gap without needing a fine-tuned embedding model.

No fallback for “I don’t know.” Covered in the code above, but worth calling out on its own: if you don’t explicitly instruct the model to admit when retrieved context doesn’t answer the question, it will usually try anyway.

Production Considerations: Cost, Latency, Security

Cost. Embedding generation is a one-time cost per document (or per re-index) and is rarely the line item that matters. Vector storage and query cost scale with corpus size and query volume and vary meaningfully between a self-hosted pgvector instance and a managed vector database’s per-query or per-pod pricing - check the vendor’s current pricing page before committing, since this changes often. The dominant cost in almost every RAG system I’ve built is the LLM generation call, because you’re paying input-token pricing on every retrieved chunk on every single query - this is the direct, ongoing cost of not reranking and trimming context aggressively.

Latency. A naive RAG pipeline chains embed query → vector search → rerank → LLM generation sequentially, and each hop adds real wall-clock time - a reranking call over 20-30 candidates is not free. For a chat UI, users tolerate a second or two before the first token streams. For a voice AI agent, that same latency chain is often the difference between a usable product and one that feels broken, because there’s no scrollback to hide behind while the user waits in silence - I go into the specific latency budgeting for that case in the voice AI agents guide. Caching embeddings for repeated queries and running retrieval and any non-dependent setup work in parallel are the first two latency levers I reach for.

Security: access control. The retrieval layer is a new place your access-control model has to be enforced correctly, and it’s easy to get wrong because it doesn’t look like a typical authorization boundary. If a vector store is queried without row-level filtering by tenant or user permissions, RAG will happily retrieve - and the LLM will happily surface - content the requesting user was never supposed to see. This isn’t a hypothetical: it’s the single most common security defect I review for in client RAG systems, and it’s caught by testing retrieval directly with adversarial queries scoped to a low-permission user, not by testing the chat UI.

Security: retrieval poisoning and indirect prompt injection. The second RAG-specific risk is that your index is an untrusted input channel. If anything that reaches your corpus can be influenced from outside - a support ticket a customer wrote, a scraped web page, a shared document - then an attacker can plant text designed to be retrieved and obeyed. The retrieved chunk arrives in the prompt looking exactly like your own trusted documentation, and the model has no reliable way to tell the difference. OWASP tracks this pair as LLM01: Prompt Injection and LLM04: Data and Model Poisoning in its GenAI Top 10. The mitigations that actually help are unglamorous: treat retrieved text as data rather than instructions in your prompt structure, restrict which sources can write into the index, and never let a retrieved chunk trigger a privileged action without a separate authorization check.

Evaluating a RAG System (Not Just the LLM)

In one line

A RAG system has two independent failure surfaces - retrieval and generation - and one end-to-end score cannot tell you which one broke. Measure them separately or you will spend weeks tuning the wrong half.

The mistake I see teams make is evaluating a RAG system the same way they’d evaluate a plain chatbot - by eyeballing whether the final answer sounds right. That conflates two independent failure surfaces. A RAG system can fail because retrieval didn’t find the right chunks, or because generation didn’t use the chunks it was given correctly, and you need separate measurements to tell which one broke.

For the retrieval half, build a labeled evaluation set - real queries mapped to the chunk IDs that should be retrieved for each - and measure:

  • Recall@k: of the chunks that should have been retrieved, what fraction actually showed up in the top k?
  • Precision@k: of the chunks retrieved, what fraction were actually relevant?

For the generation half, once you know the right context was retrieved, score the answer independently on:

  • Faithfulness / groundedness: is every claim in the answer actually supported by the retrieved context, or did the model add something not in there?
  • Answer relevancy: does the answer actually address the user’s question, even if it’s faithful to the context it was given?

I go deeper on building this kind of eval harness - labeled datasets, LLM-as-judge scoring, regression testing across model or prompt changes - in the LLM evaluation guide, since the discipline is the same whether you’re evaluating a RAG pipeline or any other LLM-backed feature. The RAG-specific addition is that you must evaluate retrieval and generation as two separate systems with two separate metrics, because a perfect retriever feeding a sloppy generator, and a sloppy retriever feeding a careful generator, produce equally bad end-to-end answers for completely different reasons - and only separate metrics tell you which one to fix.

Is RAG Still Relevant? Agentic RAG and Million-Token Context Windows

This comes up constantly now that frontier models advertise context windows in the hundreds of thousands to a million tokens: if you can just paste the whole corpus in, why maintain a retrieval pipeline at all?

For small, static corpora, you often shouldn’t - I say exactly that in the section below on when to skip RAG. But “just use long context” fails for three concrete reasons at any real scale:

  • Attention is not uniform across the window. Nelson Liu and colleagues’ Lost in the Middle showed that model performance is highest when relevant information sits at the very beginning or very end of the input context and degrades measurably when it’s buried in the middle - and that this holds even for models explicitly built for long contexts. Filling a million-token window doesn’t mean the model reads a million tokens equally well.
  • You pay for every token, every query. A million-token prompt billed on every request is a fundamentally different cost structure from retrieving six relevant chunks. Prompt caching softens this considerably for a stable corpus, but it doesn’t make it free.
  • You lose citations. Retrieval gives you a defensible answer to “where did this come from?” Stuffing the corpus gives you a model assertion and nothing to point at.

What has genuinely changed is the shape of the retrieval step. Agentic RAG - level 4 above - lets the model decide whether to retrieve at all, issue several reformulated searches, and judge whether what came back is sufficient before answering. In practice this means a question like “how did our refund policy change between versions?” can trigger two targeted retrievals rather than one blurry one. The retrieval, chunking, hybrid search and reranking engineering underneath is identical; what changes is who drives it. I cover that control loop in the agents guide.

So: retrieval as an architecture is more relevant than ever. Retrieval as a rigid pipeline stage is what’s being replaced.

RAG vs. MCP: Different Layers, Not Competitors

Because both acronyms show up in the same conversations, people ask which one to pick. They aren’t alternatives. RAG is a pattern for grounding answers in retrieved data. The Model Context Protocol is a transport standard for how a model client connects to tools and data sources.

Concretely: you can implement RAG with no MCP anywhere in the system (a function in your app queries pgvector directly - that’s every code sample in this guide). You can also expose that exact retrieval function as an MCP server, so a desktop AI client, your IDE, and an internal agent all query one retrieval backend instead of three reimplementations of it. The retrieval engineering is unchanged either way; MCP only decides who can reach it and how. See the MCP guide for when that indirection earns its keep.

When Not to Use RAG

RAG is not free architectural complexity, and I turn it down more often than people expect for someone whose day job is largely AI engineering.

Your corpus is small enough to just put in the prompt. With context windows now running into the hundreds of thousands to a million tokens on frontier models, if your entire knowledge base is a few hundred pages, just put it in the system prompt (with caching to control cost) and skip building a retrieval pipeline entirely. You avoid an entire class of retrieval failure modes for free. I’ve talked more than one client out of a RAG project for exactly this reason - the “sophisticated” answer wasn’t the better engineering decision.

You need a specific output format, tone, or behavior baked in, not more facts. That’s a fine-tuning problem, or in many cases just a better system prompt with a few examples, not a retrieval problem. RAG changes what the model can see; it doesn’t reliably change how the model behaves.

Your data changes so fast that indexing latency itself is the bottleneck. If the answer depends on data that’s seconds old - live pricing, live inventory - you likely want a direct API/tool call for that specific fact rather than routing it through an embedding-and-search pipeline built for relatively stable documents. This is a case for giving an agent a live-lookup tool instead of, or alongside, retrieval; see the agents guide for how that tool-selection decision typically gets made.

You don’t have anyone who will own retrieval quality over time. RAG systems degrade silently as source documents change, chunking assumptions stop matching new content types, and query patterns drift. If there’s no one who’s going to look at retrieval logs periodically, you’re signing up for a system that gets quietly worse for months before anyone notices. That operational reality is as much a part of the “should we build this” decision as anything in the expertise I typically get called in for - the architecture is the easy 20%.

Memory Hooks

The one-line version of everything above, for re-reading later rather than the whole guide.

Concept The hook
What RAG changes Fine-tuning changes how a model behaves; RAG changes what it knows about
Chunking The unit of everything downstream - the model never sees your documents, only chunks
The chunking question Never “what size?” but “what is one complete idea in this document?”
Retrieval A vector database returns the nearest chunk, never the right one
Miss 1 / 2 / 3 Vocabulary gap → hybrid search · thin question → query rewriting · unknown rule → let the model search again
Reranking A cross-encoder reads query and chunk together; an embedding never did
The funnel Retrieve wide (20-50), judge carefully, keep few (4-6)
Lost in the middle More tokens in context is not more attention on them
Evaluation Two failure surfaces, two metrics - a passing end-to-end score hides which half broke
The index is an input Anything that can write into your corpus can write into your prompt
When to skip it Corpus small enough to paste, behaviour problems, second-fresh data, or nobody to own quality

Frequently Asked Questions

What is retrieval-augmented generation (RAG) in simple terms?

RAG is a pattern where you retrieve relevant pieces of your own data - documents, tickets, code, transcripts - at query time and hand them to an LLM as context before it generates an answer. Instead of relying on what the model memorized during training, the model reads your data fresh on every request, which makes answers more current and traceable back to a source.

How is RAG different from fine-tuning?

Fine-tuning changes the model's weights to shift its behavior or style; RAG leaves the model untouched and changes what it sees at inference time. RAG is almost always the right first move for 'answer questions about our data' problems because it's cheaper to update (just re-index) and every answer can cite a source. Fine-tuning earns its cost when you need a specific output format, tone, or latency profile baked in, not when you just need the model to know more facts.

What's the difference between vector search and hybrid search in a RAG pipeline?

Vector search finds chunks whose embeddings are semantically close to the query embedding, which is great at synonyms and paraphrasing but weak on exact strings like error codes, SKUs, or names. Hybrid search runs a sparse keyword search (BM25) alongside the vector search and fuses the two ranked lists, so you keep semantic recall without losing exact-match precision. Most production RAG systems I've built end up hybrid by default.

Do I need a dedicated vector database, or can I use pgvector?

If you already run Postgres and your corpus is in the low millions of chunks, pgvector is a legitimate production choice - you get vector search, HNSW indexing, and transactional consistency with the rest of your data in one system. I reach for a dedicated vector database (Pinecone, Weaviate) when I need managed hybrid search, multi-tenant namespace isolation at scale, or query volume that would make me babysit a Postgres instance full-time.

How much does a production RAG system cost to run?

The three cost centers are embedding generation (usually a rounding error, paid once per document unless you re-chunk), vector storage/query (scales with corpus size and QPS, varies a lot by vendor and index type), and the LLM generation call, which is almost always the dominant cost because you're paying for every retrieved chunk as input tokens on every single query. Reranking and trimming context aggressively is a cost lever as much as a quality lever.

Is RAG still relevant now that models have million-token context windows?

Yes, for anything beyond a small static corpus. Long context does not remove the three reasons retrieval exists: you pay input-token pricing on every token of every query, model attention is not uniform across a long window (the 'lost in the middle' effect), and stuffing a corpus gives you no citations to point at. What has changed is that retrieval is increasingly a tool an agent chooses to call - agentic RAG - rather than a fixed pipeline stage. The underlying chunking, hybrid search and reranking work is the same either way.

What is the difference between RAG and MCP?

They operate at different layers and are not alternatives. RAG is a pattern for grounding a model's answer in retrieved data. MCP (Model Context Protocol) is a transport standard for how a model client connects to tools and data sources. You can build RAG with no MCP at all, or expose your retrieval function as an MCP server so several clients share one retrieval backend. MCP changes who can reach your retriever; it does not change how retrieval works.

Is ChatGPT a RAG model?

Not inherently. A base LLM is not a RAG system. But ChatGPT's browsing and file-upload features do implement the RAG pattern - they retrieve external content at query time and put it in context before generating. So the product uses retrieval-augmented generation as one of its features; the underlying model is not itself a RAG model.

What are the four levels of RAG?

The common maturity ladder is naive RAG (chunk, embed, top-k retrieve, stuff the prompt), advanced RAG (query rewriting, metadata filtering, hybrid search, reranking), modular RAG (retrieval as a swappable component with routing between indexes or strategies), and agentic RAG (retrieval as a tool an agent decides when and how to call). Yunfan Gao and colleagues' 2023 survey formalises the first three; agentic RAG is the practice-driven fourth. Most failed RAG projects I review built level one and concluded the pattern doesn't work.

Can a RAG system be attacked through its own documents?

Yes, and it is the RAG-specific risk teams most often miss. If anything that can be influenced from outside reaches your index - customer-written tickets, scraped pages, shared documents - an attacker can plant text engineered to be retrieved and then obeyed by the model. OWASP covers this as prompt injection and data poisoning in its GenAI Top 10. Defend it by structuring prompts so retrieved text is clearly data rather than instructions, restricting which sources can write into the index, and requiring a separate authorization check before any retrieved content triggers a privileged action.

How do I evaluate whether my RAG system is actually working?

Separate retrieval evaluation from generation evaluation. For retrieval, measure recall@k and precision@k against a labeled set of query-to-relevant-chunk pairs - did the right chunk even make it into context? For generation, use an LLM-as-judge or human review to score faithfulness (is the answer supported by the retrieved context) and answer relevancy separately, because a model can be perfectly faithful to irrelevant context and still give a useless answer.

Want help implementing this?

I take on a limited number of AI engineering engagements each quarter - RAG, agents, evals, or voice AI.

Talk to Me About AI →