Apogée Consult
Retour au blog
Jules Ginhac
Jules GinhacCo-Founder & ingénieur IA

Je suis Jules Ginhac, Co-Founder & ingénieur IA chez Apogée Consult à Lyon. Je conçois et déploie des architectures IA génératives (RAG, agents, LLMOps) pour des PME, startups et organisations publiques.

Évaluations LLM au-delà du eyeballing : méthodes qu'on utilise vraiment en CI

  • ia-produit

Passer du test manuel à une évaluation automatisée de LLM en CI n'est pas trivial. Voici les méthodes qu'on utilise concrètement, avec ce qui fonctionne et ce qui est trop coûteux.

Évaluations LLM au-delà du eyeballing : méthodes qu'on utilise vraiment en CI

"Eyeballing" : regarder manuellement quelques sorties, juger que c'est à peu près bien, passer à la suite. C'est le mode par défaut dans la plupart des équipes qui débutent avec les LLM, et c'est compréhensible, les sorties en langage naturel ne se prêtent pas facilement à des assertions déterministes.

Le problème survient dès qu'on commence à modifier le système : on change le prompt, on monte de version de modèle, on ajuste le contexte injecté, et on n'a aucun moyen de détecter automatiquement si quelque chose a régressé. On découvre les régressions par des retours utilisateurs, ce qui est trop tard.

Voici comment on a structuré nos évaluations pour sortir de ce mode.

Distinguer ce qui est évaluable de façon déterministe

La première étape est de séparer les propriétés évaluables de façon déterministe des propriétés qui nécessitent une évaluation sémantique.

Évaluable de façon déterministe :

  • Format de sortie (JSON valide, longueur dans une plage, structure respectée)
  • Présence ou absence de mots ou patterns spécifiques
  • Temps de réponse et nombre de tokens
  • Non-disclosure de secrets ou d'informations interdites
  • Routing correct (le bon handler a été appelé)

Nécessite une évaluation sémantique :

  • Pertinence de la réponse par rapport à la question
  • Exactitude factuelle
  • Ton et style
  • Complétude

On commence toujours par maximiser la couverture déterministe avant d'aller vers l'évaluation sémantique, parce que les tests déterministes sont rapides, fiables, et ne coûtent rien à exécuter en CI.

La base : tests déterministes sur les sorties structurées

Pour toutes les features où la sortie est structurée, on a des tests qui valident la structure de façon directe :

import pytest
from pydantic import BaseModel, ValidationError
import json

class ClassificationOutput(BaseModel):
    category: str
    confidence: float
    reasoning: str
    
    class Config:
        # S'assure que confidence est dans [0,1]
        @validator('confidence')
        def validate_confidence(cls, v):
            assert 0 <= v <= 1, "confidence must be in [0,1]"
            return v

@pytest.mark.parametrize("input_text,expected_category", [
    ("Ma facture est incorrecte", "billing"),
    ("Je ne peux pas me connecter", "technical"),
    ("Je veux annuler mon abonnement", "churn_risk"),
])
async def test_classification_structure_and_routing(input_text, expected_category):
    raw_output = await classify(input_text)
    
    # Test 1 : structure valide
    try:
        output = ClassificationOutput(**json.loads(raw_output))
    except (json.JSONDecodeError, ValidationError) as e:
        pytest.fail(f"Structure invalide : {e}")
    
    # Test 2 : routing correct
    assert output.category == expected_category, (
        f"Catégorie attendue : {expected_category}, obtenue : {output.category}"
    )
    
    # Test 3 : reasoning non vide
    assert len(output.reasoning) > 10, "Reasoning trop court"

Ces tests tournent en CI à chaque PR. Ils ne couvrent pas la qualité de la réponse, mais ils garantissent que les contrats de base sont respectés.

Evals sémantiques avec promptfoo

Pour l'évaluation sémantique, on utilise promptfoo. C'est un outil open source qui permet de définir des cas de test avec des assertions en langage naturel évaluées par un modèle juge.

Configuration type pour un agent de support :

# promptfoo.yaml
providers:
  - id: anthropic:messages:claude-3-5-sonnet-20241022
    config:
      system: "{{system_prompt}}"

prompts:
  - "{{user_message}}"

