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