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 :
- Le développeur modifie un prompt ou un paramètre
- Le commit déclenche le pipeline CI
- Les évals s’exécutent automatiquement sur le dataset de référence
- Si les scores passent les seuils, le déploiement continue
- 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