Retrieval-Augmented Generation
Ground a model in your own data. Trace a question through chunking, retrieval, and generation stage by stage.
Language models have two well-known failure modes: they don’t know about your private or recent data, and they will confidently hallucinate an answer when they don’t know. Retrieval-Augmented Generation (RAG) addresses both by fetching relevant facts at query time and handing them to the model as context — so it can read the answer instead of recalling it.
RAG is the workhorse pattern of applied AI. Follow a single question through the pipeline, stage by stage.
User question
“What is our refund window for damaged items?”
Embed the question
text → [0.11, -0.83, 0.02, … ] (a vector)
Retrieve top-k chunks
vector search → 3 most similar policy snippets
Assemble the prompt
system + retrieved context + question
Generate
LLM reads context, writes an answer
Grounded answer + citations
“Damaged items can be returned within 30 days…” [policy §4]
The two phases
RAG splits cleanly into an offline phase and an online phase.
Indexing (offline, done once):
- Load your documents.
- Chunk them into passages.
- Embed each chunk and store the vectors in a vector database.
Retrieval + generation (online, per query):
- Embed the user’s question.
- Retrieve the top-k most similar chunks.
- Assemble a prompt: instructions + retrieved context + the question.
- Generate an answer grounded in that context.
- Return the answer with citations back to the source chunks.
def answer(question, k=4):
q_vec = embed(question)
chunks = vector_db.search(q_vec, k=k) # retrieve
context = "\n\n".join(c.text for c in chunks) # assemble
prompt = (
"Answer the question using ONLY the context below. "
"If the context is insufficient, say you don't know.\n\n"
f"Context:\n{context}\n\nQuestion: {question}"
)
return llm(prompt), [c.source for c in chunks] # generate + cite
Why grounding beats memorizing
Because the facts arrive at query time, RAG gives you three things fine-tuning can’t easily match:
- Freshness — update a document and the next answer reflects it, no retraining.
- Attribution — you can show where each claim came from, which is often a hard product requirement.
- Access control — retrieve only chunks the current user is allowed to see, and the model can only speak to those.
Where RAG quietly breaks
Most “the AI gave a bad answer” bugs are really retrieval bugs, not model bugs. If the right chunk never makes it into the context, no model can save you. The usual suspects:
- Bad chunking splits a fact across two chunks so neither is retrievable.
- Retrieval misses because the question and the answer are worded very differently (semantic gap).
- Context stuffing crams in too many low-relevance chunks, burying the good one and inflating cost.
Common remedies include hybrid search (combining keyword and vector search), re-ranking the retrieved set with a stronger model, and query rewriting to bridge the wording gap.
RAG vs. fine-tuning
A frequent interview question. The clean distinction:
RAG changes what the model knows; fine-tuning changes how the model behaves. Use RAG to inject facts (docs, policies, product data). Use fine-tuning to teach a consistent format, tone, or narrow skill. They are complementary, not competing — many production systems use both.
Pattern recognition
Variations
Worked problems
Pick chunk size and overlap
A product manual has long sections with step-by-step procedures. Current chunks are 2,000 tokens with no overlap. Retrieval often returns a chunk that mentions the right feature but buries the needed step. Propose a better starting point.
Approach. Large chunks blur multiple topics and waste prompt budget. Smaller semantic chunks with overlap increase precision while preserving boundary context.
Show solution
Start with heading-aware chunks around 500–800 tokens and 75–125 tokens of overlap. Keep procedure steps together when possible; do not split a numbered sequence in the middle just to hit an exact token count.
def chunk_paragraphs(paragraphs, max_chars=2_800, overlap_chars=400):
chunks = []
current = ""
for paragraph in paragraphs:
if len(current) + len(paragraph) > max_chars and current:
chunks.append(current.strip())
current = current[-overlap_chars:] + "\n" + paragraph
else:
current += "\n" + paragraph
if current.strip():
chunks.append(current.strip())
return chunksThen evaluate with known manual questions: did the supporting step appear in the top retrieved chunks, and did answer token cost fall?
Complexity. O(n) over paragraph text. The right chunk size is measured by retrieval success, not chosen once forever.
Debug retrieval quality
A RAG assistant gives a wrong answer. The generated answer is fluent, but the retrieved chunks do not contain the correct policy. List the debugging steps before changing models.
Approach. Isolate retrieval from generation. If the right evidence is absent, fix indexing, search, filters, or query transformation first.
Show solution
Work the pipeline backward:
- Confirm the source document actually contains the policy and is indexed.
- Check chunking: is the answer split across chunks or buried in a huge chunk?
- Run the query against a flat or high-recall search to see if ANN missed it.
- Inspect metadata filters: tenant, role, language, freshness, document status.
- Try hybrid search for exact policy names or IDs.
- Retrieve more candidates and re-rank.
- Add the case to a retrieval eval set with the expected supporting chunk IDs.
def missing_support(expected_chunk_ids, retrieved_chunk_ids):
expected = set(expected_chunk_ids)
retrieved = set(retrieved_chunk_ids)
return sorted(expected - retrieved)Only after the right chunk appears in context should you tune the generation prompt or model.
Complexity. O(k) for comparing expected and retrieved IDs. The process saves time by debugging the stage that actually failed.
Enforce citations and grounding
A legal-doc assistant must answer with citations. If the retrieved context does not support the answer, it should say it cannot determine the answer. Design the prompt and validation flow.
Approach. Give each chunk an ID, require claims to cite IDs, and validate that cited IDs were actually retrieved. Add an unsupported-answer path.
Show solution
def build_grounded_prompt(question, chunks):
context = "\n\n".join(
f"[chunk:{c['id']}]\n{c['text']}" for c in chunks
)
return (
"Answer using only the chunks below. Every factual claim must cite "
"one or more chunk IDs like [chunk:abc]. If the chunks do not support "
"the answer, say: Cannot determine from provided sources.\n\n"
f"{context}\n\nQuestion: {question}"
)
def validate_citations(answer, retrieved_ids):
cited = {part.split("]")[0] for part in answer.split("[chunk:")[1:]}
cited = {f"chunk:{x}" for x in cited}
allowed = {f"chunk:{x}" for x in retrieved_ids}
return cited <= allowed and bool(cited or "Cannot determine" in answer)This validator is intentionally simple; production code should parse citations more robustly and may run a separate support checker. The key invariant is that answers cannot cite sources that were not retrieved.
Complexity. O(c + a) for chunks and answer length. Grounding is a contract between retrieval, prompt, and validation.