research_assistant / docs /ARCHITECTURE.md
Ahmed167's picture
Upload folder using huggingface_hub
07d1ce5 verified
|
Raw
History Blame Contribute Delete
6.04 kB
# Architecture
## Overview
This project is a Retrieval-Augmented Generation (RAG) system. Given a research question, it (1) retrieves relevant evidence from a corpus of ArXiv abstracts and web search results, (2) reasons over the evidence with an LLM, and (3) returns an answer with inline citations.
The novel parts compared to a tutorial RAG are:
1. **Hybrid retrieval** β€” dense embeddings + BM25 in parallel, merged and reranked.
2. **LangGraph workflow** β€” query rewriting, evidence grading, generation, and a reflection loop, modeled as a state machine.
3. **Pluggable LLM backend** β€” one factory, four providers.
## Component map
| Layer | Module | Responsibility |
|---|---|---|
| Config | `src/config.py` | One Pydantic settings object; all env vars validated at startup |
| Ingestion | `src/ingestion/` | Load from ArXiv & DuckDuckGo, chunk with recursive splitting |
| Retrieval | `src/retrieval/` | Chroma (dense) + BM25 (sparse) + cross-encoder rerank |
| LLM | `src/llm/` | Provider factory + prompt templates |
| Orchestration | `src/graph/workflow.py` | LangGraph state machine |
| Pipeline | `src/pipeline.py` | High-level `ingest()` / `ask()` API |
| Interfaces | `api.py`, `app.py` | FastAPI service, Gradio UI |
| Evaluation | `src/evaluation/` | RAGAS metrics runner |
## Data flow
```
User
β”‚ question
β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Gradio / FastAPI β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚
β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ ResearchPipeline β”‚ ◀── singleton, holds the Chroma store
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚
β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ LangGraph β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β–Ό β–Ό β–Ό
rewrite_query β†’ retrieve β†’ grade β†’ generate β†’ reflect
β–² β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
(one loop allowed)
```
## LangGraph nodes β€” what each one does
| Node | Purpose | Why it exists |
|---|---|---|
| `rewrite_query` | LLM rewrites the user's question into 2–3 search queries | Improves recall β€” academic search benefits from keyword variation that users don't naturally produce |
| `retrieve` | Hybrid retrieval over the corpus | The core retrieval step |
| `grade` | Per-document LLM call: is this relevant? | Filters out near-misses that the retriever still ranks high; cheap because we use the small/free LLM here |
| `generate` | LLM produces the answer with inline `[N]` citations | The synthesis step |
| `reflect` | LLM reads its own draft, decides "ok" or "go fetch X" | Lets the system self-correct on questions where the first retrieval missed key evidence |
The reflection loop is capped at **one iteration** (`MAX_REFLECTION_LOOPS = 1`) to bound latency and cost β€” important on free-tier inference where each call can take seconds.
## Retrieval strategy
**Why hybrid?**
Pure dense retrieval misses rare-keyword matches (e.g., specific algorithm names, model identifiers). Pure BM25 misses semantic paraphrases. Combining them and deduplicating consistently produces a higher-recall candidate set, which the cross-encoder then reorders for precision.
**Pipeline:**
1. Embed the query and the corpus chunks with `all-MiniLM-L6-v2` (384-dim, CPU-friendly).
2. Dense top-10 from Chroma (cosine similarity).
3. Sparse top-10 from BM25 over the same chunks.
4. Merge, dedupe by the first 200 chars of `page_content`.
5. Cross-encoder (`ms-marco-MiniLM-L-6-v2`) scores each (query, chunk) pair.
6. Return top-5.
## Why pluggable LLM providers?
In development we use the **Hugging Face Inference API** (free tier, no credit card). For higher quality or production load we'd switch to **Groq** (fast Llama 3 inference, generous free tier), **Anthropic** (Claude Haiku for cost-effective quality), or **OpenAI**. The factory in `src/llm/providers.py` is the only place that knows about provider SDKs β€” everything downstream depends on LangChain's `BaseChatModel` interface.
This is a deliberate design choice: it means swapping providers is a single env var change, not a refactor.
## Trade-offs and limitations
| Trade-off | Choice made | Why | What I'd change at scale |
|---|---|---|---|
| Vector store | Chroma (embedded) | Zero ops, persists to disk, works on HF Spaces | Qdrant or Weaviate for multi-tenant, sharded deployments |
| Embedding dim | 384 (MiniLM) | Fits on free CPU | bge-large (1024) on GPU for higher recall |
| Chunk size | 800 chars | Good middle ground for abstracts | Adaptive sizing per source type |
| BM25 in memory | In-process, rebuilt on ingest | Simple, no extra service | OpenSearch / Elasticsearch for persistence and scale |
| Reflection loops | Capped at 1 | Latency budget | Adaptive: stop when answer "stable" |
| Evaluation | Offline RAGAS | Standard, reproducible | Online A/B + LangSmith feedback loop |
## What runs where
- **Local dev**: `python app.py` (Gradio on :7860) or `uvicorn api:app` (FastAPI on :8000)
- **Docker**: `docker compose up` β€” both services with persisted Chroma + HF cache volumes
- **HF Spaces**: `bash scripts/deploy_hf_space.sh <user> <space>` β€” Docker SDK Space, app on :7860