Most teams ship a RAG demo in a weekend and a broken RAG product in production. The pipeline looks identical on a whiteboard, but the difference is the seven stages where things silently go wrong. This lesson walks the full pipeline end-to-end — extraction, chunking, embedding, retrieval, reranking, prompt assembly, generation — and shows you exactly where Perplexity, Glean, and Notion AI invested to make theirs actually work.
Learning Objectives
After this lesson, you will be able to:
Follow a user's question through every step of the RAG pipeline -- from loading documents, to searching, to generating an answer -- and understand what happens at each stage
Spot how a failure at any one stage (bad chunking, poor retrieval, weak prompt) cascades and ruins the final answer
Write prompt templates that tell the AI 'answer ONLY from these documents' so it stays grounded instead of making things up
Debug RAG failures systematically from left to right -- extraction first, then chunking, then retrieval, then generation -- instead of jumping straight to prompt tuning
You know why RAG exists. Now let's go deep on how each stage actually works in code.
This is the "putting it all together" lesson. Everything you have learned so far -- embeddings, chunking, vector search -- connects here into one working system. Take your time and enjoy watching the pieces fit together.
Your Reflection
Saves automatically
What’s one thing you learned? What’s still confusing?
A user asks a natural language question: "What is our refund policy for enterprise customers?" This is the starting point of every RAG query. The system must find the answer in its knowledge base and return a grounded, cited response.
The question is passed through an embedding model to produce a dense vector. This vector captures the semantic meaning of the question -- not just its keywords, but its intent. The same model that was used to embed documents must be used here.
The query vector is compared against all document vectors in the database using approximate nearest neighbor search (HNSW or IVF). In milliseconds, the system identifies the chunks whose meaning is closest to the question.
The most similar chunks are pulled from the database -- typically the top 3 to 10. Each chunk includes the original text, similarity score, and metadata (source document, section, page number). These are the raw materials for the answer.
A cross-encoder re-ranker scores each query-chunk pair more carefully than the initial vector search. This second pass reorders the results, promoting the truly relevant chunks and demoting false positives. It catches nuances that embedding similarity misses.
The system constructs the final LLM prompt: a system message (instructions on how to answer), the retrieved context (the top-ranked chunks), and the user question. The prompt explicitly tells the model to answer only from the provided context and to cite sources.
The assembled prompt is sent to the LLM, which generates an answer grounded in the retrieved context. Because the relevant facts are right there in the prompt, the model produces accurate, specific answers instead of hallucinating from its training data.
The final answer is returned to the user along with source citations -- which documents, sections, and page numbers the answer came from. This makes the response verifiable and builds trust. The user can click through to the original source to verify.
Raw documents come in many formats: PDFs, HTML pages, Markdown files, Word documents, Slack messages, Notion pages, database records. The first stage converts them all into a uniform text representation.
This is harder than it sounds. PDFs have headers, footers, page numbers, and multi-column layouts. HTML has navigation bars, ads, and boilerplate. The quality of your text extraction directly limits the quality of everything downstream.
pythonrunnable cell
1
2
3
4
5
6
7
8
# Common document loaders
from langchain.document_loaders import (
PyPDFLoader, # PDFs
UnstructuredHTMLLoader, # HTML
NotionDBLoader, # Notion
CSVLoader, # CSV files
)
docs = PyPDFLoader("report.pdf").load()
Try it! Install langchain (pip install langchain) and try loading a PDF you have on your computer. Print the first 500 characters of the extracted text. You will immediately see the messiness -- headers, footers, page numbers mixed in -- that the next stage must clean up.
Split cleaned documents into appropriately-sized passages using the strategies from the previous lesson. The choice of chunking strategy depends on document type, embedding model, and query patterns.
Key parameters to set:
Chunk size: Typically 256-1024 tokens
Overlap: Typically 10-20% of chunk size
Strategy: Recursive, semantic, or document-type-specific
Each chunk is passed through an embedding model to produce a dense vector. This is the most expensive stage computationally (for large corpora) and the most impactful for retrieval quality.
For a corpus of 1 million chunks at 500 tokens each, embedding with OpenAI text-embedding-3-small costs approximately $10 and takes about 30 minutes with batched API calls.
pythonrunnable cell
1
2
3
4
5
6
7
8
9
10
11
12
from openai import OpenAI
client = OpenAI()
def embed_batch(texts, model="text-embedding-3-small"):
response = client.embeddings.create(input=texts, model=model)
return [e.embedding for e in response.data]
# Process in batches of 100 for API efficiency
vectors = []
for i in range(0, len(chunks), 100):
batch = [c.page_content for c in chunks[i:i+100]]
vectors.extend(embed_batch(batch))
Store the vectors in a vector database along with the original text and metadata. The metadata is crucial -- it enables filtering, source attribution, and deduplication.
pythonrunnable cell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import chromadb
client = chromadb.Client()
collection = client.create_collection("knowledge_base")
collection.add(
ids=[f"chunk_{i}" for i in range(len(chunks))],
embeddings=vectors,
documents=[c.page_content for c in chunks],
metadatas=[{
"source": c.metadata["source"],
"page": c.metadata.get("page", 0),
"chunk_index": i,
} for i, c in enumerate(chunks)]
)
At this point, your knowledge base is ready to answer queries.
The user's raw question may need preprocessing before embedding:
Query expansion: Add related terms. "Python error handling" might expand to "Python exception try except error handling."
Query rewriting: An LLM rephrases the query for better retrieval. "What did the CEO say about Q3?" becomes "CEO statement quarterly earnings Q3 revenue."
Intent classification: Determine if retrieval is needed at all. "Hello, how are you?" does not need a knowledge base search.
pythonrunnable cell
1
2
3
4
5
6
def process_query(raw_query):
# Optional: use an LLM to rewrite for better retrieval
rewritten = llm.rewrite(
f"Rewrite for document retrieval: {raw_query}"
)
return rewritten or raw_query
Convert the processed query to a vector using the same embedding model used during ingestion. Using a different model is a common bug -- the vectors live in different spaces and similarity scores become meaningless.
Build the final prompt that combines the user's question with retrieved context. This is where prompt engineering meets retrieval.
pythonrunnable cell
1
2
3
4
5
6
7
8
9
10
prompt = f"""Answer the user's question based ONLY on the
following context. If the context doesn't contain the answer,
say "I don't have enough information to answer that."
Context:
{chr(10).join(top_docs)}
Question: {user_query}
Answer:"""
The system prompt must:
Instruct the model to use only the provided context
Tell the model what to do when context is insufficient
Low temperature (0.0-0.3) is standard for RAG because you want the model to faithfully reproduce information from the context, not be creative. Higher temperatures lead to more paraphrasing and potential deviation from the retrieved facts.
You are a helpful assistant. Answer the user's question using
ONLY the provided context. If the context does not contain
enough information, say "I don't have enough information."
Context:
{retrieved_chunks}
Question: {user_question}
Answer the question using the provided sources. Cite your
sources using [Source N] notation.
Sources:
[Source 1] {chunk_1}
[Source 2] {chunk_2}
[Source 3] {chunk_3}
Question: {user_question}
Provide your answer with citations:
Based on the following context, answer the question.
Respond in JSON format with:
- "answer": your answer
- "confidence": "high", "medium", or "low"
- "sources": list of source document names used
- "reasoning": brief explanation of how you derived the answer
Context: {retrieved_chunks}
Question: {user_question}
Research has shown that LLMs pay more attention to information at the beginning and end of their context, while information in the middle gets less attention. This has practical implications for how you order retrieved chunks:
Place the most relevant chunk first
Place the second most relevant chunk last
Place less relevant chunks in the middle
Alternatively, keep the number of retrieved chunks small (3-5) to avoid the problem entirely.
Tests · Verify retrieval returns the refund and enterprise documents for the example query. Verify the prompt includes both retrieved chunks with source citations.
RAG is a multi-stage pipeline where failures cascade. Poor chunking leads to bad embeddings, bad embeddings lead to irrelevant retrieval, and irrelevant retrieval leads to hallucinated answers; quality at each stage matters
Document ingestion is the most underestimated stage. Parsing PDFs, cleaning HTML, handling tables, and extracting metadata correctly determines the ceiling of your entire RAG system's quality
Prompt templates must explicitly ground the LLM in retrieved context. Instruct the model to only use provided context, cite sources, and admit when the context does not contain the answer
The retrieval-generation interface is where most RAG systems fail. Even with perfect retrieval, poorly formatted context injection or LLMs that ignore context can produce wrong answers
In a RAG pipeline, what is the most common root cause of poor answer quality?
You now understand the complete RAG pipeline from end to end. But the basic pipeline has limitations -- what about queries that need multiple retrieval steps, or hybrid keyword and semantic search? Next up: Advanced RAG Patterns -- the techniques that take RAG from good to great.