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.
PyPDFLoader
RecursiveCharacterTextSplitter
HuggingFaceEmbeddings and index with FAISS
RunnableWithMessageHistory
ChatGroq
Setup
Create a virtual environment first to keep dependencies isolated:
python3 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
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:
export GROQ_API_KEY="gsk_..."
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.
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)
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.
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.
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)
python3 ingest.py your_document.pdf
2. Start the chatbot
python3 chatbot.py
Example session
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
ChatGroqwithllama-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.