Lesson 4 — Tester, évaluer, exploiter

Évaluer et améliorer

6 min read

This content is not translated yet. Showing the original version.

Évaluer et améliorer

flowchart LR
  G["golden.json : 10 questions + 2 pièges"] --> R["eval.py --recherche"]
  R --> V["rappel@k vecteur"]
  R --> B["rappel@k BM25"]
  R --> H["rappel@k hybride"]
  G --> A["eval.py : 12 réponses"]
  A --> F["fidélité (juge)"]
  A --> C["citations valides"]
  A --> RF["refus corrects / faux refus"]

1. Les tests sans clé

test_code_rag.py vérifie le découpage (lignes exactes, décorateurs, constantes), les jetons BM25, l'incrémental, la vérification des citations et les liens :

files/test_code_rag.py (extrait)
def test_citations_et_liens():
    hits = [{"path": "facturo/tva.py", "start": 6, "end": 10}]
    assert cr.citations_valides("Voir facturo/tva.py:8.", hits)
    assert not cr.citations_valides("Voir facturo/tva.py:30.", hits)    # hors de l'extrait
    assert not cr.citations_valides("La TVA est calculée.", hits)       # aucune citation
    assert not cr.citations_valides("Voir facturo/tva.py:8-12.", hits)  # la plage déborde
    assert cr.liens("cf. `facturo/tva.py:6-10`") == "cf. [facturo/tva.py:6-10](projet_demo/facturo/tva.py#L6-L10)"

2. Le rappel quand une question attend plusieurs morceaux

golden.json attend fichier::nom (par exemple facturo/relances.py::planifier_relances). Deux questions attendent deux morceaux (relances, remise maximale) : avec k = 1, leur rappel ne peut pas dépasser 0,5. C'est pour cela que rappel@1 plafonne sous 1,0 même avec une recherche parfaite.

Sur 10 questions, 8 attendent 1 morceau et 2 en attendent 2. Quel est le rappel@1 maximal possible ?

3. Trois corrections, trois mesures

Chaque ligne est une exécution réelle de eval.py --recherche (1 appel d'embedding chacune) le 2026-10-15 :

Versionrappel@1 V / B / Hrappel@3 V / B / Hrappel@5 V / B / H
a. morceau par docstring de module, accents gardés0,75 / 0,35 / 0,550,90 / 0,65 / 0,850,95 / 0,75 / 1,0
b. docstring de module en titre, plus en morceau0,75 / 0,50 / 0,700,95 / 0,80 / 1,01,0 / 0,90 / 1,0
c. b + accents retirés des jetons BM250,75 / 0,70 / 0,800,95 / 0,90 / 1,01,0 / 0,90 / 1,0

V = vectoriel, B = BM25, H = hybride (RRF).

  • En version a, l'hybride était moins bon que le vectoriel seul à k = 1 et 3 : BM25 tirait vers le haut des morceaux génériques (docstrings, cli.py qui contient le mot « relances »), et la fusion le suivait.
  • La version b a surtout aidé BM25 et l'hybride.
  • La version c fait passer BM25 de 0,50 à 0,70 à k = 1 ; l'hybride devient le meilleur mode à tous les k. La troisième correction (citations entre backticks, leçon précédente) concernait les liens, pas le rappel.

Résultat obtenu

Exécution réelle du 2026-10-15, version finale (gemini-3.5-flash pour les réponses, gemini-3.5-flash-lite pour le juge, k = 5) :

$ python -m pytest -v
... 10 passed in 1.13s

$ python eval.py
[ok] Où sont gérées les relances des factures impayées ?
[ok] Où est défini le taux de TVA réduit ?
[ok] Comment est calculé le total TTC d'une facture ?
[ok] Comment les numéros de facture sont-ils générés et quand la séquence repart-elle à 1 ?
[ok] Que se passe-t-il si l'envoi d'un e-mail échoue ?
[ok] Quelle est la remise maximale autorisée avec un code promo ?
[ok] Où les mots de passe sont-ils vérifiés et comment ?
[ok] Comment annule-t-on une facture déjà émise ?
[ok] Où est fait le rapprochement entre le relevé bancaire et les factures ?
[ok] Quelle variable d'environnement règle le délai de paiement ?
[REFUS] Où est gérée la connexion avec un compte Google (OAuth) ?
[REFUS] Comment le projet génère-t-il les QR codes de paiement ?
{"rappel@5 vecteur": 1.0, "rappel@5 bm25": 0.9, "rappel@5 hybride": 1.0, "fidelite": 1.0,
 "citations_valides": 1.0, "refus_corrects": 1.0, "faux_refus": 0.0}

L'exécution précédente (même code, avant la suppression des accents) avait donné "fidelite": 0.9 : le juge avait noté NON la réponse sur le total TTC. À la relecture, cette réponse était correcte (« total HT + TVA ligne par ligne, ajustée de la remise, arrondi au centime », ce que fait Facture.total_ttc) : c'était un faux négatif du juge. Une fidélité à 0,9 sur 10 réponses se relit avant de se croire.

Le juge note NON une réponse que vous trouvez juste. Que faire ?

4. Exploiter

SituationQue faire
un développeur pousse du codepython code_rag.py index <racine> dans la CI : seuls les fichiers modifiés sont réembeddés
un fichier ne se parse pas (SyntaxError)le signaler et continuer, plutôt que de bloquer tout l'index (exercice ci-dessous)
les numéros de ligne ont bougéles liens pointent vers la version indexée : réindexez, et utilisez un identifiant de commit dans les liens
code confidentiell'envoi des morceaux à l'API d'embedding est une sortie de données : vérifiez la politique de l'entreprise

Exercice : survivre à un fichier invalide

Aujourd'hui, un seul fichier avec une erreur de syntaxe fait échouer index_folder (ast.parse lève SyntaxError). Modifiez-le pour ignorer ce fichier, le compter dans stats["erreurs"], et continuer.

Voir la solution
files/code_rag.py (dans la boucle de index_folder)
        try:
            chunks = chunk_python(rel, src)
        except SyntaxError as e:
            print(f"ignoré : {rel} ligne {e.lineno} : {e.msg}")
            stats["erreurs"] = stats.get("erreurs", 0) + 1
            continue

Le continue est placé avant la mise à jour de la table files : l'empreinte du fichier fautif n'est pas enregistrée, il sera donc retenté à la prochaine indexation, une fois corrigé. Ses anciens morceaux restent dans l'index en attendant, ce qui vaut mieux qu'un trou.

Je sais faire

  • Comparer vectoriel, BM25 et hybride sur un même jeu de questions
  • Calculer le plafond d'un rappel@k quand une question attend plusieurs morceaux
  • Relier chaque correction à une mesure, et relire les verdicts du juge
  • Intégrer la réindexation incrémentale dans un flux de développement