RAG: Retrieval-Augmented Generation
How RAG fetches permitted, fresh text and puts it in the prompt so an LLM answers with citations, and why chunking and retrieval decide the quality.
A large language model (LLM) only knows what was in its training data, frozen at a cutoff and with no record of where any fact came from. RAG (retrieval-augmented generation) fixes that at question time: search your own documents for the passages that answer the question, paste them into the prompt with source labels, and instruct the model to answer only from them and cite them. The model supplies language and reasoning; the retrieval step decides which facts it can see, so most RAG quality problems are search problems.
Context
The name comes from a 2020 paper by Lewis and colleagues at Facebook AI Research, which paired a neural retriever over Wikipedia with a text generator and showed that the pair answered knowledge-heavy questions better than a much larger model working from memory. Once ChatGPT made LLMs mainstream at the end of 2022, the same idea became the default way to point a model at private or changing data: company wikis, help centres, contracts, code, tickets. Its alternatives each fall short on one axis: fine-tuning teaches style and format far more reliably than it teaches facts, and retraining for every document change is not practical.
You have used it every time an assistant answered with numbered sources, a support bot quoted the help-centre article it relied on, or a "chat with this PDF" tool answered questions about a file it was never trained on. Stripped to the minimum, it is a prompt with retrieved text in it:
System: Answer only from the sources below. Cite them as [n].
If they do not contain the answer, say you do not know.
Sources:
[1] (Refund policy, updated 2026-08) Annual plans can be refunded
within 30 days of purchase...
[2] (Billing FAQ) Monthly plans are not refundable, but you can
cancel at any time...
Question: Can I get my money back on a yearly plan?- Chunk
- A passage cut from a document, small enough that several fit in a prompt and focused enough to match one question.
- Embedding
- A vector of numbers representing the meaning of a text, so that texts about the same thing end up close together.
- BM25 (Best Matching 25)
- The classic keyword ranking function behind most search engines: rewards rare query words that appear often in a passage.
- Reranker
- A slower model that scores each (question, chunk) pair together and reorders the candidates the fast search returned.
- Grounded / faithful
- An answer whose every claim is supported by the retrieved sources, as opposed to filled in from the model’s memory.
Why it matters
RAG is how most products put an LLM in front of real data, and the parts that make it work are ordinary engineering: indexing pipelines, search relevance, access control, caching and evaluation. When a RAG feature gives a confident wrong answer, the cause is usually that the right passage was never retrieved, or that a passage the user was not allowed to see was. Both are fixed in the retrieval layer, not by changing the model or rewording the prompt.
Two pipelines: indexing and query
A RAG system is two pipelines that share an index. The indexing pipeline runs offline whenever documents change: load them, strip boilerplate, split them into chunks, embed each chunk, and store the vector together with the text, a keyword index entry and metadata such as tenant, permissions, source URL and date. The query pipeline runs on every question: turn the question into a standalone search query, retrieve candidates the user is allowed to see, rerank them, assemble a prompt with numbered sources, and generate an answer that cites them.
The query rewrite step matters more than it looks. In a conversation, the user's third message is often "and what about yearly ones?", which matches nothing on its own. A cheap model call that rewrites it into "refund policy for annual plans" using the chat history turns a guaranteed miss into a good search.
Chunking and the index
Chunk size is a trade-off between precision and context. Small chunks match a specific question sharply but lose the sentence that explains them; large chunks keep context but dilute the embedding with unrelated text and use up the prompt budget. Most systems land between a few hundred and roughly a thousand tokens, split along the document's own structure where it has one.
| Strategy | How it splits | Good for | Watch out for |
|---|---|---|---|
| Fixed + overlap | Every N tokens, repeating the last 10–20% | Unstructured text, a quick baseline | Cuts mid-sentence and mid-table; overlap duplicates hits |
| Structure-aware | By headings, then pack paragraphs up to a budget | Docs, wikis, Markdown, HTML | Very long sections still need a size cap |
| Semantic | Where the topic shifts, using embedding similarity | Long narrative text without headings | Slower to index; boundaries vary between runs |
| Parent / child | Search small chunks, return their larger parent | Precise matching with enough context | Two index levels to keep in sync |
type Chunk = {id: string; docId: string; heading: string; text: string}
// split on headings, then pack paragraphs up to a size budget
export function chunk(docId: string, md: string, maxChars = 2000) {
const chunks: Chunk[] = []
for (const section of md.split(/\n(?=#{1,3} )/)) {
const heading = section.match(/^#{1,3} (.*)/)?.[1] ?? ''
let buf = ''
const flush = () => {
const id = docId + '#' + chunks.length
chunks.push({id, docId, heading, text: buf})
}
for (const para of section.split(/\n{2,}/)) {
if (buf && buf.length + para.length > maxChars) {
flush()
buf = buf.slice(-200) // small overlap carries context
}
buf += (buf ? '\n\n' : '') + para
}
if (buf) flush()
}
return chunks
}
// embed heading + text, not text alone: "Refunds > Annual plans" helpsRetrieval: hybrid search and reranking
Vector search finds passages that mean the same thing in different words, which is exactly what keyword search misses. It is weak where keyword search is strong: error codes, product SKUs, people's names, version numbers and rare jargon, which an embedding blurs into "something similar". Running both and merging the lists covers both failure modes. The usual merge is RRF (reciprocal rank fusion, 2009): each chunk scores the sum of 1 / (k + rank) over the lists it appears in, with k around 60, so agreement between the two searches matters more than either raw score, and scores on different scales never have to be compared.
Both searches score the query and a chunk separately, which is what makes them fast. A reranker (a cross-encoder) reads the query and one chunk together and scores how well that chunk answers that question, which is much more accurate and much slower, so it only sees the few dozen fused candidates and keeps the best handful for the prompt.
// illustrative interfaces: plug in your store, index and model
export async function answer(user: User, question: string, history: Msg[]) {
const query = await rewrite(question, history) // standalone query
const filter = {tenantId: user.tenantId, groups: user.groups}
const [byVector, byKeyword] = await Promise.all([
vectors.search(await embed(query), {filter, limit: 40}),
keywords.search(query, {filter, limit: 40}), // BM25
])
const fused = rrf([byVector, byKeyword]).slice(0, 30)
const top = (await rerank(query, fused)).slice(0, 5)
const sources = top
.map((c, i) => `[${i + 1}] (${c.docTitle})\n${c.text}`)
.join('\n\n')
return llm.generate({
model: 'your-model',
system:
'Answer only from the sources. Cite them as [n]. ' +
'If they do not contain the answer, say you do not know.',
prompt: `Sources:\n${sources}\n\nQuestion: ${question}`,
})
}
function rrf(lists: Chunk[][], k = 60): Chunk[] {
const scores = new Map<string, {chunk: Chunk; s: number}>()
for (const list of lists) {
list.forEach((chunk, rank) => {
const s = (scores.get(chunk.id)?.s ?? 0) + 1 / (k + rank + 1)
scores.set(chunk.id, {chunk, s})
})
}
return [...scores.values()].sort((a, b) => b.s - a.s).map(x => x.chunk)
}Prompt assembly
Number the sources and require citations by number, so an answer can be checked against what was retrieved and the interface can link each claim to its document. Tell the model what to do when the sources are insufficient; without that, it fills the gap from memory, which is the hallucination RAG was meant to prevent. And put retrieved text in a clearly delimited data section, never in the system instructions: a document can contain text that reads like an instruction.
When answers go wrong, and measuring it
Every bad answer has one of two causes. A retrieval miss: the passage with the answer was not in the prompt. A generation miss: it was, and the model ignored, misread or contradicted it. They have different fixes, so the first debugging step is always to look at what was retrieved.
- 1Log, for every answer, the rewritten query, the candidate IDs with their scores before and after reranking, and the final prompt. Without this, debugging RAG is guesswork.
- 2For a bad answer, search the index by hand for the passage that should have answered it. If it does not exist, the problem is ingestion: a document never indexed, deleted content still present, or a table mangled by the parser.
- 3If it exists but was not retrieved, compare the query with its wording: a missing synonym points at vector search, a missing exact identifier at keyword search, a filter excluding it at metadata.
- 4If it was retrieved but ranked out of the top results, look at the reranker and the chunk boundaries. If it was in the prompt and the answer still contradicts it, that is a generation miss: tighten the instructions, reduce the number of sources, or use a stronger model.
Models also use their context unevenly. Liu and colleagues (2023) found that models were best at using information at the beginning or end of a long prompt and worst in the middle, so stuffing twenty chunks in "to be safe" can lower accuracy while raising cost. A few well-ranked chunks usually beat many mediocre ones.
| Measure | Question it answers | How to get it |
|---|---|---|
| Recall@k | Was the right passage in the top k? | A labelled set of questions with known source passages |
| Faithfulness | Is every claim supported by the sources? | Human review or a model grader checking claims against sources |
| Answer relevance | Does it answer what was asked? | Human ratings or a grader with a rubric |
| Abstention | Does it say "I don’t know" when it should? | Include questions the corpus cannot answer |
Pitfalls
- Enforcing permissions in the prompt
An instruction not to reveal restricted documents is a request, not a control. Once a passage is in the prompt, the model can quote it. Filter by tenant and permissions inside the search, so that forbidden text is never retrieved.
- Vector search alone
Embeddings smooth over exact strings, so questions about an error code, an invoice number or a function name retrieve passages that are merely similar. Add keyword search and fuse the two lists; it is cheap and fixes a whole class of misses.
- An index that drifts from the source
If re-indexing only adds documents, edited and deleted content keeps being retrieved and the bot confidently quotes last year's policy. Index with stable document IDs, replace all of a document's chunks on change, and delete them when the document goes.
- Tuning prompts before looking at retrieval
Rewording the system prompt cannot help when the answer was never in it. Check retrieval first; most quality problems in RAG are search problems, and the logged candidate list shows which one you have.
- Treating retrieved text as trusted
Retrieved documents are input, sometimes written by outsiders: a support ticket or a web page can contain text addressed to the model. Keep sources in a delimited data block, never give the model tools it can trigger from document content alone, and see prompt injection for the full defence.
Interview questions
Q1What problem does RAG solve that fine-tuning does not?
It gives the model fresh, private, permission-checked facts at question time, with sources it can cite. Fine-tuning changes the model's behaviour and style well but is an unreliable way to add facts, cannot be updated per document change, cannot respect per-user permissions and gives no attribution. The two combine: fine-tune for format, retrieve for facts.
Q2Walk me through implementing a RAG assistant over a company wiki.
Indexing: pull pages through the wiki's API with their permissions, strip navigation, chunk by headings to a few hundred tokens with the page title and heading path prepended, embed each chunk, and store vector, text, keyword index entry and metadata, re-indexing pages on change. Query: rewrite the question using chat history, run vector and keyword search filtered by the user's groups, fuse with RRF, rerank the top 30 to 5, and prompt with numbered sources, requiring citations and "I don't know" when unsupported. Log everything and build a labelled question set to measure recall and faithfulness.
Q3How do you choose a chunk size?
Empirically, against a labelled set of questions, starting from structure-aware chunks of a few hundred tokens. Too small and the chunk loses the context that makes it meaningful; too large and its embedding averages several topics and wastes prompt budget. I measure recall@k for a few sizes, and use parent-child retrieval if I need both precise matching and wider context.
Q4What happens when a user asks about a document they are not allowed to see?
With the filter in the right place, nothing is retrieved, so the model answers that it does not know, exactly as for a document that does not exist. That requires permission metadata on every chunk and the filter applied inside the search before ranking. If permissions were only enforced in the prompt, the passage would be in the context and could leak.
Q5Why combine keyword and vector search?
They fail on different queries. Vector search handles paraphrases and synonyms; keyword search handles exact identifiers, names and rare terms that embeddings blur. Reciprocal rank fusion merges the two ranked lists by position, so I do not have to calibrate scores on different scales, and passages both searches agree on rise to the top.
Q6The assistant gave a confident wrong answer. How do you debug it?
First look at what was retrieved for that question. If the right passage was not in the prompt, it is a retrieval miss, and I check whether it is indexed at all, whether a filter excluded it, and which search should have found it. If it was in the prompt, it is a generation miss, and I look at the instructions, the number and order of sources, and the model. Then I add the case to the eval set.
Q7How would you evaluate a RAG system?
Separately for retrieval and generation. A labelled set of questions with known source passages gives recall@k for retrieval; faithfulness (every claim supported by the sources) and answer relevance score the generated text, by people or by a grader model spot-checked by people. Include unanswerable questions to test that it abstains, and run the set on every change to chunking, models or prompts.
Q8Context windows are now very large. Do we still need RAG?
For a small, stable corpus with one audience, putting it all in the prompt with caching can be simpler. For anything larger than the window, changing daily, shared between users with different permissions, or queried often, retrieval stays cheaper, faster and safer, and models still use a few relevant passages better than a huge prompt where the answer sits in the middle.
- RAG retrieves relevant, permitted passages at question time and makes the model answer from them with citations; fine-tuning shapes style, retrieval supplies facts.
- Two pipelines share one index: indexing (load, clean, chunk, embed, store with metadata) and query (rewrite, retrieve, rerank, prompt, generate).
- Chunk along document structure, prepend titles and headings, and keep re-indexing in sync with edits and deletions.
- Filter by tenant and permissions inside the search; a prompt instruction is not access control.
- Hybrid search plus reciprocal rank fusion and a reranker fixes most misses; a few well-ranked chunks beat many.
- Debug by checking retrieval first, and measure retrieval and generation separately: recall@k, faithfulness, relevance, abstention.