Spaces:
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:
- Hybrid retrieval β dense embeddings + BM25 in parallel, merged and reranked.
- LangGraph workflow β query rewriting, evidence grading, generation, and a reflection loop, modeled as a state machine.
- 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:
- Embed the query and the corpus chunks with
all-MiniLM-L6-v2(384-dim, CPU-friendly). - Dense top-10 from Chroma (cosine similarity).
- Sparse top-10 from BM25 over the same chunks.
- Merge, dedupe by the first 200 chars of
page_content. - Cross-encoder (
ms-marco-MiniLM-L-6-v2) scores each (query, chunk) pair. - 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) oruvicorn 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