Spaces:
Running
Running
| # 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 | |