codewithgowtham
Let's talk

codewithgowtham.com

© 2026 Gowtham. All rights reserved.

All posts
LangChainRAGPythonAIFAISS

Building a PDF Q&A Agent with LangChain and FAISS

A step-by-step guide to building a local RAG pipeline that lets you chat with any PDF document using LangChain, FAISS, and Groq's Llama 3.1 — no cloud storage required.

June 15, 2026·8 min read
Share:
LinkedIn
X (Twitter)
WhatsApp

Was this helpful?

Comments (0)

Leave a comment

Retrieval-Augmented Generation (RAG) is one of the most practical patterns in applied AI. Instead of fine-tuning a model on your data, you give the model relevant context at query time — so it can answer questions grounded in your actual documents.

In this post, I'll walk through the exact architecture I used to build my PDF Q&A Agent: a fully local RAG pipeline that lets you chat with any PDF, with cited source pages.

The Architecture

The pipeline has three stages:

  1. Ingestion — Load the PDF, split it into overlapping chunks
  2. Indexing — Embed the chunks and store in FAISS
  3. Retrieval + Generation — At query time, retrieve the top-k chunks and pass them to the LLM
PDF → chunks → embeddings → FAISS index
                                ↓
query → embedding → top-3 chunks → Groq Llama 3.1 → answer

Setting Up the Environment

I'm using uv for fast dependency management:

uv init pdf-qa-agent
cd pdf-qa-agent
uv add langchain langchain-community faiss-cpu sentence-transformers pypdf langchain-groq

Create a .env file:

GROQ_API_KEY=your_groq_api_key_here

Stage 1: Load and Chunk the PDF

from langchain_community.document_loaders import PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
 
def load_and_chunk(pdf_path: str):
    loader = PyPDFLoader(pdf_path)
    pages = loader.load()
 
    splitter = RecursiveCharacterTextSplitter(
        chunk_size=800,
        chunk_overlap=120,  # overlap preserves context across chunks
        separators=["\n\n", 

The chunk_overlap=120 is important — without it, a sentence split across a chunk boundary loses context on both sides.

Stage 2: Build the FAISS Index

I'm using all-MiniLM-L6-v2 from HuggingFace — it's fast, lightweight, and runs entirely local.

from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain_community.vectorstores import FAISS
import os
 
CACHE_DIR = ".faiss_cache"
 
def get_vector_store(chunks, pdf_name: str):
    cache_path = os.path.join(CACHE_DIR, pdf_name)
 
    embeddings = HuggingFaceEmbeddings(
        model_name="sentence-transformers/all-MiniLM-L6-v2"
    )
 
    if os.path.exists(cache_path):
        print("Loading cached index..."








Caching the index to disk means subsequent runs are instant — no re-embedding needed.

Stage 3: Retrieval + Generation

from langchain_groq import ChatGroq
from langchain.prompts import ChatPromptTemplate
from langchain.schema.runnable import RunnablePassthrough
from langchain.schema.output_parser import StrOutputParser
 
def build_qa_chain(vector_store):
    retriever = vector_store.as_retriever(search_kwargs={"k": 3})
 
    llm = ChatGroq(
        model_name="llama-3.1-8b-instant",
        temperature=




















Putting It All Together

import sys
 
def main():
    if len(sys.argv) < 2:
        print("Usage: python main.py path/to/document.pdf")
        sys.exit(1)
 
    pdf_path = sys.argv[1]
    pdf_name = os.path.basename(pdf_path).replace(".pdf", "")
 
    print(f"Loading: {pdf_path}")
    chunks 














Running It

uv run python main.py research-paper.pdf
 
# Output:
# Loading: research-paper.pdf
# Created 47 chunks
# Building index...
# Ready. Type your question (Ctrl+C to exit)
 
# Q: What is the main contribution of this paper?
# A: The paper proposes a novel attention mechanism...

What I'd Add Next

  • Source citation: LangChain's RetrievalQAWithSourcesChain returns which page each answer came from
  • Multi-PDF support: Merge multiple FAISS indices or use a single index with metadata filters
  • Streaming: Groq supports streaming via stream=True — important for long answers
  • Web UI: Wrap it in a FastAPI + Next.js interface for a proper app

The full source is on GitHub. If you have questions or want to extend this, drop a comment below.

"
\n
"
,
"."
,
" "
],
)
return splitter.split_documents(pages)
)
return FAISS.load_local(
cache_path, embeddings, allow_dangerous_deserialization=True
)
print("Building index...")
store = FAISS.from_documents(chunks, embeddings)
os.makedirs(cache_path, exist_ok=True)
store.save_local(cache_path)
return store
0
,
)
prompt = ChatPromptTemplate.from_template("""
You are a helpful assistant. Answer the question using ONLY the context below.
If the answer isn't in the context, say "I don't have enough information."
Context:
{context}
Question: {question}
Answer:
""")
chain = (
{"context": retriever, "question": RunnablePassthrough()}
| prompt
| llm
| StrOutputParser()
)
return chain
=
load_and_chunk(pdf_path)
print(f"Created {len(chunks)} chunks")
store = get_vector_store(chunks, pdf_name)
chain = build_qa_chain(store)
print("\nReady. Type your question (Ctrl+C to exit)\n")
while True:
question = input("Q: ").strip()
if not question:
continue
answer = chain.invoke(question)
print(f"A: {answer}\n")
if __name__ == "__main__":
main()