The README table was a pair of unexplained numbers. Spell out recall vs ranking here, and expand EVAL.md §2 with the formulas used on this corpus. Co-authored-by: Cursor <cursoragent@cursor.com>
392 lines
20 KiB
Markdown
392 lines
20 KiB
Markdown
# Évaluation RAG — protocole et jeu de questions
|
||
|
||
> **Statut** : actif (13/09/2026). Jeu étendu à **101 questions** sourcées
|
||
> depuis la base locale (goldens vérifiés existants en base ; pertinence des
|
||
> fils canoniques à valider par un humain du forum).
|
||
> Principe : **on mesure par couche** (retrieval ≠ génération), cf. `REFERENCES.md` §5.
|
||
>
|
||
> **Intention d'exposition** : ce dossier `rag/` (protocole, résultats
|
||
> horodatés, tableau comparatif) est un livrable destiné à être **présenté
|
||
> publiquement à terme** (Forgejo). Conséquence sur la façon de travailler :
|
||
> - chaque run produit un JSON horodaté commité dans `results/` ;
|
||
> - le tableau §4 est rempli à chaque config testée, avec notes qualitatives ;
|
||
> - les commits racontent le raisonnement (pourquoi une config, ce que la
|
||
> mesure a montré) — pas juste la mécanique ;
|
||
> - un facteur à la fois (§3), pour que le tableau soit lisible comme une
|
||
> démonstration.
|
||
|
||
## 1. Structure du jeu d'éval
|
||
|
||
101 questions (visé initial 15–20 dépassé — un corpus plus large donne des
|
||
signaux moins bruités), chacune avec ses **golden documents** (fils/messages
|
||
qui *devraient* remonter). Format :
|
||
|
||
```yaml
|
||
- id: calceophilie
|
||
question: "C'est quoi la calcéophilie ?"
|
||
expected_topic_ids: [34091] # /t/du-terme-calceophile/34091
|
||
expected_post_ids: [] # vide = grain topic suffit
|
||
difficulty: facile
|
||
tags: [définition]
|
||
|
||
- id: <ex-megathread>
|
||
question: "<question dont la réponse est un message précis dans un long fil>"
|
||
expected_topics: [<topic_id>]
|
||
expected_posts: [<post_id>] # le message précis — vrai test grain post
|
||
difficulty: megathread
|
||
tags: [contexte, anaphore]
|
||
```
|
||
|
||
Couverture réelle (101 questions, `questions.yml` sections A–J) :
|
||
|
||
| Type de question | n | Ce que ça teste |
|
||
|---|---|---|
|
||
| Définition / technique fabrication | 7 | retrieval sémantique sur concepts (montages, trépointe…) |
|
||
| Avis marques | 24 | naviguer marque→fil canonique, megathreads jusqu'à 2.7k posts |
|
||
| Entretien / réparation | 21 | grain fin (cirage, glaçage, décoloration…) |
|
||
| Achat / occasion / budget | 8 | intentions transactionnelles |
|
||
| Pointure / largeur | 7 | sizing — réponses dispersées en posts |
|
||
| Types de chaussures | 12 | taxonomie produit + megathreads couleur |
|
||
| Vie du forum / règles | 10 | catégories non « souliers », sujets sensibles |
|
||
| Réponse dans un megathread (grain post) | 2 | **le cœur du chantier** — `expected_post_ids` |
|
||
| Hors-sujet | 6 | abstention correcte |
|
||
| Anaphore / contexte conversationnel | — | hors scope du harness single-turn — test manuel du bot |
|
||
| LLM indisponible | — | test manuel (message humoristique) |
|
||
|
||
## 2. Métriques
|
||
|
||
Les deux chiffres du README (**Hit@5 = 0.97**, **MRR = 0.89**) mesurent
|
||
**uniquement le retrieval** (le bon fil/post remonte-t-il ?), pas la
|
||
qualité de la phrase générée. Ils sont calculés sur le jeu golden
|
||
(`questions.yml`) : chaque question a un ou plusieurs `expected_topic_ids`
|
||
(et parfois `expected_post_ids`).
|
||
|
||
### Hit@5 (rappel dans le top-5)
|
||
|
||
Pour une question, le retrieval **réussit** si **au moins un** document
|
||
golden apparaît dans les 5 premiers résultats. Hit@5 = cette réussite
|
||
moyennée sur le jeu (0 ou 1 par question).
|
||
|
||
- **0.97** (config E, n=796) : 97 questions sur 100 voient le bon fil dans
|
||
le top-5.
|
||
- Un Hit@5 à 1.0 avec le bon fil toujours en 5e place resterait « parfait »
|
||
au sens du rappel, et mauvais au classement → d'où le MRR à côté.
|
||
- Variantes dans les tableaux : **Hit@k topic** (le fil golden est dans le
|
||
top-k) ; **Hit@k post** (le *message* golden est dans le top-k, grain
|
||
post uniquement). Le chiffre vitrine est le Hit@5 **topic** à l'échelle
|
||
(lot K). Les premières passes utilisaient aussi k ∈ {3, 6} sur le lot
|
||
curé A–J.
|
||
|
||
### MRR — Mean Reciprocal Rank (classement du premier hit)
|
||
|
||
Pour chaque question : `1 / rang` du **premier** résultat pertinent
|
||
(1er → 1, 2e → 0.5, 3e → ≈ 0.33, 5e → 0.2). Si le golden est hors de la
|
||
liste renvoyée : **0**. MRR = moyenne de ces scores.
|
||
|
||
- **0.89** (config E) : le premier hit pertinent est en pratique 1er ou
|
||
2e, rarement 4e/5e.
|
||
- Contrairement au Hit@5, un golden passé de #1 à #5 **fait baisser** le
|
||
MRR (1 → 0.2) même s'il reste « dans le top-5 ».
|
||
|
||
En une ligne : **Hit@5 = est-ce qu'on a raté ?** · **MRR = est-ce qu'on
|
||
l'a mis en haut ?** Le saut topic → post (0.68 → 0.94 Hit@5) est le gain
|
||
principal ; le rerank (0.94 → 0.97 Hit@5, 0.84 → 0.89 MRR) améliore
|
||
surtout l'ordre.
|
||
|
||
### Retrieval (automatique) — récap
|
||
|
||
| Métrique | Définition |
|
||
|---|---|
|
||
| **Hit@5 topic** | au moins un `expected_topic` dans le top-5 (chiffre README) |
|
||
| **Hit@k topic** | idem pour un k donné (lots A–J : aussi k ∈ {3, 6}) |
|
||
| **Hit@k post** | le `expected_post` dans le top-k posts (grain post uniquement) |
|
||
| **MRR** | moyenne de `1/rang` du premier hit pertinent (0 si absent) |
|
||
| Pollution | slots evidence occupés par un même topic (mesure l'agrégation) |
|
||
|
||
### Génération (semi-auto)
|
||
|
||
| Métrique | Comment |
|
||
|---|---|
|
||
| Liens cités réels | % `[[n]]` → `Post.exists?` — devrait être 100 % (sanitizer) |
|
||
| Citation pertinente | le message cité porte-t-il la réponse ? (humain, 0/1) |
|
||
| Fidélité | claims supportées par l'evidence (LLM-juge ou relecture) |
|
||
| Abstention correcte | hors-sujet → abstention, pas d'invention |
|
||
| CTA présent | footer « Pour aller plus loin » avec invitation à poster |
|
||
|
||
## 3. Protocole
|
||
|
||
1. **Baseline `A`** (config live, grain topic) : faire tourner le jeu sur le
|
||
forum local → remplir le tableau. C'est la référence à battre.
|
||
2. Chaque config (`B`, `C`, `D`, `E`…) : **un seul facteur changé** vs la
|
||
config précédente, même jeu, mêmes métriques.
|
||
3. Résultats → tableau ci-dessous + notes qualitatives (exemples marquants).
|
||
4. Une question qui régresse = on la comprend avant de conclure.
|
||
|
||
## 4. Tableau de résultats (à remplir)
|
||
|
||
| Config | Hit@5 topic | Hit@k post | Liens réels | Abstention OK | Notes |
|
||
|---|---|---|---|---|---|
|
||
| A (live, topics) | **39/95** (kw 17/95, sem 32/95) | n/a | | | 13/09 v2, goldens corrigés — hybrid=41% |
|
||
| B (posts natifs) | **88/95** | post exact `#1` ×2 (carmina-sizing, glaçage-sèche-cheveux) | | | 13/09 v2 — 425k posts embeddés |
|
||
| C (posts enrichis) | **0.85** (n=796) | — | | | 14/09 — **mesuré, rejeté** : enrichir le texte embeddé (extrait OP + parent) dégrade vs B (0.94) — dilution du contenu du post, §4quater |
|
||
| D (+ voisinage) | | | | | côté génération (contexte LLM), pas Hit@k |
|
||
| E (+ rerank) | **97%** (n=796) | `p✓` conservés | | | 13/09 — cross-encoder CPU, Hit@5=0.97/MRR=0.89 (rrf rerank×hybrid) — détail §4ter |
|
||
| F (fusions RRF) | **87-88/95** | `p✓` ×2 conservés en #1 | | | 13/09 v2 — posts×hybrid MRR **0.80** |
|
||
| G (routing taille) | 84-88/95 | `p✓` ×2 | | | 13/09 — **rejeté** : dégrade vs F, le RRF route déjà implicitement |
|
||
|
||
Constats routing par taille (13/09, seuils 25/50/100/250 posts) : filtrer les
|
||
topics > N posts du canal topic avant fusion **dégrade monotonement** (MRR
|
||
0.68@25 → 0.76@250 < 0.80 sans filtre). Les longs fils n'obtiennent de toute
|
||
façon quasiment jamais de vote « topic » (embedding dilué = invisible), donc
|
||
il n'y a pas de bruit à retirer ; couper les fils moyens retire des votes
|
||
utiles. **Le RRF implémente déjà l'esprit du routing** : un megathread ne
|
||
remonte que via ses posts, un fil court profite des deux canaux. Variante
|
||
documentée puis abandonnée — résultat négatif, gardé pour la démonstration.
|
||
|
||
Constats baseline A→B (13/09, n=95, goldens v2) : le canal posts natif seul
|
||
fait **Hit@5 0.93 / MRR 0.77** vs hybrid topic 0.41. Correction de goldens
|
||
par pooling (13/09) : les « échecs » initiaux remontaient des fils **valides**
|
||
absents de la liste attendue — élargis pour `corthay-avis`, `noeuds-lacets`,
|
||
`presentation-nouveau-membre`, `bienvenue-nouveau` ; `richelieu-bout-rapporte`
|
||
reformulé (un membre nomme la marque). **Leçon méthodo : l'écart initial
|
||
0.87→0.93 vient de la complétude des goldens, pas du retrieval** — tenir les
|
||
goldens à jour est la partie la plus fragile du benchmark.
|
||
|
||
Constats fusions (13/09) : RRF posts×{semantic, hybrid} **ne gagne pas de
|
||
rappel** (posts suffit) mais **améliore le MRR** (0.77→0.80) et rescute des
|
||
échecs posts (`loding-pointure` #23→**#1**, `grande-pointure` #11→#1 — le
|
||
sizing dilué en posts profite du signal topic sémantique). RRF posts×keyword
|
||
**dégrade** (bruit BM25 : MRR 0.74). Pollution canal posts : top-5 = 2.8
|
||
topics distincts en moyenne, topic dominant 63 % des slots.
|
||
|
||
Constats baseline A (12/09) : le megathread Carmina (2690 posts) absent des
|
||
top-20 sémantiques **et** top-25 keyword → la dilution topic-embedding est
|
||
réelle et mesurée. La gate titre (`relevance_score` exige un mot distinctif
|
||
dans le titre) rejette aussi des fils pertinents dont le titre ne contient
|
||
pas le mot-clé — à garder en tête quand on lira les métriques post-grain.
|
||
|
||
## 4bis. Confirmation à grande échelle (812 questions, 13/09)
|
||
|
||
Le lot A-J (101 questions curées, goldens vérifiés par pooling) a été
|
||
complété par un **lot généré à l'échelle** (section K de `questions.yml`,
|
||
+701 questions + 10 hors-sujet, sourcées par mining large de toutes les
|
||
catégories/tags de la base — méthodologie différente, documentée en tête
|
||
de section, golden = topic source unique sans union multi-fils). Objectif :
|
||
vérifier que les résultats du lot curé ne sont pas un artefact de petit
|
||
échantillon.
|
||
|
||
| Canal | Hit@5 (n=796) | MRR |
|
||
|---|---|---|
|
||
| semantic (topic) | 0.55 | 0.43 |
|
||
| keyword (topic) | 0.41 | 0.38 |
|
||
| posts natifs | **0.94** | **0.84** |
|
||
| hybrid (prod actuelle) | 0.68 | 0.63 |
|
||
| rrf posts×hybrid | **0.95** | **0.88** |
|
||
| routed_250 | 0.95 | 0.86 |
|
||
| posts enrichis (C) | 0.85 | 0.80 |
|
||
| rrf enriched×hybrid | 0.92 | 0.85 |
|
||
| rerank enriched | 0.91 | 0.86 |
|
||
|
||
**Confirmé** : l'avantage posts natifs / fusion RRF tient à l'échelle, en
|
||
s'améliorant légèrement (bruit statistique réduit). **Écart notable vs le
|
||
lot curé** : `hybrid` prod fait 0.68 ici contre 0.41 sur A-J — le lot généré
|
||
contient beaucoup de titres très distinctifs (marques/produits nommés) que
|
||
le BM25 matche facilement, alors que A-J teste délibérément des
|
||
reformulations et megathreads plus durs. Les deux mesures sont
|
||
complémentaires : A-J documente les cas fins, K mesure la robustesse
|
||
générale. Le résultat du routing par taille (rejeté sur A-J) est confirmé
|
||
rejeté ici aussi (routed_250 < rrf_post_hyb sans filtre).
|
||
|
||
## 4ter. Config E — rerank cross-encoder (13/09, n=796)
|
||
|
||
Modèle : `cross-encoder/mmarco-mMiniLMv2-L12-H384-v1` (multilingue, léger,
|
||
CPU — chargé une fois via `sentence-transformers`, `--rerank` dans
|
||
`run_eval.py`). Re-trie le top-25 du canal posts (`{titre}. {300 premiers
|
||
caractères du post}` vs question) avant dédup par topic.
|
||
|
||
| Canal | Hit@5 | MRR |
|
||
|---|---|---|
|
||
| posts natifs | 0.94 | 0.84 |
|
||
| rrf posts×hybrid | 0.95 | 0.88 |
|
||
| **rerank** | **0.97** | 0.88 |
|
||
| **rrf rerank×hybrid** | **0.97** | **0.89** |
|
||
|
||
Le rerank gagne surtout en **rappel** (0.94→0.97 Hit@5) : il remonte des
|
||
posts pertinents mal classés par la seule distance cosine, déjà présents
|
||
dans le top-25 mais pas dans le top-5. Gain net mais incrémental par
|
||
rapport au saut initial topic→post (qui reste le facteur dominant du
|
||
chantier). `eval_retrieval.rb` expose désormais `text` (titre + extrait
|
||
300c) par post pour permettre le rerank côté hôte sans étape supplémentaire
|
||
côté conteneur.
|
||
|
||
## 4quater. Config C (posts enrichis) — mesuré, rejeté (14/09, n=796)
|
||
|
||
Implémentée pour la vitrine (métriques comparables sur tout le dataset) :
|
||
backfill complet `strategy_id=2` sur les 425 505 posts
|
||
(`backfill_enriched.rb`, ~3h GPU). Texte embeddé = contexte natif
|
||
(titre+cat+tags) **+ extrait 500c du message initial du fil + extrait 400c
|
||
du post parent** (`reply_to_post_number`), tronqué à max_seq−2 comme le
|
||
natif. Hypothèse : désambiguïser les réponses courtes (« pareil en 8.5 »)
|
||
que le post nu ne contextualise pas.
|
||
|
||
| Canal | Hit@5 | MRR |
|
||
|---|---|---|
|
||
| posts natifs (s1) | **0.94** | **0.84** |
|
||
| **posts enrichis (s2)** | 0.85 | 0.80 |
|
||
| rrf posts×hybrid | **0.95** | **0.88** |
|
||
| rrf enriched×hybrid | 0.92 | 0.85 |
|
||
| rerank (s1) | **0.97** | 0.88 |
|
||
| rerank enriched (s2) | 0.91 | 0.86 |
|
||
|
||
**Résultat négatif, uniforme sur toutes les variantes** : l'enrichissement
|
||
coûte ~9 pts de Hit@5 et ~4 pts de MRR au canal seul, et la dégradation se
|
||
propage aux fusions et au rerank. Interprétation : l'extrait OP/parent
|
||
consomme du budget de tokens et **dilue le contenu propre du post** dans
|
||
le vecteur — le bruit contextuel nuit plus que la désambiguïsation n'aide.
|
||
Cas typique : `scaled-faq-les-diff-rents-montages` — le golden (36778)
|
||
est #1 en s1 mais **sort du top-25 en s2** : le contexte OP mutualisé
|
||
uniformise les vecteurs des posts d'un même fil et fait basculer vers un
|
||
fil voisin (36856) qui squatte les 5 premiers slots à distance ~égale.
|
||
|
||
**Décision : rejetée sur mesure.** Le post nu (titre+cat+tags+contenu) est
|
||
la bonne granularité. Le contexte a sa place **au moment de la génération**
|
||
(config D — voisinage injecté dans le prompt LLM, sans toucher aux
|
||
embeddings), pas dans le vecteur indexé.
|
||
|
||
Note dataset : les échecs résiduels (~3 % après rerank) restent dominés par
|
||
les titres génériques du lot K (« Présentation » : 1671 fils proches en
|
||
base, « Question » : 211, « ceinture » : 123) dont le golden single-topic
|
||
est structurellement ambigu — limite de mesure, pas du retrieval.
|
||
|
||
## 4quinquies. Éval FULL — génération réelle (14/09, n=17→20)
|
||
|
||
Premier test bout-en-bout avec le **vrai LLM de prod** (`qwen3:30b-a3b-q6k`,
|
||
vLLM mini PC `:32134`). `eval_full.rb` émet par question l'evidence prod
|
||
(`instructions_block`/`for_question` tels quels) + le top-25 posts natifs ;
|
||
`run_full_eval.py` reconstruit le bloc « new » (posts+rerank, même gabarit),
|
||
appelle le LLM avec `system = persona + block`, et recueille la réponse +
|
||
les marqueurs `[[n]]` pour validation. Digest lisible dans
|
||
`results/*-full-*.yml`.
|
||
|
||
### Enseignements (17 questions : 14 in-domain, 3 hors-sujet)
|
||
|
||
| | prod (grain topic) | new (posts+rerank) |
|
||
|---|---|---|
|
||
| in-domain | répond sur 12/14 ; **abstention à tort ×2** (`montage-norvegien-def`, `carmina-sizing-megathread` — evidence vide) | répond sur 14/14 |
|
||
| hors-sujet | s'abstient ×3 proprement | **hallucine ×3** avec citations (recette inventée, « 42 °C à Paris », « comité d'investissement DPEC ») |
|
||
| citations invalides | 2 (`scaled-aston-martin` cite [[2]][[3]] avec 1 seule source) | 0 |
|
||
| latence LLM | ~3-10 s/réponse (mini PC) | idem |
|
||
|
||
**Constat central** : la gate titre de prod servait d'abstention *par
|
||
accident* — hors-sujet → aucun mot distinctif dans les titres → evidence
|
||
vide → abstention. En la supprimant, on gagne le rappel in-domain (les 2
|
||
abstentions à tort sont corrigées) mais on **perd la protection
|
||
out-of-domain** : le canal posts retourne toujours 25 candidats et le LLM
|
||
les exploite, même hors propos.
|
||
|
||
**Le score CE seul ne tranche pas proprement** (calibration 14/09) :
|
||
top-1 CE sur 796 questions in-domain : min −4.74, p25 +0.42, 4/796 < −4 ;
|
||
sur 16 hors-sujet : −5.9 → **+2.77**. L'overlap vient du Comptoir —
|
||
le forum parle cuisine, vin, cinéma : « bœuf bourguignon » a des fils
|
||
tangentiels légitimes (cinema hors-sujet = +2.77). Un seuil −4 coupe les
|
||
cas extrêmes sans faux négatif mesuré in-domain, mais laisse passer la
|
||
zone grise ; c'est autant une décision **produit** (le bot doit-il répondre
|
||
aux questions Comptoir ?) qu'un réglage technique.
|
||
|
||
**À décider pour le design final de l'abstention** (options, pas encore
|
||
tranché) : seuil CE calibré, nombre minimal de sources au-dessus d'un
|
||
score, deuxième signal lexical, ou catégories à exclure du retrieval.
|
||
L'éval full a rempli son rôle : révéler ce déficit que Hit@k ne mesure
|
||
pas — 0.97 de rappel ≠ réponses sûres.
|
||
|
||
### 4.4 Solution mesurée : zone graduée (weak evidence) — 14/09 run `weak-zone`
|
||
|
||
Le gate binaire (evidence pleine / vide) est remplacé par **trois zones**
|
||
sur le score CE top-1 : `< −4` → evidence vide (abstention) ;
|
||
`[−4, +1[` → bloc `WEAK_EVIDENCE` : les sources sont fournies mais avec
|
||
l'instruction d'assumer la pertinence faible (« dis honnêtement qu'aucun
|
||
extrait ne répond vraiment, pointe le fil le plus proche avec prudence,
|
||
invite à poster ») ; `≥ +1` → evidence normale.
|
||
|
||
Résultat sur les 17 cas représentatifs (`--variants new`,
|
||
`--weak-threshold 1.0`) :
|
||
|
||
- **les 3 hallucinations hors-sujet disparaissent** : cuisine → « aucun
|
||
extrait ne fournit une recette » ; meteo → « aucune information
|
||
précise » ; finance → « aucune recommandation disponible ». Plus de
|
||
« 42 °C » ni de « comité d'investissement DPEC » affirmés.
|
||
- **aucune régression in-domain** : `calceophilie` (CE −2.32, zone
|
||
faible !), `bannissement` (−1.87), `carmina-megathread` (−0.91),
|
||
`presentation` (−0.21) répondent tous correctement — le cadrage gradué
|
||
ne transforme pas les questions légitimes à CE bas en abstentions.
|
||
- les cas confiants (CE ≥ +1, incluant Comptoir : aston-martin +3.8,
|
||
finsbury +3.4) répondent normalement.
|
||
|
||
Enseignement clé : l'overlap des distributions CE (in-domain min −4.7 vs
|
||
hors-sujet max +2.8) rend tout seuil dur imparfait — **mais la zone
|
||
graduée n'a pas besoin de séparer proprement** : les cas ambigus sont
|
||
traités avec honnêteté plutôt que tranchés. C'est la réponse mesurée au
|
||
problème « grain post sans protection OOD » relevé en §4.3.
|
||
|
||
**Attention à l'arbitrage qualité/sécurité** — distribution CE top-1
|
||
recomputée sur les 796 questions in-domain (offline, sans LLM) :
|
||
|
||
| Seuil weak | % questions légitimes en zone « prudente » |
|
||
|---|---|
|
||
| +2.0 | 33 % |
|
||
| +1.0 | **25 %** (valeur du premier test — trop haut) |
|
||
| **0.0** | **17 %** |
|
||
| −1.0 | 10 % |
|
||
| −4.0 (abstention) | 0 % |
|
||
|
||
Les 3 hors-sujet mesurés (−0.74, −0.46, −3.60) tombent tous dans
|
||
`[−4, 0)` → seuil **0.0** capture les mêmes cas en divisant presque par
|
||
deux l'exposition des questions légitimes (25 % → 17 %). Le coût du
|
||
bloc WEAK est stylistique (réponse plus prudente), pas une abstention —
|
||
les questions légitimes à CE bas y répondent correctement (calceophilie
|
||
−2.3). Arbitrage à confirmer par run A/B à l'échelle.
|
||
|
||
Fichier : `results/20260914-115544-weak-zone.*`. Voir aussi `SAFETY.md`
|
||
(axe refus/abus — complémentaire : le weak-block gère la pertinence
|
||
faible, la clause de refus gère les demandes hors-rôle).
|
||
|
||
## 5. Harness
|
||
|
||
`rag/eval/` :
|
||
|
||
- `questions.yml` — jeu de questions + `expected_topic_ids` (golden). Le
|
||
seed initial est **à valider/enrichir** (certaines attendues sont des
|
||
suppositions — vérifier que le fil listé est bien le canonique).
|
||
- `safety.yml` — sondes de refus/injection (axe sécurité, cf. `SAFETY.md`).
|
||
- `input_gate.yml` — règles regex du gate d'entrée (intention, pas sujet).
|
||
- `prompt_clauses/` — variantes de system prompt testables sans toucher
|
||
la DB (`--system-append`).
|
||
- `eval_retrieval.rb` — tourné **dans le conteneur** (`rails runner`) :
|
||
rejoue `Retrieval` sans cache, décompose semantic / keyword / hybrid.
|
||
- `run_eval.py` — côté hôte : copie le ruby, exécute, calcule Hit@k/MRR
|
||
par canal, sauvegarde `results/<date>-<label>.json`.
|
||
- `eval_full.rb` + `run_full_eval.py` — éval **bout-en-bout** : retrieval
|
||
prod vs posts+rerank, vrai LLM (mini PC), validation des citations,
|
||
gate, zone faible, injection d'evidence.
|
||
|
||
```bash
|
||
python3 -m venv rag/eval/.venv && rag/eval/.venv/bin/pip install pyyaml
|
||
rag/eval/.venv/bin/python rag/eval/run_eval.py --label baseline-topic
|
||
# options : --k 6 --container app --questions rag/eval/questions.yml
|
||
|
||
# pour --rerank (config E, cross-encoder CPU, ~1.5 Go RAM au chargement) :
|
||
rag/eval/.venv/bin/pip install sentence-transformers
|
||
rag/eval/.venv/bin/python rag/eval/run_eval.py --label with-rerank --rerank
|
||
|
||
# éval génération bout-en-bout (vrai LLM mini PC, ~10 s/question) :
|
||
rag/eval/.venv/bin/python rag/eval/run_full_eval.py \
|
||
--questions rag/eval/safety.yml --label safety-v3 \
|
||
--input-gate rag/eval/input_gate.yml \
|
||
--system-append rag/eval/prompt_clauses/refusal.txt \
|
||
--weak-threshold 0.0
|
||
# options : --n 20 --ids a,b,c --variants prod,new --abstain-threshold -4
|
||
```
|
||
|
||
> Les métriques retrieval tournent **sans LLM** → boucle d'itération rapide.
|
||
> L'éval génération appelle le vrai `qwen3:30b` du mini PC via Tailscale —
|
||
> compter ~10 s par question et par variante.
|