LangChain Hands-on · Module 2 · Lab 11

RAG: Chat with your PDF

⏱ 20 min + hands-on🎯 Beginner to intermediate🖼 1 diagram🧪 3 live demos🐍 Python code

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.

LoadPyPDFLoaderSplitchunks + overlapEmbedMiniLMStoreFAISSRetrievetop-k chunksGenerateLLM + context
Steps 1–4 run once (indexing). Steps 5–6 run for every question.
Why RAG?Because…
LLM does not know your private datayour PDF was never in its training
Fewer hallucinationsthe answer is grounded in retrieved text
No fine-tuningcheaper and faster to build
Easy to updatere-index the new PDF, done

02The RAG chain, line by line

rag_chain.py
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
)
Demo · What happens when you call rag_chain.invoke("What is the UPI limit for a new payee?")
Demo · Conversational RAG: why follow-ups need a rewrite

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.

lab11_rag.py
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"]}))
indexed: 3 pages -> 9 chunks Q: What is the UPI limit for a new payee? A: The UPI limit for a new payee is Rs 25,000 in the first 24 hours. Q: What does error U30 mean? A: Error code U30 means the debit failed at the bank. The money is not deducted. Q: Who won the cricket world cup 2011? A: I don't know based on the document. A: Video KYC is available from 9 AM to 9 PM on all days. | sources: pages [3]
Runs here without downloads. A tiny keyword embedding (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

Colab setup (same in every notebook): click 🔑 Secrets in the Colab sidebar, add groq_key with your free key from console.groq.com, switch on Notebook access, then run the cells from the top.
Demo · Notebook player · Lab 11

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 overlapcomplete, focused chunks
Same embedding model for docs and questionssame vector space
“Answer only from context” + “I don’t know” rulefewer hallucinations
Return sources (page numbers)users can check
Hybrid search / re-ranking (Lab 10)better retrieval
Evaluate faithfulness and context relevanceknow when changes help (GenAI Course Session 36)

06Practice tasks

  1. Replace the PDF with your company policy or resume and ask 5 questions.
  2. Use the hybrid retriever (BM25 + FAISS) inside the RAG chain.
  3. 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

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

Created by Ajeetkumar