Un système RAG complet écrit sans framework (pas de LangChain), pour
comprendre chaque pièce, et — surtout — un harnais d'évaluation recall@k
qui mesure la qualité de la recherche au lieu de la deviner.
RAG en une phrase : un LLM ne répond qu'à partir de ce qu'on met dans son prompt ; tout le métier consiste à y mettre les bons paragraphes — et à mesurer qu'on y arrive.
- Embeddings : Voyage (
voyage-3.5-lite), avecinput_typequery/document (retrieval asymétrique). - Génération : Claude (
claude-sonnet-4-6), réponse ancrée « uniquement à partir du contexte ». - Appels API en
fetchbrut, sans SDK, pour garder la mécanique visible.
corpus/*.md → chunk → embed → store.json (l'index vectoriel) → retrieve → generate
| Fichier | Rôle |
|---|---|
src/chunk.ts |
découper les documents en chunks |
src/embed.ts |
texte → vecteurs (POST Voyage) |
src/similarity.ts |
similarité cosinus |
src/ingest.ts |
découpe + embedde + écrit store.json |
src/retrieve.ts |
top-k par cosinus |
src/ask.ts |
recherche + génération Claude |
src/evalset.ts est une vérité terrain écrite à la main (question → fichier
attendu → fragment de la vraie réponse). src/eval.ts calcule le recall@k :
la proportion de questions dont un chunk récupéré contient réellement la réponse.
src/sweep.ts rejoue ce recall pour plusieurs tailles de chunk. Résultat :
recall@1 selon la taille de chunk :
1.00 géant (fichier entier) [5 chunks]
1.00 baseline 120/20 [9 chunks]
0.80 petit 40/0 [20 chunks]
0.47 minuscule 12/0 [60 chunks]
Enseignements : (1) un benchmark saturé (100 %) ne discrimine rien, il faut le durcir ; (2) des chunks trop petits déchiquettent la réponse → le recall s'effondre, alors même que leur score cosinus est le plus élevé (le score absolu est trompeur) ; (3) le recall seul ne punit pas les chunks géants — leur défaut (contexte gaspillé) demande une métrique de précision.
npm install
cp .env.example .env # puis coller VOYAGE_API_KEY et ANTHROPIC_API_KEY
npm run ingest
npm run ask "comment réinitialiser mon mot de passe ?"
npm run eval # recall@k (K=1 par défaut, ex. K=5 npm run eval)
npm run sweep # comparer les tailles de chunk