Building a Document Q&A Chatbot with RAG

Answering one question about a document is the easy part. A chatbot also gets follow-ups such as which one, and why. This guide builds a document Q&A chatbot over a 48-page PDF with page citations, runs one conversation two ways, and shows how rewriting follow-ups turned a wrong answer into a right one.

GloryTecks Team

Training & Placement Faculty

28 March 2026 10 min read

Last updated: 3 October 2026

Share:

"Chat with your documents" is the most requested generative AI feature in companies, and the first version is easy: split a PDF into pieces, find the pieces closest to the question, and ask a model to answer from them. It works well for the first question.

Then a user asks a follow-up. "Which one is the base for the others?" makes sense to a person who remembers the conversation. To a search index, "which one" means nothing. This guide builds a small document chatbot, runs the same conversation two ways, and shows where the naive version fails.

The document

The example uses the NIST AI Risk Management Framework, a 48-page PDF published by the US National Institute of Standards and Technology (NIST: AI RMF 1.0). NIST released it on 26 January 2023 for voluntary use and is revising it (NIST: AI Risk Management Framework). It suits the test: real, dense prose with figures, lists and cross-references.

The chatbot

Four steps:

  1. Ingest. Extract text page by page with pypdf and split each page into overlapping chunks that remember their page number. Extraction works only for PDFs that contain text; scanned pages may yield nothing without OCR (pypdf: extract text).
  2. Retrieve the three chunks closest in meaning to the search query.
  3. Answer from those chunks only, citing pages, or say the document does not cover it.
  4. Remember the conversation: the model sees the earlier turns.

The script runs the same five-turn conversation twice. The first run searches with the user's words as typed. The second asks the model to rewrite each follow-up as a standalone question before searching.

python
import json
import re
import urllib.request

from pypdf import PdfReader
from sentence_transformers import SentenceTransformer

URL = "http://localhost:11434/api/chat"
OPTIONS = {"temperature": 0, "seed": 42, "num_ctx": 4096, "num_predict": 150}

# 1. Ingest: split each PDF page into ~120-word chunks that remember their page number
pages = [" ".join((p.extract_text() or "").split()) for p in PdfReader("nist-ai-rmf.pdf").pages]
chunks = []
for number, text in enumerate(pages, 1):
    words = text.split()
    for start in range(0, len(words), 100):                      # 20 words of overlap
        chunks.append({"page": number, "text": " ".join(words[start:start + 120])})
embedder = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2")
vectors = embedder.encode([c["text"] for c in chunks], normalize_embeddings=True)
print(f"{len(pages)} pages -> {len(chunks)} chunks")


def retrieve(query, k=3):
    scores = vectors @ embedder.encode([query], normalize_embeddings=True)[0]
    return [chunks[i] for i in scores.argsort()[::-1][:k]]


def chat(messages):
    body = {"model": "llama3.2", "messages": messages, "stream": False, "options": OPTIONS}
    req = urllib.request.Request(URL, json.dumps(body).encode(), {"Content-Type": "application/json"})
    with urllib.request.urlopen(req, timeout=900) as r:
        return json.load(r)["message"]["content"].strip()


def standalone(question, history):
    # 2. Rewrite a follow-up into a question that makes sense on its own
    if not history:
        return question
    recent = "\n".join(f"{role}: {text}" for role, text in history[-4:])
    return chat([{"role": "user", "content":
                  f"Conversation so far:\n{recent}\n\nRewrite this follow-up as one standalone question. "
                  f"Reply with the question only.\nFollow-up: {question}"}])


def answer(question, found, history):
    # the model sees the conversation in both runs; only the search query differs
    context = "\n\n".join(f"[page {c['page']}] {c['text']}" for c in found)
    past = [{"role": role, "content": text} for role, text in history]
    return chat([{"role": "system", "content":
                  "Answer from the excerpts only, in at most two sentences, and cite pages like (page 5). "
                  "If the excerpts do not contain the answer, reply: Not found in the document."},
                 *past,
                 {"role": "user", "content": f"Excerpts:\n{context}\n\nQuestion: {question}"}])


