Case study
Upload a document, ask it questions, and get answers built only from its pages — with a citation on every claim, a critic that checks the draft before you see it, and a trace that shows the whole pipeline's work.
Ask an LLM about a document it hasn't read and it will answer anyway — fluently, confidently, and sometimes from thin air. Retrieval-augmented generation is the standard fix: find the relevant passages first, then tell the model to answer from those and nothing else. But most RAG demos treat retrieval as a solved step — one vector search, top few results, straight into the prompt — and that's exactly where they quietly fail. Vector search whiffs on exact terms like invoice numbers and names; keyword search misses paraphrases; and whatever slips through, the model happily narrates over the gaps.
Synapse was my attempt to take the retrieval problem seriously and to make every stage of the answer auditable. Retrieval quality, not the model, is where RAG lives or dies — which is why three of the seven backend modules are retrieval stages.
Ingestion first: loaders.py reads PDFs page-by-page (keeping page numbers for citations), chunker.py slices the text into ~1600-character chunks along paragraph boundaries with a 200-character overlap so facts straddling a boundary survive intact, and each chunk lands in three places — its text in sqlite, its embedding in a FAISS vector index, and its words in a BM25 keyword index.
When a question arrives at /api/chat, the orchestrator (orchestrator.py, about 140 lines) runs four LLM roles in sequence, streaming progress to the browser as server-sent events:
Every stage is visible in the UI: click "Show reasoning trace" and you get the planner's sub-questions, the exact chunks retrieved for each, the pre-critique draft, and the critic's actual review text. It is not one prompt — it's a plan–retrieve–draft–critique pipeline, and you can watch it think.
Each stage exists because the one before it has a known blind spot. Embeddings catch paraphrases ("firing" matches "termination") but fumble exact tokens; BM25 nails exact terms but misses rewordings — so run both. Their scores live on incomparable scales (a cosine of 0.83 versus a BM25 of 14.2 means nothing), so RRF ignores raw scores entirely and scores each chunk by its rank position in each list — a chunk ranked well by both retrievers floats to the top. And rank fusion is still just similarity math: it can rank a lexically-similar but off-topic passage highly. So the last word goes to an LLM that sees all 12 candidates at once and outputs an ordering — one cheap call over short previews, and it can spot what similarity scores can't.
Retrieval quality, not the model, is where RAG lives or dies. If the right passage isn't in the evidence, no prompt can save the answer.
| what | number | where it lives |
|---|---|---|
| retrieval funnel | 20 → 12 → 5 | config.py |
| chunk size / overlap | 1600 / 200 chars | chunker.py |
| planner cap | 3 sub-questions | config.py |
| session memory | last 6 turns | routes.py |
| rate limits | 20 chats, 10 uploads /hr/IP | rate_limit.py |
| tests | 31 across 7 files | pytest, no model needed |
| eval dataset | 2 cases — a stub | eval/dataset.json |
All 31 tests run instantly with no model installed, because every LLM call is faked — the fallback paths are the thing under test. CI is a single job on Python 3.12 running pytest on every push. And the eval harness deserves a plain sentence: it's real and runnable — an LLM judge scores each answer 1–5 on faithfulness — but the shipped dataset is 2 stub cases, so I don't quote a hit-rate or a faithfulness score, because I haven't earned one yet.
A real eval dataset, so faithfulness and hit-rate become numbers instead of claims — the harness is already built and waiting. Then latency: skip the planner for obviously simple questions and parallelize sub-question retrieval. Then the scaling list in order — Redis for sessions, an HNSW index, an incremental keyword index. Each is a known fix for a limitation I've already written down above.
One note before you click: the live demo runs on Render's free tier, which sleeps when idle — the first load can take a minute to wake — and uploaded documents reset whenever the instance restarts. Bring a small PDF and re-upload if it's gone.
David Jeremie Anand · New Delhi · every number on this page comes from config.py, the test suite, or the prompts in the repo — nothing measured is claimed that wasn't run.