DS DevShelfHub Projects · AI tools
Tutorials / LangChain / Build a RAG Chatbot
LangChain Hands-on · 20 min read Page 8 of 20

How to Build a RAG Chatbot in LangChain (Python)

By DevShelfHub

Hands-on: build a complete document Q&A chatbot end-to-end — load, chunk, embed, store, retrieve, answer, and add conversation memory.

Series progress8 / 20
How to build a RAG chatbot in LangChain — index a PDF into FAISS, answer with a Groq LLM, and add conversation memory

What we're building

A command-line chatbot that answers questions about a PDF you provide. It remembers conversation history — so you can ask follow-up questions — and only answers from the document content.

1 Load a PDF with PyPDFLoader
2 Split it into chunks with RecursiveCharacterTextSplitter
3 Embed locally with HuggingFaceEmbeddings and index with FAISS
4 Build a conversational RAG chain with RunnableWithMessageHistory
5 Run an interactive chat loop powered by ChatGroq
Stack choice: We use Groq (free, fast LLM API) instead of OpenAI, and HuggingFace local embeddings instead of OpenAI Embeddings — so you can run this end-to-end with just a free Groq API key.

Setup

Create a virtual environment first to keep dependencies isolated:

bash
python3 -m venv .venv
source .venv/bin/activate   # Windows: .venv\Scripts\activate
bash
pip3 install langchain langchain-groq langchain-huggingface \
    langchain-community langchain-text-splitters \
    faiss-cpu pypdf sentence-transformers truststore

Get a free API key at console.groq.com, then set it:

bash
export GROQ_API_KEY="gsk_..."
macOS SSL note: The truststore package fixes a TLS certificate error that occurs with Python 3.12+ on macOS. It's imported at the top of chatbot.py and requires no configuration.

ingest.py — build the index

Run this once to load your PDF, chunk it, embed with a local model, and save the FAISS index to disk. Re-run only when the document changes.

python
import os
import sys
from langchain_community.document_loaders import PyPDFLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_community.vectorstores import FAISS

INDEX_DIR = "faiss_index"
EMBED_MODEL = "all-MiniLM-L6-v2"


def ingest(pdf_path: str, index_dir: str = INDEX_DIR):
    if not os.path.exists(pdf_path):
        print(f"Error: file not found — {pdf_path}")
        sys.exit(1)

    print(f"Loading {pdf_path}...")
    loader = PyPDFLoader(pdf_path)
    docs = loader.load()
    print(f"  Loaded {len(docs)} pages")

    splitter = RecursiveCharacterTextSplitter(
        chunk_size=500,
        chunk_overlap=60,
    )
    chunks = splitter.split_documents(docs)
    print(f"  Split into {len(chunks)} chunks")

    print(f"  Embedding with {EMBED_MODEL} (local, no API key)...")
    embeddings = HuggingFaceEmbeddings(model_name=EMBED_MODEL)
    vectorstore = FAISS.from_documents(chunks, embeddings)
    vectorstore.save_local(index_dir)
    print(f"  Index saved to ./{index_dir}/")
    print("Done. Run chatbot.py to start chatting.")


if __name__ == "__main__":
    pdf = sys.argv[1] if len(sys.argv) > 1 else "document.pdf"
    ingest(pdf)
Why HuggingFaceEmbeddings? all-MiniLM-L6-v2 runs entirely on your machine (~90 MB, downloaded once). No API key, no cost per embedding call — and quality is solid for most RAG use cases.
bash
python3 ingest.py report.pdf

chatbot.py — the conversational chain

This is the main file. It loads the saved index, builds a conversational RAG chain with Groq, and runs the chat loop.

python
import os
import truststore
truststore.inject_into_ssl()  # fixes macOS TLS cert error

from langchain_groq import ChatGroq
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_community.vectorstores import FAISS
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_community.chat_message_histories import ChatMessageHistory
from operator import itemgetter

INDEX_DIR = "faiss_index"
EMBED_MODEL = "all-MiniLM-L6-v2"
GROQ_MODEL  = "llama-3.3-70b-versatile"

if not os.path.exists(INDEX_DIR):
    print("Index not found. Run 'python3 ingest.py <your_document.pdf>' first.")
    raise SystemExit(1)

# ── Load vector store ──────────────────────────────────────
embeddings = HuggingFaceEmbeddings(model_name=EMBED_MODEL)
vectorstore = FAISS.load_local(
    INDEX_DIR, embeddings, allow_dangerous_deserialization=True
)
retriever = vectorstore.as_retriever(search_kwargs={"k": 4})

# ── LLM ───────────────────────────────────────────────────
llm = ChatGroq(model=GROQ_MODEL, temperature=0)

# ── Prompt ────────────────────────────────────────────────
system_prompt = """You are a helpful assistant that answers questions \
about a document. Use only the context provided below to answer. \
If the answer is not in the context, say "I don't know based on the document."

Context:
{context}"""

prompt = ChatPromptTemplate.from_messages([
    ("system", system_prompt),
    MessagesPlaceholder(variable_name="history"),
    ("human", "{question}"),
])

# ── Chain ─────────────────────────────────────────────────
def format_docs(docs):
    return "\n\n".join(doc.page_content for doc in docs)

chain = (
    {
        "context":  itemgetter("question") | retriever | format_docs,
        "question": itemgetter("question"),
        "history":  itemgetter("history"),
    }
    | prompt
    | llm
    | StrOutputParser()
)

