# Evals Cookbook

Conception de golden set, graders, calibration du LLM-as-judge, tests A/B avec significativité, YAML de régression CI, reporting par segment et dashboards de coût/latence.

import { Steps } from '@prosefly/astro-components';

Les evals transforment « ça semble mieux » en preuve. La posture correcte à l’examen : un **golden set**, le **grader suffisant le moins cher**, des juges dans une **session séparée**, un reporting **par segment** et un **gate de régression CI** sur les changements de prompt/modèle.

:::tip[Signal d’examen]
« L’exactitude globale est de 95 % » est un piège — exigez des métriques par segment. « Are you sure? » dans le même chat est un piège — utilisez une nouvelle session/un autre modèle. Comparer deux prompts sans significativité est un piège — utilisez un A/B par paires avec un test de significativité.
:::

## Golden-set design

<Steps>
1. **Couvrir la distribution** — échantillonnez des entrées réelles sur tous les segments (type de document, langue, difficulté, cas limites), pas seulement les faciles.
2. **Inclure des cas difficiles/adversariaux** — tentatives d’injection, entrées ambiguës, échecs passés connus.
3. **Étiqueter avec des sorties attendues ou des grilles** — réponses exactes quand c’est possible ; grilles pour les tâches subjectives.
4. **Taille** — assez par segment pour détecter les régressions (visez ≥ 30–50 par segment pour des taux stables).
5. **Versionner et geler** — un golden set est un actif contrôlé ; le changer invalide les comparaisons.
6. **Holdout séparé** — gardez un ensemble contre lequel vous ne réglez jamais pour détecter le surapprentissage sur l’eval.
</Steps>

## Graders — cheapest sufficient first

| Grader | À utiliser quand | Coût | Fiabilité |
| --- | --- | --- | --- |
| Exact / normalised match | Réponses déterministes (classification, champs d’extraction) | Gratuit | Élevée |
| Regex / structural | Format, présence de champs, validité de schéma | Gratuit | Élevée |
| Heuristic (tolérance numérique, recouvrement d’ensembles) | Nombres, listes | Gratuit | Moyenne |
| Rubric grading | Qualité subjective avec critères clairs | Faible | Moyenne |
| LLM-as-judge | Qualité ouverte, préférence par paires | Plus élevé | Moyenne (nécessite calibration) |
| Human | Vérité terrain, référence de calibration | Le plus élevé | La plus élevée |

Escaladez seulement autant que la tâche l’exige — exact match avant rubric avant LLM-judge.

## LLM-as-judge calibration

Le juge doit s’exécuter dans une **session séparée, idéalement un modèle différent**, et être **calibré contre des étiquettes humaines** avant que vous ne lui fassiez confiance.

<Steps>
1. Rédigez la grille avec des critères explicites, non chevauchants, et une échelle.
2. Faites étiqueter par des humains un jeu de calibration (par exemple 100 items).
3. Exécutez le juge sur le même jeu ; mesurez l’accord (par exemple le κ de Cohen, ou le % d’accord).
4. Si l’accord est faible, resserrez la grille, ajoutez des exemplaires few-shot de chaque note, ou réduisez l’échelle (le binaire est plus facile à calibrer que 1–10).
5. Remesurez ; ne déployez le juge qu’une fois l’accord atteignant votre seuil (par exemple κ ≥ 0.6).
6. Recalibrez quand le modèle, la grille ou la tâche change.
</Steps>

| Symptôme de calibration | Correctif |
| --- | --- |
| Juge trop indulgent | Ajoutez des exemplaires négatifs ; affûtez les critères de « fail » |
| Juge incohérent d’un run à l’autre | Baissez la température ; grille binaire ; vote majoritaire de 3 |
| Biais de position en comparaison par paires | Randomisez l’ordre A/B ; moyennez les deux ordonnancements |
| Biais de verbosité | Instruisez d’ignorer la longueur ; pénalisez les affirmations non étayées |

## A/B testing with significance

Comparaison par paires du prompt/modèle B contre A. Ne jugez pas à l’œil quelques sorties — testez.

Exemple travaillé : B gagne 118 des 200 comparaisons en tête-à-tête (égalités partagées). B est-il vraiment meilleur ?

```text
n = 200, wins = 118, p̂ = 0.59
Two-sided test of p = 0.5:
  z = (118 − 100) / sqrt(200 × 0.5 × 0.5) = 18 / 7.07 ≈ 2.55
  p-value ≈ 0.011  → significant at α = 0.05
95% CI on win rate: 0.59 ± 1.96 × sqrt(0.59×0.41/200) = 0.59 ± 0.068 → [0.52, 0.66]
```

B est significativement meilleur. Si B avait gagné 108/200 : `z ≈ 1.13`, p ≈ 0.26 — **non** significatif ; ne déployez pas sur cette seule base.

| Piège | Correctif |
| --- | --- |
| Trop peu d’échantillons | Calibrez la puissance du test ; plus d’items pour de petits effets |
| Peeking / arrêt précoce | Fixez n à l’avance ou utilisez des corrections de test séquentiel |
| Ignorer les égalités | Définissez leur traitement d’emblée |
| Agrégat seulement | Testez aussi par segment |

## CI regression gate

Exécutez le golden set à chaque changement de prompt/modèle ; bloquez le déploiement si un segment régresse au-delà de la tolérance.