# 3. One conversation, run twice: retrieving with the raw question, and with the rewritten one
TURNS = [("What are the four functions of the AI RMF Core?", ["govern", "map", "measure", "manage"]),
         ("Which of them is described as cross-cutting?", ["govern"]),
         ("What characteristics of trustworthy AI does the framework list?", ["valid", "reliable", "fair"]),
         ("Which one is shown as the base for the others?", ["valid"]),
         ("What fines does the EU AI Act set for violations?", ["not found"])]

for mode in ("raw question", "rewritten question"):
    print(f"\n=== retrieval with the {mode}")
    history, right = [], 0
    for question, needed in TURNS:
        query = standalone(question, history) if mode == "rewritten question" else question
        found = retrieve(query)
        reply = answer(question, found, history)
        ok = all(term in reply.lower() for term in needed)
        right += ok
        history += [("user", question), ("assistant", reply)]
        print(f"Q: {question}")
        if query != question:
            print(f"   searched for: {query}")
        print(f"   pages {[c['page'] for c in found]} -> {' '.join(reply.split())[:170]}")
        print(f"   {'OK' if ok else 'MISSED'}")
    print(f"{right} of {len(TURNS)} answered as expected")
text
48 pages -> 186 chunks

=== retrieval with the raw question
Q: What are the four functions of the AI RMF Core?
   pages [25, 25, 4] -> The four functions of the AI RMF Core are GOVERN, MAP, MEASURE, and MANAGE (page 25). Governance is a cross-cutting function that informs and is infused throughout the ot
   OK
Q: Which of them is described as cross-cutting?
   pages [32, 5, 43] -> Governance is described as a cross-cutting function (page 5). It informs and is infused throughout the other three functions, namely GOVERN, MAP, MEASURE, and MANAGE.
   OK
Q: What characteristics of trustworthy AI does the framework list?
   pages [17, 5, 7] -> The framework lists the following characteristics of trustworthy AI systems: 1. Valid and reliable 2. Safe 3. Secure and resilient 4. Accountable and transparent 5. Expla
   OK
Q: Which one is shown as the base for the others?
   pages [32, 43, 26] -> The base for the others is "software or hardware systems" (page 26). It serves as a foundation for the other categories, such as data, and is also subject to similar cont
   MISSED
Q: What fines does the EU AI Act set for violations?
   pages [11, 21, 34] -> Not found in the document.
   OK
4 of 5 answered as expected

=== retrieval with the rewritten question
Q: What are the four functions of the AI RMF Core?
   pages [25, 25, 4] -> The four functions of the AI RMF Core are GOVERN, MAP, MEASURE, and MANAGE (page 25). Governance is a cross-cutting function that informs and is infused throughout the ot
   OK
Q: Which of them is described as cross-cutting?
   searched for: Is governance the cross-cutting function among GOVERN, MAP, MEASURE, and MANAGE?
   pages [5, 26, 41] -> Governance is described as a cross-cutting function (page 5). It informs and is infused throughout the other three functions.
   OK
Q: What characteristics of trustworthy AI does the framework list?
   searched for: What are the characteristics of trustworthy AI listed by the framework, specifically in relation to the four functions of the AI RMF Core (GOVERN, MAP, MEASURE, and MANAGE)?
   pages [7, 25, 17] -> The Framework lists the following characteristics of trustworthy AI systems: 1. Valid and reliable 2. Safe 3. Secure and resilient 4. Accountable and transparent 5. Expla
   OK
Q: Which one is shown as the base for the others?
   searched for: What characteristic of trustworthy AI systems is described as the base or foundation for the others, according to the framework?
   pages [5, 17, 17] -> Valid & Reliable is shown as the base for other trustworthiness characteristics (page 17).
   OK
