RAG: Chat with your PDF
Everything from Labs 06–10 comes together here. RAG (Retrieval-Augmented Generation) means: at question time, find the right pages of your document and give them to the LLM with the question. The model answers from your data, cites the page, and says “I don’t know” when the answer is not there. No fine-tuning needed, and you can update the PDF any time.
In this session
- The 6 RAG steps: Load → Split → Embed → Store → Retrieve → Generate
- The LCEL RAG chain in 5 lines
- Prompt rule: answer only from context, else “I don’t know”
- Return sources (page numbers)
- Streaming the answer
- Conversational RAG: rewrite follow-up questions
01The big idea: an open-book exam
Think of an open-book exam. A student who memorised everything still forgets details. A student allowed to open the textbook finds the right page and answers exactly, and can say “page 47”. RAG makes the LLM an open-book student: the retriever opens the right pages, the LLM writes the answer from them.
| Why RAG? | Because… |
|---|---|
| LLM does not know your private data | your PDF was never in its training |
| Fewer hallucinations | the answer is grounded in retrieved text |
| No fine-tuning | cheaper and faster to build |
| Easy to update | re-index the new PDF, done |
02The RAG chain, line by line
prompt = ChatPromptTemplate.from_messages([
("system", "You are a helpful assistant. Answer ONLY from the context below. "
"If the answer is not in the context, say: I don't know based on the document.\n\n"
"Context:\n{context}"),
("human", "{question}"),
])
rag_chain = (
{"context": retriever | format_docs, "question": RunnablePassthrough()} # 1 fetch context, keep question
| prompt # 2 fill the template
| llm # 3 answer from context
| StrOutputParser() # 4 plain text
)03Hands-on: run it here
The full pipeline on the NovaBank PDF (3 pages → 9 chunks). The stand-in LLM follows the system rule strictly: it quotes the best line from the context, or says it does not know. Watch the cricket question get refused and the KYC answer come back with its page number.
import warnings; warnings.filterwarnings("ignore")
from standins import KeywordEmbeddings, RuleModel
from langchain_community.document_loaders import PyPDFLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_community.vectorstores import FAISS
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough, RunnableParallel
pages = PyPDFLoader("novabank_handbook.pdf").load() # 1 Load
chunks = RecursiveCharacterTextSplitter(chunk_size=200, chunk_overlap=30).split_documents(pages) # 2 Split
vector_store = FAISS.from_documents(chunks, KeywordEmbeddings()) # 3 Embed + 4 Store
retriever = vector_store.as_retriever(search_kwargs={"k": 2}) # 5 Retrieve
print(f"indexed: {len(pages)} pages -> {len(chunks)} chunks")
class ContextOnly(RuleModel):
"""Stand-in LLM that obeys 'answer only from context': it quotes the best context line or says I don't know."""
def _pick(self, messages):
import re
sys_txt, q = messages[0].content, messages[-1].content.lower()
words = {w for w in re.findall(r"[a-z0-9]+", q) if len(w) > 2} - {"what", "does", "when", "who", "the", "for", "how", "mean", "available"}
best = max((l for l in sys_txt.splitlines() if l and not l.startswith("You")), key=lambda l: len(words & set(re.findall(r"[a-z0-9]+", l.lower()))), default="")
hits = len(words & set(re.findall(r"[a-z0-9]+", best.lower())))
return best.split("] ", 1)[-1] if hits >= 1 else "I don't know based on the document."
llm = ContextOnly()
prompt = ChatPromptTemplate.from_messages([
("system", "You are a helpful assistant. Answer ONLY from the context below. "
"If the answer is not in the context, say: I don't know based on the document.\n\nContext:\n{context}"),
("human", "{question}")])
def format_docs(docs): return "\n".join(f"[page {d.metadata['page']}] {l}" for d in docs for l in d.page_content.splitlines()[1:] or [d.page_content])
rag_chain = {"context": retriever | format_docs, "question": RunnablePassthrough()} | prompt | llm | StrOutputParser() # 6 Generate
for q in ["What is the UPI limit for a new payee?", "What does error U30 mean?", "Who won the cricket world cup 2011?"]:
print("\nQ:", q, "\nA:", rag_chain.invoke(q))
rag_with_sources = RunnableParallel({"docs": retriever, "question": RunnablePassthrough()}).assign(
answer=(lambda x: {"context": format_docs(x["docs"]), "question": x["question"]}) | prompt | llm | StrOutputParser())
r = rag_with_sources.invoke("When is video KYC available?")
print("\nA:", r["answer"], "| sources: pages", sorted({d.metadata["page"] + 1 for d in r["docs"]}))KeywordEmbeddings in standins.py) stands in for MiniLM, so every output below is real, but the scores are smaller than MiniLM’s. In Colab use HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2").04Hands-on: Ajeetkumar’s Colab notebook
groq_key with your free key from console.groq.com, switch on Notebook access, then run the cells from the top.The notebook uses the “Attention Is All You Need” PDF from arXiv, so it needs Colab. To use the NovaBank PDF instead, upload novabank_handbook.pdf to Colab and change the file name in Step 1.
05RAG checklist
| ✅ | Why |
|---|---|
| Chunk size 500–1000 with overlap | complete, focused chunks |
| Same embedding model for docs and questions | same vector space |
| “Answer only from context” + “I don’t know” rule | fewer hallucinations |
| Return sources (page numbers) | users can check |
| Hybrid search / re-ranking (Lab 10) | better retrieval |
| Evaluate faithfulness and context relevance | know when changes help (GenAI Course Session 36) |
06Practice tasks
- Replace the PDF with your company policy or resume and ask 5 questions.
- Use the hybrid retriever (BM25 + FAISS) inside the RAG chain.
- Build a Gradio UI:
gr.ChatInterface(lambda msg, hist: ask(msg)).launch().
Try these in the same Colab notebook after you finish the lab. Change one thing at a time and look at the output again.
07Summary
- RAG = retrieve the right chunks, then generate from them.
- Indexing once: load, split, embed, store. Every question: retrieve, generate.
- The LCEL chain:
{"context": retriever | format_docs, "question": RunnablePassthrough()} | prompt | llm | parser. - Tell the model to answer only from context and to say “I don’t know”.
- Return sources; rewrite follow-up questions with history.
08Quick quiz
1. Which RAG steps run for every question?
2. Why "Answer ONLY from the context"?
3. What does RunnablePassthrough do in the RAG chain?
4. Follow-up "How long did it take to train it?" retrieves badly. Fix?
5. How do you show page numbers?
09References
- LangChain: Build a RAG app
- Attention Is All You Need (arXiv)
- Sample PDF: NovaBank handbook (fictional) (download)
- Ajeetkumar’s notebook: LC_11_RAG_Chat_with_PDF.ipynb (download)