Évaluation humaine : Méthodologie et outils
Complétez l'évaluation automatisée par l'expertise humaine. Protocoles d'annotation, accord inter-annotateurs et outils de labeling pour RAG.
Évaluation humaine : Méthodologie et outils
L'évaluation automatisée avec RAGAS et les métriques LLM-as-judge offre reproductibilité et rapidité. Mais elle manque la nuance du jugement humain. Ce guide présente les méthodologies d'évaluation humaine pour compléter votre pipeline de qualité RAG.
Pourquoi l'évaluation humaine reste essentielle
Les limites de l'automatisation
| Aspect | Évaluation auto | Évaluation humaine |
|---|---|---|
| Nuances culturelles | Difficile | Excellent |
| Ton et style | Approximatif | Précis |
| Cas ambigus | Échoue souvent | Jugement contextuel |
| Satisfaction réelle | Proxy indirect | Mesure directe |
| Découverte de bugs | Patterns connus | Nouveaux problèmes |
Quand privilégier l'humain ?
- Lancement produit : Validation avant mise en production
- Nouveaux domaines : Pas de ground truth automatique
- Cas sensibles : Médical, juridique, financier
- Debugging ciblé : Comprendre pourquoi une réponse échoue
- Calibrage des métriques : Vérifier que les scores auto correspondent au jugement réel
Protocoles d'évaluation
1. Évaluation par critères (Likert)
Le plus courant. Chaque réponse est notée sur plusieurs dimensions.
DEVELOPERpythonfrom dataclasses import dataclass from enum import Enum from typing import Optional class Rating(Enum): TERRIBLE = 1 POOR = 2 ACCEPTABLE = 3 GOOD = 4 EXCELLENT = 5 @dataclass class EvaluationCriteria: relevance: Rating # La réponse répond-elle à la question ? accuracy: Rating # Les informations sont-elles correctes ? completeness: Rating # Manque-t-il des informations importantes ? clarity: Rating # La réponse est-elle claire et bien structurée ? helpfulness: Rating # La réponse aide-t-elle l'utilisateur ? comment: Optional[str] # Commentaire libre @dataclass class AnnotationTask: task_id: str question: str rag_answer: str contexts: list[str] ground_truth: Optional[str] annotator_id: str evaluation: Optional[EvaluationCriteria] = None
Grille d'évaluation type :
| Score | Pertinence | Précision | Complétude |
|---|---|---|---|
| 5 | Répond parfaitement | 100% correct | Tout couvert |
| 4 | Répond bien | Quasi correct | Essentiel couvert |
| 3 | Répond partiellement | Quelques erreurs mineures | Lacunes mineures |
| 2 | Hors sujet partiel | Erreurs significatives | Lacunes importantes |
| 1 | Ne répond pas | Faux | Rien d'utile |
2. Comparaison par paires (A/B)
Plus fiable pour des différences subtiles. L'annotateur choisit la meilleure réponse entre deux versions.
DEVELOPERpython@dataclass class PairwiseTask: task_id: str question: str answer_a: str answer_b: str contexts: list[str] annotator_id: str preference: Optional[str] = None # "A", "B", ou "equal" confidence: Optional[int] = None # 1-5 reason: Optional[str] = None def calculate_win_rate(annotations: list[PairwiseTask]) -> dict: """Calcule le taux de victoire entre deux modèles""" wins_a = sum(1 for a in annotations if a.preference == "A") wins_b = sum(1 for a in annotations if a.preference == "B") ties = sum(1 for a in annotations if a.preference == "equal") total = len(annotations) return { "model_a_wins": wins_a / total, "model_b_wins": wins_b / total, "ties": ties / total }
3. Évaluation par erreurs (Error taxonomy)
Identifie les types d'erreurs spécifiques pour un debugging ciblé.
DEVELOPERpythonclass ErrorType(Enum): HALLUCINATION = "hallucination" FACTUAL_ERROR = "factual_error" INCOMPLETE = "incomplete" OFF_TOPIC = "off_topic" WRONG_CONTEXT = "wrong_context" OUTDATED = "outdated" FORMATTING = "formatting" TONE = "tone" @dataclass class ErrorAnnotation: task_id: str errors: list[ErrorType] error_details: dict[ErrorType, str] severity: int # 1-5 fixable: bool def analyze_error_distribution(annotations: list[ErrorAnnotation]) -> dict: """Analyse la distribution des erreurs""" error_counts = {} for annotation in annotations: for error in annotation.errors: error_counts[error.value] = error_counts.get(error.value, 0) + 1 total_errors = sum(error_counts.values()) return { error: count / total_errors for error, count in sorted(error_counts.items(), key=lambda x: x[1], reverse=True) }
Accord inter-annotateurs
Pourquoi le mesurer ?
Si deux annotateurs sont en désaccord sur 50% des samples, vos annotations ne sont pas fiables. L'accord inter-annotateurs (IAA) mesure la cohérence.
Métriques d'accord
DEVELOPERpythonimport numpy as np from sklearn.metrics import cohen_kappa_score def calculate_iaa(annotations_1: list[int], annotations_2: list[int]) -> dict: """Calcule plusieurs métriques d'accord inter-annotateurs""" # Pourcentage d'accord exact exact_agreement = sum( 1 for a, b in zip(annotations_1, annotations_2) if a == b ) / len(annotations_1) # Cohen's Kappa (corrige pour le hasard) kappa = cohen_kappa_score(annotations_1, annotations_2) # Accord à 1 point près close_agreement = sum( 1 for a, b in zip(annotations_1, annotations_2) if abs(a - b) <= 1 ) / len(annotations_1) # Corrélation de Pearson correlation = np.corrcoef(annotations_1, annotations_2)[0, 1] return { "exact_agreement": exact_agreement, "close_agreement": close_agreement, "cohens_kappa": kappa, "pearson_correlation": correlation }
Interprétation des scores
| Cohen's Kappa | Interprétation | Action |
|---|---|---|
| < 0.20 | Accord faible | Revoir les guidelines |
| 0.20-0.40 | Accord modeste | Clarifier les critères |
| 0.40-0.60 | Accord modéré | Acceptable pour démarrer |
| 0.60-0.80 | Accord substantiel | Bon niveau |
| > 0.80 | Accord presque parfait | Excellent |
Améliorer l'accord
DEVELOPERpythonclass AnnotationGuidelines: """Structure pour les guidelines d'annotation""" def __init__(self): self.examples = {} self.edge_cases = [] self.calibration_set = [] def add_example(self, score: int, question: str, answer: str, explanation: str): """Ajoute un exemple de référence pour chaque score""" if score not in self.examples: self.examples[score] = [] self.examples[score].append({ "question": question, "answer": answer, "explanation": explanation }) def run_calibration(self, annotators: list[str], samples: list[dict]) -> dict: """Session de calibrage : tous annotent les mêmes samples""" results = {} for annotator in annotators: results[annotator] = self._get_annotations(annotator, samples) # Identifier les désaccords disagreements = [] for i, sample in enumerate(samples): scores = [results[a][i] for a in annotators] if max(scores) - min(scores) > 1: disagreements.append({ "sample_idx": i, "sample": sample, "scores": dict(zip(annotators, scores)) }) return {"disagreements": disagreements}
Plateforme d'annotation
Interface web simple
DEVELOPERpythonfrom fastapi import FastAPI, HTTPException from pydantic import BaseModel import uuid app = FastAPI() class AnnotationSubmission(BaseModel): task_id: str annotator_id: str relevance: int accuracy: int completeness: int clarity: int comment: str = "" tasks_db = {} annotations_db = {} @app.get("/task/{annotator_id}") async def get_next_task(annotator_id: str): """Retourne la prochaine tâche à annoter""" for task_id, task in tasks_db.items(): if not any( a["annotator_id"] == annotator_id for a in annotations_db.get(task_id, []) ): return task raise HTTPException(404, "No tasks available") @app.post("/annotate") async def submit_annotation(submission: AnnotationSubmission): """Soumet une annotation""" if submission.task_id not in tasks_db: raise HTTPException(404, "Task not found") annotation = { "id": str(uuid.uuid4()), "task_id": submission.task_id, "annotator_id": submission.annotator_id, "scores": { "relevance": submission.relevance, "accuracy": submission.accuracy, "completeness": submission.completeness, "clarity": submission.clarity }, "comment": submission.comment } if submission.task_id not in annotations_db: annotations_db[submission.task_id] = [] annotations_db[submission.task_id].append(annotation) return {"status": "success", "annotation_id": annotation["id"]}
Outils existants
| Outil | Type | Prix | Forces |
|---|---|---|---|
| Label Studio | Open-source | Gratuit | Flexible, self-hosted |
| Argilla | Open-source | Gratuit | Spécialisé NLP/RAG |
| Prodigy | Commercial | 390 EUR | UX excellente, rapide |
| Scale AI | Service | Variable | Annotateurs inclus |
Échantillonnage intelligent
Stratégie d'échantillonnage
DEVELOPERpythonimport random from collections import defaultdict class SmartSampler: def __init__(self, all_samples: list[dict]): self.samples = all_samples def stratified_sample(self, n: int, strata_key: str) -> list[dict]: """Échantillonnage stratifié par catégorie""" strata = defaultdict(list) for sample in self.samples: strata[sample.get(strata_key, "unknown")].append(sample) samples_per_stratum = n // len(strata) selected = [] for stratum_samples in strata.values(): selected.extend(random.sample( stratum_samples, min(samples_per_stratum, len(stratum_samples)) )) return selected[:n] def uncertainty_sample(self, n: int, ragas_scores: dict) -> list[dict]: """Échantillonne les samples avec scores RAGAS incertains""" uncertain = [ (i, sample) for i, sample in enumerate(self.samples) if 0.4 < ragas_scores.get(i, {}).get("faithfulness", 0) < 0.7 ] return [sample for _, sample in uncertain[:n]]
Taille d'échantillon recommandée
| Objectif | Taille mini | Annotateurs | Temps estimé |
|---|---|---|---|
| Validation rapide | 50 | 1 | 2h |
| Calibrage modèle | 100 | 2 | 6h |
| Benchmark sérieux | 300 | 3 | 24h |
| Production critique | 500+ | 3+ | 40h+ |
Intégration avec l'évaluation automatique
Pipeline hybride
DEVELOPERpythonclass HybridEvaluationPipeline: def __init__(self, ragas_evaluator, human_platform): self.ragas = ragas_evaluator self.human = human_platform async def evaluate(self, samples: list[dict]) -> dict: # Étape 1 : Évaluation automatique auto_results = await self.ragas.evaluate(samples) # Étape 2 : Identifier les samples à valider humainement uncertain_indices = [ i for i, score in enumerate(auto_results["per_sample"]) if 0.4 < score["faithfulness"] < 0.7 ] # Étape 3 : Créer les tâches d'annotation human_tasks = [samples[i] for i in uncertain_indices] await self.human.create_tasks(human_tasks) # Étape 4 : Attendre les annotations human_results = await self.human.wait_for_completion() # Étape 5 : Combiner les résultats return self._merge_results(auto_results, human_results, uncertain_indices) def _merge_results(self, auto, human, human_indices): """Combine les scores auto et humains""" final_scores = auto["per_sample"].copy() for i, idx in enumerate(human_indices): human_score = human[i]["average_score"] auto_score = final_scores[idx]["faithfulness"] final_scores[idx]["final_score"] = 0.6 * human_score + 0.4 * auto_score final_scores[idx]["human_validated"] = True return final_scores
Checklist évaluation humaine
| Étape | Action | Fait |
|---|---|---|
| Guidelines | Rédiger avec exemples pour chaque score | [ ] |
| Calibrage | Session initiale avec tous les annotateurs | [ ] |
| Pilote | 20 samples pour vérifier l'accord | [ ] |
| Production | Lancer l'annotation complète | [ ] |
| Qualité | Calculer l'IAA régulièrement | [ ] |
| Feedback | Intégrer les retours dans les guidelines | [ ] |
Pour aller plus loin
- Framework RAGAS - Évaluation automatisée
- Métriques RAG - Vue d'ensemble
- Génération RAG - Améliorer les réponses
FAQ
Évaluation humaine simplifiée avec Ailog
Mettre en place un pipeline d'évaluation humaine demande infrastructure et coordination. Avec Ailog, bénéficiez d'outils intégrés :
- Interface d'annotation intuitive
- Dashboard de progression en temps réel
- Calcul IAA automatique
- Échantillonnage intelligent des cas critiques
- Rapports combinant scores auto et humains
Testez gratuitement et validez la qualité de votre RAG avec expertise humaine.
Tags
Articles connexes
RAGAS : Framework d'évaluation RAG open-source
Maîtrisez RAGAS pour évaluer automatiquement vos systèmes RAG. Installation, métriques, datasets synthétiques et intégration CI/CD.
Évaluer un système RAG : Métriques et méthodologies
Guide complet pour mesurer la performance de votre RAG : faithfulness, relevancy, recall, et frameworks d'évaluation automatisée.
Réduire la Latence RAG : De 2000ms à 200ms
RAG 10x Plus Rapide : Récupération Parallèle, Réponses en Streaming et Optimisations Architecturales pour une Latence Inférieure à 200ms.