Q: What fines does the EU AI Act set for violations?
   searched for: What are the specific fines imposed by the EU AI Act for non-compliance with its regulations?
   pages [47, 27, 7] -> Not found in the document.
   OK
5 of 5 answered as expected

What the output shows

  • First questions are easy. Both runs found the four functions, GOVERN, MAP, MEASURE and MANAGE, on page 25.
  • A vague follow-up broke the naive run. "Which one is shown as the base for the others?" retrieved pages 32, 43 and 26, none of which answer it. The model still answered, confidently and with a page citation: "software or hardware systems". It was wrong, and the citation made it look reliable.
  • Rewriting fixed it. The rewritten query, "What characteristic of trustworthy AI systems is described as the base or foundation for the others", retrieved page 17. That page holds the figure caption stating that Valid and Reliable is the base, and the answer was right.
  • Some follow-ups survive anyway. "Which of them is described as cross-cutting?" worked both ways, because "cross-cutting" is a rare, specific word. Follow-ups made of pronouns and comparisons are the ones that fail.
  • Both runs declined the off-topic question about EU AI Act fines: "Not found in the document."
  • Rewrites can overreach. One rewritten question added a detail the user never asked about. It did no harm here, but rewriting is itself a model call that needs testing.

This rewrite step is common enough to be built into frameworks. LlamaIndex's "condense question" chat mode looks at the chat history and rewrites the user's message into a query before searching (LlamaIndex: chat engines).

Design notes

Keep page numbers with every chunk, so answers can cite them and users can check. A citation is only useful if it leads to the right page.

Retrieve with the rewritten question, answer with the conversation. The search needs a self-contained query; the answer benefits from knowing what was said before.

Limit the history you rewrite from. The script uses only the last four messages. Very long conversations should be summarised first, or the rewrite drifts towards older topics.

Log both versions of every question, the user's words and the rewritten query, so that a wrong answer can be traced to retrieval, to rewriting or to the answer itself.

Give the model a way out. A fixed phrase for "not in the document" is easy to detect in code and to show in the interface.

Test conversations, not just questions. Build test cases as sequences with follow-ups, pronouns and changes of topic, because that is where chatbots fail. See retrieval evaluation.

Check citations. The wrong answer above cited a real page that did not support it. Verifying that the cited text contains the claim catches this. See how to reduce LLM hallucinations.

Watch the ingestion. Tables, multi-column layouts and scanned pages need more than plain text extraction. For splitting choices, see chunking strategies for RAG.

Common mistakes

  • Searching with raw follow-up questions.
  • Dropping page numbers during chunking.
  • Trusting citations without checking them.
  • Testing single questions only.
  • Assuming every PDF has extractable text.
  • No answer for "not in the document", so the model fills the gap.

For the general architecture behind this chatbot, see RAG architecture explained.

How do I build a chatbot that answers questions about PDFs?

Extract text page by page, split it into chunks that keep their page numbers, embed them for search, retrieve the closest chunks for each question, and have a model answer only from them with page citations, or say the answer is not in the document.

Why does my RAG chatbot fail on follow-up questions?

Because the search uses the follow-up as typed, and phrases like "which one" or "what about the second" mean nothing without the conversation. Rewrite each follow-up into a standalone question using the chat history before searching.

What is query rewriting or condensing in RAG?

A step in which a model turns the latest user message, plus the conversation so far, into one self-contained search query. LlamaIndex calls its version the condense question chat mode.

How do I make a document chatbot cite its sources?

Store a page or section reference with every chunk, include it in the text given to the model, instruct the model to cite it, and check in code that the cited passage supports the answer.

Building RAG-based Q&A systems and chatbots is part of the Building Gen AI Applications module of the Generative AI course.

Sources

GloryTecks Team

Training & Placement Faculty

The GloryTecks faculty team mentors learners across Data Science, AI and Data Engineering, with a focus on real-time projects and placement readiness in Hyderabad.

Want to master Generative AI?

Join GloryTecks’ expert-led Generative AI training in Hyderabad with real-time projects and 100% placement support.

Explore course