```yaml
# .github/workflows/evals.yml
name: evals
on: [pull_request]
jobs:
  eval:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: pip install -r eval/requirements.txt
      - name: Run golden set
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
        run: python eval/run.py --golden eval/golden.jsonl --out eval/report.json
      - name: Gate
        run: |
          python - <<'PY'
          import json, sys
          r = json.load(open("eval/report.json"))
          # per-segment gate: no segment below its baseline minus 2 points
          fails = [s for s, m in r["by_segment"].items() if m["accuracy"] < r["baseline"][s] - 0.02]
          if fails:
              print("Regressed segments:", fails); sys.exit(1)
          print("All segments within tolerance.")
          PY
```

Conditionnez sur des seuils **par segment**, pas seulement sur l’agrégat — c’est ce qui attrape un unique type de document défaillant.

## Per-segment reporting

```text
Segment          n     Accuracy   Faithfulness   Δ vs baseline
--------------   ---   --------   ------------   -------------
invoices         120   0.94       0.95            +0.01
contracts         90   0.88       0.91            −0.00
receipts (scan)   60   0.61 ◄     0.72 ◄          −0.18 ◄
--------------   ---   --------   ------------   -------------
OVERALL          270   0.86       0.90            −0.03
```

L’agrégat de 0.86 paraît correct ; le segment des reçus scannés à 0.61 est la vraie histoire. Reportez toujours la ventilation.

## Cost / latency dashboards

| Métrique | Pourquoi | À surveiller |
| --- | --- | --- |
| Coût par requête / par 1k | Budget | Pic après un changement de modèle ou de prompt |
| Tokens in/out (p50/p95) | Coût + pression de contexte | Prompts qui grossissent → cachez ou élaguez |
| Latence p50 / p95 / p99 | UX / SLO | Dépassement du p95 même quand le p50 va bien |
| Cache hit rate | Efficacité de coût | Baisse → préfixe invalidé |
| Taux d’erreur par type (429/5xx/refusal) | Fiabilité | 429 → palier de rate-limit ; refus → prompt/politique |
| Fallback rate | Capacité | Élevé → problème de capacité ou de modèle |
| Qualité (score d’eval) dans le temps | Régression | Dérive silencieuse après des changements en amont |

Reportez les **p95/p99**, pas seulement les moyennes — une moyenne masque la traîne qui viole le SLO.

## Idées fausses courantes
| Idée reçue | Réalité | Pourquoi cela compte à l’examen |
| --- | --- | --- |
| « 95 % au global, on est bons » | Un segment peut échouer ; reportez par segment | Anti-pattern métrique agrégée |
| « Le modèle peut noter sa propre réponse » | Utilisez une session/un modèle séparé, calibré | Anti-pattern revue-en-même-session |
| « B semblait meilleur sur quelques sorties » | Testez la significativité avec assez d’échantillons | Distracteur jugement-à-l’œil |
| « Le LLM-juge est objectif » | Il nécessite une calibration contre l’humain ; il a des biais | Distracteur juge-non-calibré |
| « La latence moyenne respecte le SLO » | Surveillez les traînes p95/p99 | Distracteur latence-agrégée |
| « Les evals sont ponctuelles » | Régression CI + monitoring en ligne, en continu | Distracteur ponctuel |
| « Changer le golden set pour passer » | Gelez-le ; le changer invalide la comparaison | Distracteur surapprentissage |

## Étude de cas guidée
Une équipe veut faire passer le pipeline d’extraction de Sonnet 5 à Haiku 4.5 pour réduire les coûts. L’exactitude agrégée sur un test rapide semble « à peu près la même ». Comment décider correctement ?

<Steps>
1. **Golden set, par segment** — exécutez les deux modèles sur le jeu gelé, ventilez les résultats par type de document.
2. **Le bon grader** — exact/structural match sur les champs extraits (déterministe), pas un LLM-juge.
3. **Significativité** — là où c’est subjectif, un A/B par paires avec un test de significativité, pas un jugement à l’œil.
4. **Trouver la régression cachée** — Haiku correspond sur les PDF propres mais chute de 18 points sur les reçus scannés (l’agrégat l’a masqué).
5. **Décider par contrainte** — si les reçus scannés comptent, gardez Sonnet 5 pour ce segment (cascade), Haiku pour le reste.
6. **Le conditionner** — ajoutez les seuils par segment à la CI pour qu’un futur changement ne puisse pas régresser silencieusement.
7. **Surveiller coût + latence** — confirmez que l’économie est réelle sur le dashboard (p95, coût par 1k).
</Steps>

Alternatives rejetées : basculer sur l’exactitude agrégée (métrique agrégée), juger avec le même modèle dans la même session (revue en même session) et juger à l’œil « à peu près la même » sans significativité (distracteur jugement-à-l’œil).

## À retenir
- Le golden set d’abord : couvrez la distribution, incluez les cas difficiles, versionnez et gelez-le.
- Utilisez le **grader suffisant le moins cher** ; le LLM-as-judge doit être en session séparée et calibré.
- Prouvez les améliorations par un **A/B par paires + significativité**, pas quelques sorties.
- Conditionnez les déploiements par une **suite de régression CI** sur des seuils **par segment**.
- Surveillez coût et latence aux **p95/p99**, et la qualité en continu — les evals sont permanentes, pas ponctuelles.
