A production-ready, research-level Adaptive Multi-Stage Retrieval-Augmented Generation (RAG) pipeline for long-document question answering. Supports PDF, DOCX, and TXT documents with multi-stage retrieval, cross-encoder re-ranking, adaptive feedback loops, and a full evaluation suite.
Most open-source RAG systems stop at vector search. This pipeline goes further:
- 3-stage retrieval — vector search → cross-encoder re-ranking → context compression
- Query-aware routing — retrieval strategy adapts to whether the query is factual, summarization, reasoning, or multi-hop
- Adaptive feedback loop — low-confidence answers automatically trigger expanded retrieval and regeneration
- Full evaluation — retrieval precision, context relevance, answer faithfulness, and answer relevance out of the box
Document ──► Ingestion ──► Chunking ──► Embeddings ──► FAISS Index
│
Query ──► Query Router ──► Stage 3.1 Vector Search │◄──────┐
──► Stage 3.2 Cross-Encoder Re-Rank │
──► Stage 3.3 Context Compression │
│ │
──► LLM Generation ──► Answer │
│ │
──► Confidence Check ──► Feedback Loop ───────┘
│
──► Evaluation Metrics
adaptive-multistage-rag-system/
├── ingestion/
│ └── document_loader.py # PDF / DOCX / TXT loading + cleaning
├── chunking/
│ └── semantic_chunker.py # Adaptive semantic chunking
├── embeddings/
│ └── embedder.py # Sentence-Transformers batched encoding
├── vector_store/
│ └── faiss_store.py # FAISS IndexFlatIP + metadata store
├── retrieval/
│ ├── retriever.py # Stage 3.1 – vector retrieval
│ ├── reranker.py # Stage 3.2 – cross-encoder re-ranking
│ └── adaptive_retrieval.py # Stage 3.3 – dedup + merge + token budget
├── query_router/
│ └── classifier.py # Query type classification
├── generation/
│ ├── generator.py # LLM generation (OpenAI / HuggingFace / stub)
│ └── feedback_loop.py # Adaptive feedback loop
├── evaluation/
│ └── metrics.py # Evaluation metrics + optional RAGAS
├── scripts/
│ ├── create_notebook.py
│ ├── sample_generator.py
│ └── run_eval.py
├── .github/workflows/
│ └── ci.yml # GitHub Actions CI
├── main.py # CLI entry point
├── pyproject.toml
├── requirements.txt
├── research_notebook.ipynb
├── adaptive_rag_evaluation.ipynb
└── eval_results.json
# 1. Clone the repo
git clone https://github.com/Geeta3521/adaptive-multistage-rag-system.git
cd adaptive-multistage-rag-system
# 2. Create and activate a virtual environment
python -m venv .venv
source .venv/bin/activate # Linux / macOS
# .venv\Scripts\activate # Windows
# 3. Install the package
pip install -e .
# 4. Download NLTK sentence tokeniser
python -c "import nltk; nltk.download('punkt'); nltk.download('punkt_tab')"export OPENAI_API_KEY="sk-..."pip install transformers accelerate
export HF_MODEL="mistralai/Mistral-7B-Instruct-v0.2"# Run with the built-in demo (no API key needed)
python main.py --demo
# Answer a question using the stub backend (no API key needed)
python main.py \
--document data/sample.txt \
--question "What is the main contribution of the Transformer?" \
--backend stub \
--evaluate
# Use OpenAI
python main.py \
--document my_paper.pdf \
--question "Summarise the methodology" \
--backend openai \
--model gpt-4o \
--evaluate
# Save output as JSON
python main.py \
--document report.pdf \
--question "What are the key findings?" \
--output-json result.json| Flag | Default | Description |
|---|---|---|
--document |
(required) | Path to PDF / TXT / DOCX |
--question |
(required) | Query to answer |
--backend |
auto |
auto / openai / huggingface / ollama / stub |
--model |
gpt-3.5-turbo |
LLM model name |
--embed-model |
all-MiniLM-L6-v2 |
Sentence-Transformers model |
--store-dir |
./index_cache |
FAISS index cache directory |
--rebuild |
false |
Force re-index even if cache exists |
--evaluate |
false |
Compute evaluation metrics |
--output-json |
"" |
Write result dict to a JSON file |
--demo |
false |
Run the built-in demo (no document needed) |
ingestion/document_loader.py
Supports PDF (PyMuPDF with pdfplumber fallback), DOCX, and TXT/Markdown. Extracts per-page text, cleans Unicode, and infers section titles.
chunking/semantic_chunker.py
Splits on paragraph boundaries, merges blocks below min_tokens, and splits blocks above max_tokens at sentence boundaries with configurable overlap. Produces clean, semantically coherent chunks rather than fixed-size windows.
embeddings/embedder.py · vector_store/faiss_store.py
Batched encoding via sentence-transformers (default all-MiniLM-L6-v2). L2-normalised vectors stored in FAISS IndexFlatIP for exact cosine search. The index is serialised to disk so subsequent runs load from cache instantly.
| Sub-stage | File | What it does |
|---|---|---|
| 3.1 | retrieval/retriever.py |
Top-k cosine similarity search |
| 3.2 | retrieval/reranker.py |
Cross-encoder re-ranking (ms-marco-MiniLM) |
| 3.3 | retrieval/adaptive_retrieval.py |
Dedup · merge · token-budget enforcement |
query_router/classifier.py
Classifies each query as factual, summarization, reasoning, or multi_hop. Each type maps to a different RetrievalConfig controlling top-k, token budget, re-ranking, and whether iterative retrieval is used.
Handled by adaptive chunk sizes (Stage 1b) and the token-budget compression step (Stage 3.3). No separate truncation step — context fits within the LLM window by construction.
generation/generator.py
Builds a structured prompt [context + question + instructions] and calls:
- OpenAI — any chat-completions compatible endpoint
- HuggingFace — local model via
transformers.pipeline - Ollama — local models via the Ollama API
- Stub — returns the prompt directly, no model required (great for testing)
generation/feedback_loop.py · main.py
If the generated answer has low confidence (measured by cosine similarity between the answer embedding and the context), the pipeline automatically doubles the retrieval budget and regenerates — up to max_feedback_loops times.
evaluation/metrics.py
| Metric | Method |
|---|---|
retrieval_precision |
Token-Jaccard overlap between query and chunks |
context_relevance |
Mean cosine similarity of chunk embeddings to query |
answer_faithfulness |
Fraction of answer sentences supported by context |
answer_relevance |
Cosine similarity of answer embedding to query |
token_usage |
Approximate total tokens consumed |
latency_seconds |
Wall-clock time for the full pipeline |
| RAGAS (optional) | faithfulness + answer_relevancy via the RAGAS library |
──────────────────────────────────────────────────────────────────────
Question : What is the main contribution of the Transformer?
Query type : factual
Backend : openai
Chunks used: 3
Latency : 1.842 s
──────────────────────────────────────────────────────────────────────
ANSWER
──────────────────────────────────────
The Transformer's main contribution is demonstrating that attention
alone is sufficient for high-quality sequence modelling, removing
the need for recurrence. This enables parallelism and scaling to
billions of parameters.
EVALUATION SCORES
──────────────────────────────────────
retrieval_precision 0.8333
context_relevance 0.7241
answer_faithfulness 0.8750
answer_relevance 0.8102
token_usage 487
latency_seconds 1.842
──────────────────────────────────────────────────────────────────────
pip install -e ".[dev]"
pytest -v| Concern | Recommendation |
|---|---|
| Index speed | Switch to IndexIVFFlat with nprobe tuning for > 100k chunks |
| Embedding quality | Use all-mpnet-base-v2 for higher accuracy |
| Embedding speed | Use all-MiniLM-L12-v2 for faster encoding |
| Re-ranking speed | Use cross-encoder/ms-marco-TinyBERT-L-2 for 4× faster re-ranking |
| Memory | Use IndexFlatIP + memmap for corpora that don't fit in RAM |
| Latency | Cache query embeddings; pre-compute chunk embeddings once |
| Long docs | Enable hierarchical retrieval — embed section summaries separately |
| Accuracy | Fine-tune the cross-encoder on domain-specific relevance labels |
- Streaming generation support
- REST API wrapper (FastAPI)
- Streamlit / Gradio demo UI
- Support for web URLs as document sources
- Hierarchical retrieval for very large document sets
- Docker container for one-command deployment
Contributions are welcome! Please open an issue first to discuss what you'd like to change. Make sure all tests pass before submitting a pull request.
pip install -e ".[dev]"
pytest -vMIT — free for academic and commercial use. See LICENSE for details.
Geetha · GitHub
If you find this project useful, please consider giving it a ⭐ on GitHub!