Évaluation automatisée des prompts
Évaluation automatisée des prompts
L’évaluation automatisée est la pierre angulaire d’un pipeline de prompt engineering mature. Elle vous permet de détecter les régressions, de comparer les modèles et de valider les modifications avant le déploiement. Sans elle, chaque changement de prompt est un pari aveugle.
Types d’évaluation
Évaluation exacte (match)
Pour les tâches avec une réponse attendue unique :
def eval_exact_match(prediction: str, reference: str) -> bool:
"""Correspondance exacte après normalisation."""
return prediction.strip().lower() == reference.strip().lower()
def eval_set_match(prediction: set, reference: set) -> dict:
"""Correspondance sur des ensembles (entités, catégories)."""
tp = len(prediction & reference)
fp = len(prediction - reference)
fn = len(reference - prediction)
precision = tp / (tp + fp) if (tp + fp) > 0 else 0
recall = tp / (tp + fn) if (tp + fn) > 0 else 0
f1 = (2 * precision * recall / (precision + recall)
if (precision + recall) > 0 else 0)
return {"precision": precision, "recall": recall, "f1": f1}
Évaluation sémantique (embedding)
Pour les réponses textuelles où la formulation peut varier :
from openai import OpenAI
import numpy as np
client = OpenAI()
def eval_semantic_similarity(prediction: str,
reference: str) -> float:
"""Similarité cosinus entre les embeddings."""
response = client.embeddings.create(
model="text-embedding-3-large",
input=[prediction, reference]
)
emb_pred = np.array(response.data[0].embedding)
emb_ref = np.array(response.data[1].embedding)
similarity = float(
np.dot(emb_pred, emb_ref) /
(np.linalg.norm(emb_pred) * np.linalg.norm(emb_ref))
)
return similarity
Évaluation par LLM (LLM-as-judge)
Le modèle évalue la qualité d’une réponse :
def eval_llm_judge(question: str, prediction: str,
criteria: list[str]) -> dict:
"""Évaluation par un modèle juge."""
criteria_text = "\n".join(
f"- {c}: note de 1 à 5" for c in criteria
)
response = client.responses.create(
model="gpt-5.4", # Modèle plus capable comme juge
instructions="Tu es un évaluateur expert et impartial.",
input=f"""Évalue cette réponse selon les critères suivants :
Question posée : {question}
Réponse à évaluer : {prediction}
Critères :
{criteria_text}
Pour chaque critère, donne une note de 1 à 5 avec justification.""",
text={
"format": {
"type": "json_schema",
"name": "evaluation",
"schema": {
"type": "object",
"properties": {
"scores": {
"type": "array",
"items": {
"type": "object",
"properties": {
"critere": {"type": "string"},
"note": {"type": "integer"},
"justification": {"type": "string"}
},
"required": ["critere", "note",
"justification"],
"additionalProperties": False
}
},
"note_globale": {"type": "number"},
"verdict": {"type": "string"}
},
"required": ["scores", "note_globale", "verdict"],
"additionalProperties": False
},
"strict": True
}
},
temperature=0.1
)
return json.loads(response.output_text)
Pipeline d’évaluation complet
import json
from dataclasses import dataclass
from pathlib import Path
@dataclass
class TestCase:
"""Un cas de test pour l'évaluation."""
input_text: str
expected_output: str = None
metadata: dict = None
class EvalPipeline:
"""Pipeline d'évaluation automatisée."""
def __init__(self, test_suite_path: str):
"""Charge la suite de tests depuis un fichier JSON."""
data = json.loads(Path(test_suite_path).read_text())
self.test_cases = [TestCase(**tc) for tc in data]
self.results = []
def run(self, prompt_config: dict,
evaluators: list[callable]) -> dict:
"""Exécute tous les tests et évalue les résultats."""
self.results = []
for tc in self.test_cases:
# Générer la réponse
response = client.responses.create(
model=prompt_config["model"],
instructions=prompt_config["system_prompt"],
input=tc.input_text,
temperature=prompt_config.get("temperature", 0.1)
)
prediction = response.output_text
# Évaluer avec chaque évaluateur
scores = {}
for evaluator in evaluators:
score = evaluator(
question=tc.input_text,
prediction=prediction,
reference=tc.expected_output
)
scores[evaluator.__name__] = score
self.results.append({
"input": tc.input_text,
"expected": tc.expected_output,
"prediction": prediction,
"scores": scores,
"tokens": {
"input": response.usage.input_tokens,
"output": response.usage.output_tokens
}
})
return self.summary()
def summary(self) -> dict:
"""Résume les résultats de l'évaluation."""
if not self.results:
return {"error": "Aucun résultat"}
# Agréger les scores par évaluateur
all_scores = {}
for result in self.results:
for eval_name, score in result["scores"].items():
if eval_name not in all_scores:
all_scores[eval_name] = []
if isinstance(score, (int, float)):
all_scores[eval_name].append(score)
elif isinstance(score, dict) and "f1" in score:
all_scores[eval_name].append(score["f1"])
summary = {
"total_tests": len(self.results),
"scores_moyens": {
name: sum(scores) / len(scores)
for name, scores in all_scores.items()
if scores
},
"cout_total_tokens": sum(
r["tokens"]["input"] + r["tokens"]["output"]
for r in self.results
)
}
return summary
Suite de tests : format
[
{
"input_text": "Mon colis n'est pas arrivé",
"expected_output": "reclamation",
"metadata": {"categorie": "livraison"}
},
{
"input_text": "Comment activer le mode sombre ?",
"expected_output": "question",
"metadata": {"categorie": "fonctionnalite"}
},
{
"input_text": "L'app plante au démarrage sur iOS 18",
"expected_output": "bug",
"metadata": {"categorie": "technique"}
}
]
Intégration CI/CD
Exécutez l’évaluation à chaque modification de prompt :
def ci_eval_check(prompt_config: dict,
test_suite: str,
min_score: float = 0.85) -> bool:
"""Gate CI/CD : vérifie que le prompt atteint le score minimum."""
pipeline = EvalPipeline(test_suite)
def accuracy(question, prediction, reference):
return 1.0 if prediction.strip().lower() == reference.strip().lower() else 0.0
results = pipeline.run(prompt_config, [accuracy])
avg_score = results["scores_moyens"].get("accuracy", 0)
print(f"Score moyen : {avg_score:.2%}")
print(f"Seuil minimum : {min_score:.2%}")
if avg_score < min_score:
print("ÉCHEC : score inférieur au seuil")
return False
print("SUCCÈS : prompt validé")
return True
Tableau de bord d’évaluation
Tracez l’évolution des scores dans le temps :
def rapport_comparatif(resultats: dict[str, dict]) -> str:
"""Génère un rapport comparatif entre versions de prompts."""
rapport = "# Rapport d'évaluation comparatif\n\n"
rapport += "| Version | Score moyen | Tokens moyens | Coût |\n"
rapport += "|---------|-------------|---------------|------|\n"
for version, data in resultats.items():
score = data.get("scores_moyens", {})
score_val = list(score.values())[0] if score else 0
tokens = data.get("cout_total_tokens", 0)
rapport += (f"| {version} | {score_val:.2%} | "
f"{tokens} | ${tokens * 5 / 1e6:.4f} |\n")
return rapport
Mise en pratique
- Créez une suite de 20 cas de test pour un de vos prompts
- Implémentez 3 types d’évaluateurs (exact match, sémantique, LLM-as-judge)
- Exécutez le pipeline sur 2 versions de votre prompt
- Comparez les résultats et identifiez la version gagnante
- Mettez en place un script CI qui bloque le déploiement si le score baisse
Points clés à retenir
- L’évaluation automatisée est indispensable pour itérer avec confiance
- Combinez évaluation exacte, sémantique et LLM-as-judge selon le cas
- Maintenez une suite de tests qui évolue avec votre application
- Intégrez l’évaluation dans votre CI/CD avec un seuil minimum
- Utilisez un modèle plus capable comme juge pour évaluer un modèle moins cher
- Documentez les scores de chaque version pour tracer l’évolution