- AI
- RAG
- LLM
- Python
- Generative AI
What Is RAG? Retrieval-Augmented Generation Explained Simply (with Python)
RAG (retrieval-augmented generation) means searching your own data for the passages that answer a question, then giving those passages to a large language model along with the question. The model answers from that context instead of from memory, so it can use private, recent information and cite its sources, with no retraining.
Why do LLMs need RAG?
A language model only knows what was in its training data. That causes three problems in real applications:
- It doesn't know your data. Your company wiki, product docs, contracts or support tickets were never in the training set.
- Its knowledge is frozen. Anything that changed after the training cutoff is missing or out of date.
- It answers confidently anyway. Ask about something it doesn't know and it may produce a fluent, wrong answer, often called a hallucination.
You could paste every document into the prompt, but context windows are limited, long prompts cost more and get slower, and models attend less reliably to details buried in very long inputs. RAG solves this by sending only the few passages that matter for this question.
Think of it as an open-book exam: the model is still the student writing the answer, but retrieval hands it the right pages first.
How does a RAG pipeline work?
A RAG system has two halves. Ingestion prepares your documents once (and again whenever they change). Querying runs on every question.
| Stage | Step | What happens |
|---|---|---|
| Ingest | Load | Read PDFs, web pages, docs, tickets; extract clean text and keep metadata (title, URL, date, permissions) |
| Ingest | Chunk | Split text into passages of a few hundred tokens, usually with a small overlap |
| Ingest | Embed | Turn each chunk into a vector with an embedding model |
| Ingest | Store | Save vectors, chunk text and metadata in a vector index or database |
| Query | Embed | Turn the user's question into a vector with the same embedding model |
| Query | Retrieve | Find the top-k most similar chunks (often k = 3 to 10), optionally filtered by metadata |
| Query | Rerank | Optionally re-score the candidates with a reranker model and keep the best few |
| Query | Prompt | Put the chunks into the prompt with instructions: answer only from this context, cite sources |
| Query | Generate | The LLM writes the answer and references the chunks it used |
The quality of the final answer is capped by retrieval. If the right chunk is not in the prompt, no model can answer correctly from it, which is why most RAG work is really search work.
How should you chunk documents?
Chunking decides what a "passage" is, and it has more effect on answer quality than most people expect.
| Choice | Typical starting point | Trade-off |
|---|---|---|
| Chunk size | 300–800 tokens | Too small loses context; too large dilutes the match and wastes prompt space |
| Overlap | 10–20% of the chunk | Keeps sentences that cross a boundary findable from both sides |
| Split points | Headings, then paragraphs, then sentences | Splitting on structure keeps ideas together; fixed character cuts break them |
| Metadata | Title, section, URL, date, access rights | Enables filters, citations and permission checks |
Practical tips from building these systems:
- Prepend context to each chunk. A chunk that says "It expires after 30 days" is useless alone. Store it as "Refund policy → Store credit: It expires after 30 days".
- Clean the text first. Headers, footers, navigation menus and broken tables from PDF extraction add noise that ends up in the prompt.
- Keep tables and code blocks whole where you can; splitting them mid-row produces chunks nobody can use.
There is no universally best chunk size. Pick a sensible default, build a small test set of real questions, and adjust based on what retrieval actually returns.
What are embeddings and vector databases?
An embedding is a list of numbers (a vector) that represents the meaning of a piece of text. Texts with similar meaning get vectors that point in similar directions, so "How do I reset my password?" lands close to "Steps to change your login credentials" even though they share few words.
Similarity is usually measured with cosine similarity: the cosine of the angle between two vectors, where 1 means same direction and values near 0 mean unrelated.
A vector database stores those vectors and finds the nearest ones quickly. Exact search compares the question with every vector, which is fine for thousands of chunks. For millions, databases use approximate nearest neighbour (ANN) indexes such as HNSW, trading a tiny amount of accuracy for a large speed-up.
Common options, with no single right answer:
- In-memory or a local library (for example Chroma or FAISS): prototypes and small datasets.
- pgvector: a PostgreSQL extension, good when your app already uses Postgres and you want vectors next to your relational data and permissions.
- Managed vector databases (for example Pinecone, Weaviate, Qdrant): large collections, high query volume, or teams that do not want to operate the infrastructure.
Many production systems also use hybrid search: combine vector similarity with keyword search (BM25) so exact terms such as product codes, error messages and names are not missed.
How do you build a simple RAG pipeline in Python?
Here is the whole idea in a few lines. embed() and llm() stand for whichever embedding model and LLM API you use; the retrieval logic is plain NumPy.
import numpy as np
def embed(texts: list[str]) -> np.ndarray:
"""Call your embedding model; return one vector per text."""
...
def llm(prompt: str) -> str:
"""Call your chat model; return its reply."""
...
# --- Ingest (run once) ---
chunks = [
"Refunds: items can be returned within 30 days of delivery.",
"Refunds: store credit expires 12 months after it is issued.",
"Shipping: orders over 999 rupees ship free within India.",
]
vectors = embed(chunks)
vectors = vectors / np.linalg.norm(vectors, axis=1, keepdims=True)
# --- Query (run per question) ---
def answer(question: str, k: int = 2) -> str:
q = embed([question])[0]
q = q / np.linalg.norm(q)
scores = vectors @ q # cosine similarity
top = np.argsort(scores)[::-1][:k] # best k chunks
context = "\n".join(f"[{i + 1}] {chunks[j]}" for i, j in enumerate(top))
prompt = (
"Answer using only the context below. Cite sources like [1]. "
"If the answer is not in the context, say you don't know.\n\n"
f"Context:\n{context}\n\nQuestion: {question}"
)
return llm(prompt)
print(answer("How long do I have to return something?"))
Normalising every vector to length 1 makes the dot product equal to cosine similarity, so one matrix multiplication scores every chunk at once. Swap the NumPy array for a vector database when the collection grows; the shape of the code stays the same.
RAG vs fine-tuning vs prompting: which should you use?
| Prompting | RAG | Fine-tuning | |
|---|---|---|---|
| What it changes | The instructions and examples you send | The context you send, retrieved per question | The model's weights |
| Best for | Format, tone, simple tasks | Facts from your own, changing data, with citations | A consistent style, output format or narrow skill |
| Updating knowledge | Edit the prompt | Re-index the changed documents | Retrain |
| Cost to start | Lowest | Moderate: ingestion and a vector store | Highest: data preparation and training runs |
| Shows sources | No | Yes | No |
Start with prompting. Add RAG when the model needs knowledge it doesn't have. Consider fine-tuning when prompting and RAG work but the model still does not follow your format or style reliably. They also combine: a fine-tuned model can still sit behind a RAG pipeline.
How do you evaluate a RAG system?
"It looked right in the demo" is not evaluation. Build a test set of 30 to 100 real questions with the answer and the source passage for each, and measure the two halves separately:
| Metric | Measures | Question it answers |
|---|---|---|
| Retrieval recall at k | Retrieval | Is the correct chunk in the top k results? |
| Precision / MRR | Retrieval | Are the right chunks ranked near the top? |
| Faithfulness (groundedness) | Generation | Is every claim in the answer supported by the retrieved context? |
| Answer relevance | Generation | Does the answer actually address the question? |
| Correctness | End to end | Does it match the expected answer? |
Generation metrics are often scored by another LLM acting as a judge, with a human spot-checking a sample. Re-run the test set whenever you change chunking, the embedding model, k, the prompt or the LLM, so you know whether a change helped.
What are common RAG failures and how do you fix them?
| Symptom | Likely cause | Fix |
|---|---|---|
| Right document exists, wrong chunks retrieved | Poor chunking or weak embeddings | Split on structure, add titles to chunks, try hybrid search or a better embedding model |
| Exact names, codes or error messages missed | Pure vector search | Add keyword (BM25) search and merge the results |
| Right chunks retrieved, wrong answer | Prompt too vague or too much noise | Fewer, reranked chunks; clearer instructions; ask for citations |
| Confident answer that is not in the data | Model fills gaps from memory | Instruct it to say "I don't know"; check faithfulness |
| Outdated answers | Index not refreshed | Re-ingest on change; store dates and prefer recent chunks |
| Users see documents they should not | No access control in retrieval | Store permissions as metadata and filter before retrieval |
What is agentic RAG?
Basic RAG retrieves once and answers. Agentic RAG lets the model decide how to search: it can rewrite the question, split it into sub-questions, call retrieval as a tool several times, query different sources (a vector store, a SQL database, a web search), and check whether it has enough information before answering.
It handles multi-step questions such as "Compare our refund policy with last year's" much better, at the cost of more LLM calls, higher latency and more ways to go wrong. Get simple retrieval working and measured first, then add agent behaviour where single-shot retrieval clearly falls short.
Key takeaway
RAG is search plus a language model: retrieve the passages that answer the question, put them in the prompt, and make the model answer only from them with citations. Spend your effort on clean text, sensible chunks, the right embedding model and a test set, because retrieval quality sets the ceiling for every answer. For a one-page visual of the pipeline and the surrounding ML and GenAI concepts, see the free AI/ML and GenAI handwritten notes (the RAG pipeline is on page 20), and brush up on the language with the Python handwritten notes. If you want a RAG system or AI assistant built for your own data, see AI engineering services.
Frequently asked questions
What is RAG in AI?
RAG (retrieval-augmented generation) is a technique where an application first searches a knowledge source, such as company documents, for passages relevant to a question, then gives those passages to a large language model together with the question. The model writes its answer from that retrieved context, so it can use private or recent information and cite where the answer came from.
What is the difference between RAG and fine-tuning?
RAG adds knowledge at question time by retrieving documents and putting them in the prompt, so updating knowledge means updating the documents. Fine-tuning changes the model's weights by training it on examples, which is better for teaching a style, format or narrow skill than for adding facts. Most teams that need a model to know their data start with RAG.
What are the steps in a RAG pipeline?
Ingestion runs ahead of time: load documents, split them into chunks, turn each chunk into an embedding vector and store the vectors in a vector database. At query time: embed the question, retrieve the most similar chunks, optionally rerank them, put the best ones in the prompt and ask the LLM to answer only from that context, with citations.
Which vector database should I use for RAG?
For a small project, an in-memory index or a local library such as Chroma is enough. If you already run PostgreSQL, the pgvector extension keeps vectors next to your other data. Managed services such as Pinecone, Weaviate or Qdrant Cloud suit large collections or teams that do not want to run the infrastructure. Retrieval quality depends more on chunking and embeddings than on the database.
Does RAG stop LLM hallucinations?
It reduces them but does not remove them. If retrieval misses the right passage, the model may still guess, and it can misread a correct passage. Instruct the model to answer only from the context and to say it does not know otherwise, require citations, and measure faithfulness on a test set.