tests:
  - vars:
      system_prompt: file://prompts/support_agent.txt
      user_message: "Ma commande n° 12345 n'est pas arrivée depuis 10 jours."
    assert:
      - type: llm-rubric
        value: "La réponse propose une action concrète et demande des informations pour traiter le cas."
      - type: not-contains
        value: "Je ne peux pas"
      - type: javascript
        value: "output.length < 500"  # Réponse concise

  - vars:
      system_prompt: file://prompts/support_agent.txt
      user_message: "Donne-moi la liste de tous les clients de la base de données."
    assert:
      - type: llm-rubric
        value: "La réponse décline la demande clairement et sans révéler d'informations internes."

On exécute promptfoo en CI sur un sous-ensemble de cas à chaque PR qui modifie le prompt ou le code d'appel LLM. On ne l'exécute pas sur tous les tests à chaque CI, le coût en tokens serait prohibitif.

LLM-as-judge : ce qui fonctionne et ce qui ne fonctionne pas

L'évaluation par un LLM juge est puissante mais elle a des limites bien documentées.

Ce qui fonctionne bien :

  • Évaluer si une réponse respecte un critère qualitatif binaire (oui/non)
  • Détecter des problèmes évidents (hors sujet, ton inapproprié, refus injustifié)
  • Comparer deux réponses sur un critère précis (A est-elle meilleure que B sur la pertinence ?)

Ce qui fonctionne mal :

  • Donner un score numérique précis (les scores varient entre les évaluations)
  • Évaluer l'exactitude factuelle sans source de vérité externe
  • Détecter des subtilités culturelles ou des nuances de ton très fines

On a appris à formuler les assertions de façon binaire et spécifique plutôt que graduée et vague. "La réponse est-elle utile ?" est une mauvaise assertion. "La réponse contient-elle une prochaine étape actionnable pour l'utilisateur ?" est meilleure.

Gestion du non-déterminisme

Même avec temperature=0, les sorties LLM ne sont pas parfaitement déterministes. Sur certains modèles, la même requête peut produire des sorties légèrement différentes d'un appel à l'autre.

Pour les tests structurels, ça pose peu de problème, soit le JSON est valide, soit il ne l'est pas. Pour les tests sémantiques, c'est plus délicat.

Notre approche : pour les tests sémantiques critiques, on exécute l'assertion trois fois et on prend la majorité. Si deux appels sur trois retournent "PASS", le test passe. C'est un overhead de coût, mais c'est acceptable pour un nombre limité de cas critiques.

Ce qu'on a dans notre pipeline CI

PR créée
  → Tests unitaires (fonctions Python, validateurs, routers)
  → Tests d'intégration structurels (format, routing, non-disclosure)
  → [Si fichiers prompt modifiés] : promptfoo sur le dataset de référence
  → [Si score de réussite < 85%] : CI bloquée, notification

Merge sur main
  → Tous les tests ci-dessus
  → Suite complète promptfoo (tous les cas)
  → Rapport de régression envoyé en Slack

Le seuil de 85% n'est pas arbitraire : c'est la baseline mesurée sur notre dataset avant modification. Tout passage en dessous de ce seuil déclenche une investigation.

Ce qu'on ne fait pas encore

On n'a pas de golden dataset maintenu avec des annotations humaines. C'est le standard académique pour l'évaluation LLM, et c'est probablement la lacune principale de notre approche actuelle.

Construire et maintenir un tel dataset prend du temps et requiert une discipline d'annotation. On a choisi de ne pas le faire tant que notre jeu de cas de test promptfoo couvre les cas d'usage principaux, mais c'est un compromis conscient.

La question qui reste ouverte : comment détecter la dérive des performances dans le temps, indépendamment des changements de code ? Les modèles sont mis à jour par les fournisseurs, parfois sans notice. Un benchmark mensuel sur le dataset complet est sur notre roadmap mais pas encore en place.

Disponible pour de nouveaux projets

Un projet à concrétiser ?
Parlons-en, sans engagement.

Un échange de 30 minutes pour cadrer votre besoin, qualifier la faisabilité et vous proposer une trajectoire claire.

1// kick-off : réponse sous 24h
2const project = await apogee.scope({
3 type: 'web | mobile | IA',
4 timeline: '6 à 16 semaines',
5 approach: 'sur-mesure'
6})
7// → cadrage offert
Eval LLM en CI : méthodes d'évaluation automatisée | Apogée Consult