Islam Mamedov commited on
Commit
b100e4f
·
1 Parent(s): 9e3e6cb

Complete ablation table: query rewriting 0.91/0.69, dense confirmed champion

Browse files
Files changed (1) hide show
  1. README.md +142 -3
README.md CHANGED
@@ -8,7 +8,146 @@ app_file: app.py
8
  pinned: false
9
  ---
10
 
11
- # FastAPI Codebase Q&A
12
 
13
- RAG system over FastAPI's source code, docs, and GitHub issues.
14
- Full write-up coming.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  pinned: false
9
  ---
10
 
11
+ # ⚡ FastAPI Codebase Q&A — an eval-driven RAG system
12
 
13
+ Ask questions about the [FastAPI](https://github.com/fastapi/fastapi)
14
+ codebase and get answers grounded in its actual source code, documentation,
15
+ and GitHub issues — with citations linking to the exact lines, and honest
16
+ refusals when the corpus doesn't contain the answer.
17
+
18
+ **Live demo:** https://huggingface.co/spaces/Pace200413/fastapi-codebase-qa
19
+
20
+ Every architectural decision in this project was made by measuring, not
21
+ guessing: a hand-labeled 42-question eval set gates every change, and the
22
+ ablation results below include the things that *didn't* work and why.
23
+
24
+ ## Architecture
25
+
26
+ ```
27
+ GitHub repo ──► Ingestion ──► Chunking ──► Indexing ──► Retrieval ──► Generation
28
+ code clone AST-aware ChromaDB dense LLM with
29
+ docs (en) API pull (tree-sitter) bge-small (winner of citations +
30
+ issues resumable md-aware + BM25 6 ablations) refusal rules
31
+ issue-aware
32
+ │ │
33
+ └────── Eval harness ◄─────┘
34
+ recall@5 · MRR · faithfulness ·
35
+ correctness · refusal accuracy
36
+ ```
37
+
38
+ - **Corpus:** 46 source files, 161 English docs, 175 closed issues →
39
+ **1,352 chunks** (363 code, 814 doc, 175 issue)
40
+ - **Chunking:** tree-sitter AST parsing — one chunk per function/class,
41
+ classes >150 lines split per-method, every chunk carries a context header
42
+ (`# File: fastapi/routing.py | Symbol: APIRouter`) and exact line numbers
43
+ for citations
44
+ - **Stack:** Python, ChromaDB, sentence-transformers (`bge-small-en-v1.5`),
45
+ rank-bm25, Groq (LLM), Streamlit — total cost: $0
46
+
47
+ ## Evaluation
48
+
49
+ 42 hand-labeled questions across four categories: **api_usage** (16),
50
+ **behavior** (11), **location** ("where is X defined", 8), and
51
+ **unanswerable** (7 — questions FastAPI's corpus genuinely can't answer,
52
+ to measure refusal). Gold labels mark which file/symbol contains the
53
+ answer; retrieval is scored by recall@5 and MRR, generation by an
54
+ LLM-as-judge (faithfulness, correctness) plus string-matched refusals.
55
+
56
+ ### Retrieval ablations
57
+
58
+ | Configuration | recall@5 | MRR | Verdict |
59
+ |---|---|---|---|
60
+ | **dense (bge-small)** | **0.91** | **0.71** | **winner — shipped** |
61
+ | hybrid (dense + BM25, RRF) | 0.86 | 0.70 | BM25 noise displaces gold chunks |
62
+ | hybrid, weighted RRF 2:1 | 0.86 | 0.71 | weighting doesn't recover recall |
63
+ | dense + bge-reranker-base | 0.91 | 0.63 | reranker *hurts* ranking |
64
+ | dense + bge-reranker-v2-m3 | 0.83 | 0.67 | newer reranker also hurts |
65
+ | hybrid + bge-reranker-v2-m3 | 0.86 | 0.68 | — |
66
+ | dense + LLM query rewriting | 0.91 | 0.69 | no gain; noisy expansions add drift |
67
+
68
+ **Why hybrid lost:** diagnostic runs showed dense@20 = hybrid@20 = 0.97 —
69
+ BM25 contributed zero unique gold candidates on this corpus, while its
70
+ common-token matches (issue chunks full of "request", "body", "json")
71
+ crowded correct chunks out of the top 5.
72
+
73
+ **Why the rerankers lost:** both generations of cross-encoder demoted raw
74
+ code chunks on "where is X defined" questions. A code chunk *is* the
75
+ answer without *discussing* the answer — and rerankers are trained to
76
+ score passages that discuss. dense@20 recall of 0.97 vs reranked@5 recall
77
+ of 0.83–0.86 shows they were given the right candidates and ranked them
78
+ worse.
79
+
80
+ ### Generation (dense, llama-4-scout via Groq)
81
+
82
+ | Metric | Score |
83
+ |---|---|
84
+ | Faithfulness (LLM judge) | 0.89 |
85
+ | Correctness (LLM judge) | 0.91 |
86
+ | Refusal accuracy (7 unanswerable questions) | 7/7 |
87
+
88
+ Refusal was the most fragile metric: it swung 6/7 → 0/7 → 7/7 across three
89
+ system-prompt variants. The fix was naming the failure mode in the prompt
90
+ ("the context chunks are search results: they may be loosely RELATED to
91
+ the question without actually answering it"). Without the eval harness
92
+ this regression would have shipped silently.
93
+
94
+ A score-threshold refusal gate was also tested and **rejected**: top-1
95
+ similarity scores for answerable (0.74–0.89) and unanswerable (0.72–0.79)
96
+ questions overlap heavily — semantic similarity can't distinguish "related
97
+ to FastAPI" from "answerable from FastAPI's corpus".
98
+
99
+ ## Failure analysis (what still breaks)
100
+
101
+ 1. **Common-token queries.** "Where are the Query, Path, and Body parameter
102
+ functions defined?" (`fastapi/param_functions.py`) misses even at k=20
103
+ in every configuration — all its query tokens are ubiquitous. LLM query
104
+ rewriting that injects likely identifiers is the targeted fix.
105
+ 2. **Docs without code.** FastAPI's docs embed examples via
106
+ `{* ../../docs_src/... *}` include-directives; the referenced snippets
107
+ aren't inlined into chunks, so some answers describe code they can't
108
+ show. Resolving the includes at chunking time is the top corpus
109
+ improvement.
110
+ 3. **Judge strictness.** Spot-checking answers flagged `[incorrect]` showed
111
+ at least one (JWT auth) is actually correct — the LLM judge penalizes
112
+ answers referencing code the truncated context implies but doesn't
113
+ show. Judge scores are treated as directional, not absolute.
114
+
115
+ ## Engineering notes
116
+
117
+ - **Idempotent, resumable ingestion** — issues already fetched are skipped,
118
+ so network failures mid-run just need a re-run
119
+ - **Everything cached** — LLM answers, judgments, and query rewrites are
120
+ cached to disk keyed by (model, prompt), making eval re-runs free on
121
+ Groq's rate-limited free tier
122
+ - **Rate-limit resilience** — retry with backoff; eval survived TPM/TPD
123
+ limits and a mid-run model deprecation (Groq retired llama-3.3 mid-sprint)
124
+ - **Self-building deploy** — the HF Space rebuilds the vector index from
125
+ committed `chunks.jsonl` on first boot; no binary artifacts in git
126
+ - **Unit-tested chunkers** and an eval suite that doubles as a regression
127
+ gate
128
+
129
+ ## Run it yourself
130
+
131
+ ```bash
132
+ git clone https://github.com/Pace200413/codebase-rag && cd codebase-rag
133
+ python -m venv .venv && source .venv/bin/activate
134
+ pip install -r requirements.txt PyGithub tree-sitter tree-sitter-python pytest
135
+ echo "GROQ_API_KEY=gsk_..." > .env # free key: console.groq.com
136
+
137
+ python src/ingest.py --repo fastapi/fastapi # needs GITHUB_TOKEN for issues
138
+ python src/chunk.py
139
+ python src/index.py
140
+ python src/ask.py "How do I return a custom status code?"
141
+ python src/eval.py --mode dense --answers # reproduce the numbers above
142
+ streamlit run app.py
143
+ ```
144
+
145
+ ## What I'd build next
146
+
147
+ - Resolve `docs_src` include-directives into doc chunks (highest-impact
148
+ corpus fix)
149
+ - Make ingestion repo-agnostic (`--repo any/repo`) — turn the project into
150
+ a tool
151
+ - Embedding-model ablation (bge-small vs bge-base vs code-specific models)
152
+ - Ingest GitHub Discussions, where most FastAPI Q&A actually lives
153
+ - CI job that fails the build if recall@5 drops below baseline