Aller au contenu principal

CI/CD pour l'IA : évals automatisées

Intégrer les évals dans votre pipeline CI/CD

Un changement de prompt en production sans évaluation, c’est comme déployer du code sans tests. Cette leçon vous montre comment intégrer les évaluations automatisées dans votre pipeline de déploiement pour garantir que chaque modification maintient ou améliore la qualité.

Architecture d’un pipeline d’évals

Le workflow

Le pipeline d’évals s’intègre dans votre flux de déploiement :

  1. Le développeur modifie un prompt ou un paramètre
  2. Le commit déclenche le pipeline CI
  3. Les évals s’exécutent automatiquement sur le dataset de référence
  4. Si les scores passent les seuils, le déploiement continue
  5. Sinon, le déploiement est bloqué avec un rapport détaillé

Script d’évaluation CI

import json
import sys
import openai
from pathlib import Path

client = openai.OpenAI()

def charger_config_eval(chemin: str = "evals/config.json") -> dict:
    """Charge la configuration des évaluations."""
    with open(chemin) as f:
        return json.load(f)

def charger_dataset(chemin: str) -> list[dict]:
    """Charge un dataset JSONL."""
    cas = []
    with open(chemin) as f:
        for ligne in f:
            cas.append(json.loads(ligne))
    return cas

def executer_eval_ci(config: dict) -> dict:
    """Exécute les évaluations pour la CI."""
    resultats = {}

    for eval_config in config["evaluations"]:
        nom = eval_config["nom"]
        dataset = charger_dataset(eval_config["dataset"])
        seuil = eval_config["seuil_minimum"]
        modele = eval_config["modele"]
        prompt = Path(eval_config["prompt_file"]).read_text()

        scores = []
        for cas in dataset:
            response = client.responses.create(
                model=modele,
                instructions=prompt,
                input=cas["input"],
                temperature=0.0,
            )

            # Évaluation simple par inclusion de mots-clés
            if "mots_cles" in cas:
                reponse_lower = response.output_text.lower()
                trouves = sum(
                    1 for mot in cas["mots_cles"]
                    if mot.lower() in reponse_lower
                )
                score = trouves / len(cas["mots_cles"])
            else:
                score = 1.0  # Pas de critère spécifique

            scores.append(score)

        score_moyen = sum(scores) / len(scores)
        passe = score_moyen >= seuil

        resultats[nom] = {
            "score": score_moyen,
            "seuil": seuil,
            "passe": passe,
            "nb_cas": len(dataset),
        }

    return resultats

def rapport_ci(resultats: dict) -> tuple[str, bool]:
    """Génère un rapport pour la CI et indique si c'est OK."""
    lignes = ["# Rapport d'évaluation IA\n"]
    tout_passe = True

    for nom, r in resultats.items():
        statut = "PASS" if r["passe"] else "FAIL"
        if not r["passe"]:
            tout_passe = False
        lignes.append(
            f"- [{statut}] {nom}: {r[score]:.2%} "
            f"(seuil: {r[seuil]:.0%}, {r[nb_cas]} cas)"
        )

    rapport = "\n".join(lignes)
    return rapport, tout_passe

Configuration des évals

{
  "evaluations": [
    {
      "nom": "classification-tickets",
      "dataset": "evals/datasets/classification.jsonl",
      "prompt_file": "prompts/classification.txt",
      "modele": "gpt-5.3",
      "seuil_minimum": 0.90
    },
    {
      "nom": "generation-resume",
      "dataset": "evals/datasets/resumes.jsonl",
      "prompt_file": "prompts/resume.txt",
      "modele": "gpt-5.3",
      "seuil_minimum": 0.75
    }
  ]
}

Intégration GitHub Actions

# .github/workflows/eval-ia.yml
name: Évaluation IA

on:
  pull_request:
    paths:
      - "prompts/**"
      - "evals/**"
      - "src/ia/**"

jobs:
  evaluer:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"

      - run: pip install openai

      - name: Exécuter les évals
        env:
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
        run: python evals/run_ci.py

      - name: Publier le rapport
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: rapport-eval
          path: evals/rapport.md

Script d’entrée pour la CI

#!/usr/bin/env python3
"""Script d'évaluation pour la CI — evals/run_ci.py"""

import sys
from pathlib import Path

def main():
    config = charger_config_eval("evals/config.json")
    resultats = executer_eval_ci(config)
    rapport, tout_passe = rapport_ci(resultats)

    # Écrire le rapport
    Path("evals/rapport.md").write_text(rapport)
    print(rapport)

    # Exit code pour la CI
    if not tout_passe:
        print("\nDes évaluations ont échoué. Déploiement bloqué.")
        sys.exit(1)
    else:
        print("\nToutes les évaluations passent.")
        sys.exit(0)

if __name__ == "__main__":
    main()

Tests de régression pour les prompts

def test_regression_prompt(
    prompt_actuel: str,
    prompt_precedent: str,
    dataset: list[dict],
    modele: str,
    seuil_degradation: float = 0.05,
) -> dict:
    """Vérifie qu'un nouveau prompt ne dégrade pas les performances."""
    scores_actuel = []
    scores_precedent = []

    for cas in dataset:
        # Évaluer avec le prompt actuel
        r1 = client.responses.create(
            model=modele, instructions=prompt_actuel,
            input=cas["input"], temperature=0.0,
        )
        # Évaluer avec le prompt précédent
        r2 = client.responses.create(
            model=modele, instructions=prompt_precedent,
            input=cas["input"], temperature=0.0,
        )

        s1 = evaluer_reponse(r1.output_text, cas)
        s2 = evaluer_reponse(r2.output_text, cas)
        scores_actuel.append(s1)
        scores_precedent.append(s2)

    moy_actuel = sum(scores_actuel) / len(scores_actuel)
    moy_precedent = sum(scores_precedent) / len(scores_precedent)
    delta = moy_actuel - moy_precedent

    return {
        "score_actuel": moy_actuel,
        "score_precedent": moy_precedent,
        "delta": delta,
        "regression": delta < -seuil_degradation,
        "verdict": "OK" if delta >= -seuil_degradation else "REGRESSION",
    }

Points clés à retenir

  • Intégrez les évals dans votre pipeline CI/CD comme des tests unitaires
  • Définissez des seuils minimaux par type d’évaluation
  • Bloquez les déploiements quand les scores descendent sous les seuils
  • Testez les régressions en comparant nouveau prompt vs ancien prompt