# ── Memory ────────────────────────────────────────────────
store = {}

def get_history(session_id: str):
    if session_id not in store:
        store[session_id] = ChatMessageHistory()
    return store[session_id]

chat = RunnableWithMessageHistory(
    chain,
    get_history,
    input_messages_key="question",
    history_messages_key="history",
)

config = {"configurable": {"session_id": "default"}}

# ── Chat loop ─────────────────────────────────────────────
print(f"RAG Document Chatbot (Groq / {GROQ_MODEL}) — type 'quit' to exit\n")
while True:
    try:
        question = input("You: ").strip()
    except (EOFError, KeyboardInterrupt):
        print("\nBye!")
        break
    if question.lower() in ("quit", "exit", "q"):
        break
    if not question:
        continue
    answer = chat.invoke({"question": question}, config=config)
    print(f"Bot: {answer}\n")

Running it

1. Build the index (once per document)

bash
python3 ingest.py your_document.pdf

2. Start the chatbot

bash
python3 chatbot.py

Example session

Text
RAG Document Chatbot (Groq / llama-3.3-70b-versatile) — type 'quit' to exit

You: What is the main topic of this document?
Bot: The document is about ...

You: Can you give me more detail on that?
Bot: Sure, to elaborate on what I mentioned ...

You: Who is the president of France?
Bot: I don't know based on the document.

You: quit

Follow-up questions work because the full conversation history is passed to the model on every turn via RunnableWithMessageHistory. The out-of-context question correctly returns "I don't know" — the grounding instruction is working.

Common issues

SSL: CERTIFICATE_VERIFY_FAILED

This appears on macOS with Python 3.12+. The fix is already in chatbot.py — the truststore.inject_into_ssl() call at the top patches the SSL context to use your system's trusted certificates.

Model decommissioned error

Groq retires older models. If you get a model_decommissioned error, change GROQ_MODEL in chatbot.py to a current model from console.groq.com/docs/models.

RunnablePassthrough / missing history variable

In newer LangChain versions, RunnableWithMessageHistory requires a dict input and does not automatically forward the history key through chain transformations. The fix: use itemgetter("question") and itemgetter("history") in the chain, and pass {"question": question} to .invoke().

Index not found on startup

You must run python3 ingest.py your_doc.pdf before chatbot.py. The faiss_index/ directory must exist in the same folder.

Demo

Where to go next

Add a web UI

Wrap the chain in a FastAPI endpoint and call it from a simple frontend. Use .astream() with Server-Sent Events for real-time token streaming.

Persist history across restarts

Swap ChatMessageHistory for RedisChatMessageHistory or SQLChatMessageHistory so conversations survive server restarts.

Support multiple documents

Store source metadata on each chunk and filter by it at retrieval time. Use SelfQueryRetriever to let the model extract which document to search from the user's question.

Move to LangGraph for production

LangGraph gives you explicit state management, conditional branching (e.g. "should I retrieve or answer from memory?"), and built-in human-in-the-loop checkpoints — the right foundation for a production agent.

LangChain RAG Chatbot FAQ

Do I need an OpenAI API key to build a RAG chatbot in LangChain?

No. This build uses local HuggingFace embeddings (all-MiniLM-L6-v2) that run on your machine with no API key, and a free Groq LLM for answering. You only need a free GROQ_API_KEY from console.groq.com, so you can run the entire chatbot end-to-end without an OpenAI account or any paid service.

Why use Groq instead of OpenAI for the chatbot LLM?

Groq offers a free, very fast inference API for open models like llama-3.3-70b-versatile, which lets you build and run the chatbot at no cost. It exposes a standard LangChain ChatGroq interface, so the rest of the chain stays identical and you can swap in OpenAI or another provider later by changing one line.

How does conversation memory work in this RAG chatbot?

The chain is wrapped in RunnableWithMessageHistory, which stores each session's messages in a ChatMessageHistory keyed by session_id. On every turn the full prior conversation is injected into the prompt through a MessagesPlaceholder, so the model can resolve follow-up questions that refer back to earlier answers.

How do I stop the RAG chatbot from hallucinating answers?

Ground the model in retrieved context and tell it explicitly to answer only from that context, saying 'I don't know based on the document' when the answer is not present. Retrieve enough chunks (k=4 is a good start) so the answer is actually in context, and keep chunks focused so the relevant facts are not diluted.

Can the chatbot handle multiple PDFs or documents?

Yes. Load each PDF in ingest.py and add all the resulting chunks to the same FAISS index, storing a source value in each chunk's metadata. At query time you can filter by that metadata, or use a SelfQueryRetriever so the model decides which document to search based on the user's question.

Do I have to re-index the PDF every time I start the chatbot?

No. ingest.py saves the FAISS index to disk with save_local, and chatbot.py loads it with load_local on startup. You only re-run ingest.py when the source document changes, which avoids re-embedding the same PDF on every launch and keeps startup fast.

Series complete — what you built

  • A full RAG pipeline: load → chunk → embed → store → retrieve → answer
  • Local embeddings with HuggingFaceEmbeddings — no OpenAI key needed
  • Free LLM via ChatGroq with llama-3.3-70b-versatile
  • Conversational memory using RunnableWithMessageHistory
  • A clean LCEL chain that composes retrieval, history, and the LLM in one pipe

You now have a solid foundation in LangChain — prompts, chains, memory, tools, agents, and RAG. The next frontier is LangGraph for stateful, production-grade agent workflows.

Quick jump: API Reference