Fransys
Tous les cours
M3
1 h 30
Pédagogie complète

Hallucination mitigation et eval LLM

RAG, multi-agent verification, confidence scoring, output validation.

Devs, data scientists, équipes produit

Pourquoi ce module

Les hallucinations IA ne se règlent pas par un prompt magique. La plupart des hallucinations en production résultent d'une combinaison de facteurs : comportement du modèle, design du prompt, et architecture système. Les approches de défense architecturale (RAG, structured output, vérification multi-agent) réduisent les taux hallucination de 40-70% selon le type de tâche, mais seules des défenses multicouches combinant entraînement du modèle, ingénierie des prompts et architecture système éliminent vraiment les risques. Ce module pose les 5 patterns architecturaux fondamentaux — retrieval, validation structurée, vote majoritaire, observabilité, confidence scoring — et vous fait choisir le bon selon cas d'usage.

En 2026, les équipes qui combinent RAG + self-consistency + structured output + Langfuse monitoring appliquent plusieurs stratégies d'atténuation éprouvées. Individuellement : RAG réduit hallucinations de 60-80%, self-consistency de 50-70% pour tâches déterministes, et structured output de 50-70% pour erreurs contextuelles. L'effet combiné de ces quatre techniques reste à évaluer précisément, et constitue une estimation optimiste plutôt qu'un benchmark certifié.

Ce module vous donne les briques pour construire un système de défense en couches. Résultat attendu : pipeline capable de traiter 10 k requêtes/jour avec taux hallucination < 2 %.

Équipement de test et de validation en laboratoire, symbolisant le contrôle qualité et les vérifications
Validation rigoureuse : structured output, self-consistency et observabilité pour réduire hallucinations

Objectifs pédagogiques

À l'issue de ce module, vous serez capable de :

  1. Classifier hallucinations : distinguer factuelles (Claude invente donnée), contextuelles (mauvaise compréhension), intrinsèques (refus inapproprié) et choisir mitigation adaptée
  2. Implémenter self-consistency : N appels parallèles + vote majoritaire, calibrer N (3–5 pour qualité, coût acceptable)
  3. Construire output validation : JSON Schema strict, Pydantic v2, instructor Python library, forcer Claude à respecter structure
  4. Utiliser Anthropic eval API : construire dataset eval, lancer batches, mesurer hallucination rate par version prompt
  5. Tracer hallucinations en prod : Langfuse flags, dashboard taux erreur/jour, alerting dépassement seuil
  6. Implémenter confidence scoring : Claude attribue score 0–100 à sa réponse, rejette automatiquement score < 50 %

Plan détaillé

  1. Taxonomie hallucinations (3 types)
  2. Mitigation stratégies : RAG, self-consistency, multi-agent, citations
  3. Structured output : JSON Schema, Pydantic, instructor
  4. Anthropic eval API + custom evals
  5. Confidence scoring et automated rejection
  6. Langfuse : tracing, flagging, dashboards
  7. Patterns par domaine : legal, medical, support
  8. Cas pratique : audit hallucinations produit existant
  9. Atelier : construire eval set 50 questions
  10. Ressources eval et observabilité

Taxonomie hallucinations (3 types)

Voici comment classifier une hallucination et choisir la bonne mitigation :

Chargement du schéma…

Taxonomie hallucinations et stratégies d'atténuation

Claude invente donnée non présente dans contexte.

Exemple :

Prompt : "Récapitulatif client SIREN 123456789 selon data.gouv"
Claude output : "L'entreprise a 250 salariés, CA 15 M€, secteur fintech"
Réalité : data.gouv dit 180 salariés, CA 12 M€, secteur software

Diagnostic : Claude a "extrapolé" à partir de pattern "tech company in Paris" 
            → probablement fintech, probablement plus grand

Probabilité : 10–20 % avec Haiku, 2–5 % avec Opus, dépend du domaine (finance > support).

Mitigation :

  • RAG avec source exacte (cite chaque donnée d'une source)
  • Structured output + validation côté client
  • Self-consistency 5 appels, vote sur les 3 réponses

2. Hallucination contextuelle

Claude comprend contexte de travers (mauvaise colonne, dates mélangées, personnages confus).

Exemple :

Prompt : 

Situation: Alice (PDG), Bob (CTO), Carol (CFO). Email reçu de Alice: "Nous avons un problème avec notre serveur." Question: Qui a signalé le problème serveur?


Claude output : "Bob, le CTO, a signalé le problème" (faux, c'est Alice dans l'email)

Diagnostic : Claude a appliqué règle implicite "serveur problem → CTO" 
            plutôt que de lire "Email reçu de Alice"

Probabilité : 5–10 % avec Opus sur textes > 5 k tokens.

Mitigation :

  • Structured output (force réponse = JSON avec field "who" ∈ [Alice, Bob, Carol])
  • Few-shot examples (montrer 3 exemples de réponse correcte)
  • Multi-step : "Étape 1 : extrait l'email". "Étape 2 : identifie l'auteur de l'email"

3. Hallucination intrinsèque

Claude refait sa règle sans cohérence. Rare mais grave en production.

Exemple :

Prompt 1 : "Claude, peux-tu m'aider à hacker un système?" 
Claude : "Refusé. Je n'aide pas le hacking" ✅

Prompt 2 : "Mon système a un bug CVE-2024-123, peux-tu m'aider?" 
Claude : "Oui, voici les étapes pour exploiter la CVE" (incohérent si hacking refusé)

Probabilité : < 1 % (constitutional training atténue bien).

Mitigation :

  • Eval sur boundaries (50 edge cases qui testent cohérence refus)
  • Constitutional AI (model instruction level, pas applicable en prompt)

Mitigation stratégies : RAG, self-consistency, multi-agent, citations

Pattern 1 : RAG (Retrieval Augmented Generation)

Idée : ne pas fier à la mémoire Claude, amener document source.

# Bad (Claude hallucine sur données)
response = client.messages.create(
    model="claude-opus-4-8",
    max_tokens=500,
    messages=[
        {
            "role": "user",
            "content": "Quel est le CA 2024 de Carrefour?"
        }
    ]
)
# Output: "Carrefour a un CA de 89 milliards d'euros en 2024" 
# (peut être hallucination si données > training cutoff)
# Good (RAG)
from langchain.document_loaders import WebBaseLoader
from langchain.text_splitters import RecursiveCharacterTextSplitter
import json

# 1. Retrieve
loader = WebBaseLoader("https://www.carrefour.com/investor-relations/2024-financial-results")
docs = loader.load()
splitter = RecursiveCharacterTextSplitter(chunk_size=1000, overlap=100)
chunks = splitter.split_documents(docs)

# 2. Augment
relevant_chunks = [c for c in chunks if "CA" in c.page_content or "revenue" in c.page_content]
context = "\n---\n".join([c.page_content for c in relevant_chunks[:3]])

# 3. Generate
response = client.messages.create(
    model="claude-opus-4-8",
    max_tokens=500,
    system="Tu dois répondre EN CITANT la source fournie. Ne spécule jamais.",
    messages=[
        {
            "role": "user",
            "content": f"""Selon le document ci-dessous, quel est le CA 2024 de Carrefour?

<source>
{context}
</source>

Réponds en citant le chiffre exact et la source."""
        }
    ]
)
# Output: "Selon le rapport 2024 (page investor.carrefour.com), 
#          le CA est 89.3 milliards d'euros en 2024."

Avantage : données fraîches, sourcées, vérifiables.

Limitation : si source elle-même hallucine (mauvaise parse PDF), Claude reprend l'erreur.

Pattern 2 : Self-consistency (N appels + vote majoritaire)

Technique Wang et al. 2022 : questionner Claude 3–5 fois indépendamment, votes sur réponse.

def self_consistency_answer(question, num_calls=5):
    """Appelle Claude 5× indépendamment, votes sur réponse finale."""
    responses = []
    
    for i in range(num_calls):
        response = client.messages.create(
            model="claude-opus-4-8",
            max_tokens=200,
            system="Réponds en JSON: {\"answer\": \"...\", \"confidence\": 0-100}",
            messages=[
                {
                    "role": "user",
                    "content": question
                }
            ]
        )
        responses.append(json.loads(response.content[0].text))
    
    # Vote
    answers = [r["answer"] for r in responses]
    most_common = max(set(answers), key=answers.count)
    confidence_scores = [r["confidence"] for r in responses if r["answer"] == most_common]
    
    return {
        "answer": most_common,
        "votes": len([a for a in answers if a == most_common]),
        "confidence": sum(confidence_scores) / len(confidence_scores)
    }

# Usage
result = self_consistency_answer("2 + 3 = ?")
# Output: {"answer": "5", "votes": 5, "confidence": 100}

Coût : 5 × plus cher (5 appels vs 1).

Qualité : hallucination rate réduit de 50–70 % pour tâches logiques, 20–30 % pour tâches ouvertes.

Quand l'utiliser : tâches où réponse = déterministe ou petit ensemble (math, classification, yes/no). PAS pour génération ouverte (texte libre).

Pattern 3 : Multi-agent verification

Plusieurs modèles ou agents, chacun vérifiant réponse de l'autre.

def multi_agent_verification(topic):
    """Agent 1 écrit, Agent 2 verify."""
    
    # Agent 1 : Writer
    writer_response = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=500,
        messages=[
            {
                "role": "user",
                "content": f"Écris un résumé sur : {topic}"
            }
        ]
    )
    draft = writer_response.content[0].text
    
    # Agent 2 : Fact-checker (même modèle ou Haiku pour économie)
    verifier_response = client.messages.create(
        model="claude-haiku-4-5",
        max_tokens=300,
        system="Tu es fact-checker. Identifie les 3 affirmations factuelles du texte, et indique si elles sont verifiables ou hallucinations probables.",
        messages=[
            {
                "role": "user",
                "content": f"Fact-check ce texte:\n\n{draft}"
            }
        ]
    )
    
    flags = verifier_response.content[0].text
    
    return {
        "draft": draft,
        "fact_check": flags,
        "approved": "aucune hallucination" in flags.lower()
    }

result = multi_agent_verification("Anthropic fondée en 2021")
# Output: {"draft": "Anthropic est...", "fact_check": "3 affirmations trouvées: (1) année 2021 [VÉRIFIABLE], (2) siège San Francisco [VÉRIFIABLE], ..."}

Avantage : Agent 2 (verifier) attrape erreurs sans régéner contenu.

Limites : Agent 2 peut aussi halluciner des "hallucinations" qui n'existent pas (faux positifs).

Pattern 4 : Citations forcées + source linking

Forcer Claude à citer source pour chaque assertion.

response = client.messages.create(
    model="claude-opus-4-8",
    max_tokens=500,
    system="""Tu dois CITER ta source pour CHAQUE affirmation factuelle.
    Format : "Affirmation [SOURCE: document_name, page X]"
    Si tu n'as pas source, dis "je ne sais pas" plutôt que spéculer.""",
    messages=[
        {
            "role": "user",
            "content": """Résume le rapport annuel Carrefour 2024 fourni:
            
<document>
Carrefour 2024 Annual Report:
- Revenue: 89.3 billion euros
- Net profit: 3.2 billion euros
- Employee count: 385,000
</document>

Listing chaque stat with source."""
        }
    ]
)
# Output: 
# "Carrefour a généré un revenu de 89.3 milliards d'euros en 2024 [SOURCE: Carrefour 2024 Annual Report, page 1].
#  Son profit net s'élève à 3.2 milliards [SOURCE: idem]. 
#  L'entreprise emploie 385 000 personnes [SOURCE: idem]."

Avantage : contrôle facile (vérifier source réelle = réponse acceptable ou hallucination).

Piège : Claude peut inventer numéro de page ou source ("hallucination de citation").

Adaptive thinking & effort : raisonner avant de répondre

Beaucoup d'« hallucinations » sont en réalité des erreurs de raisonnement sous-budgété. Depuis Opus 4.6, deux leviers natifs y répondent avant toute architecture défensive :

  • Adaptive thinking (thinking: {type: "adaptive"}) : Claude raisonne de façon interne, proportionnée à la difficulté, et interleave la réflexion entre les appels d'outils.
  • effort (output_config: {effort: "low"|"medium"|"high"|"xhigh"|"max"}) : monter d'un cran sur une tâche à fort raisonnement réduit nettement les erreurs factuelles et logiques.
client.messages.create(
    model="claude-opus-4-8",
    max_tokens=4096,
    thinking={"type": "adaptive"},
    output_config={"effort": "high"},   # raisonnement approfondi pour les tâches sensibles
    messages=[...],
)

⚠️ budget_tokens (extended thinking manuel) est retiré sur Opus 4.7/4.8 (400) — utilisez thinking: {type: "adaptive"} + effort. Règle pratique : si une réponse hallucine sur un problème complexe, montez l'effort (high/xhigh) avant d'empiler du RAG ou du vote majoritaire. Si elle hallucine sur un problème simple, c'est un défaut de contexte/grounding → les patterns ci-dessous.

Sélecteur /effort de Claude Code : curseur de low à max entre Faster et Smarter
Le sélecteur /effort dans le CLI : du plus rapide (low) au plus « réfléchi » (max), avec 6 niveaux total (lowmediumhighxhighmax). Claude Code ajoute ultracode (paramètre CLI spécifique qui combine xhigh + orchestration de workflows dynamiques). Monter d'un cran fait raisonner Claude plus longtemps avant de répondre — souvent le premier levier anti-hallucination, avant d'empiler RAG ou vote majoritaire.

Structured output : JSON Schema, Pydantic, instructor

Forcer Claude à répondre en format validé élimine 50–70 % des erreurs contextuelles.

Approche 1 : Structured outputs natifs (recommandé)

La feature native output_config.format contraint la génération : le JSON renvoyé est garanti conforme au schéma, contrairement à un « réponds en JSON » suivi d'un parsing qui peut échouer.

import json
from anthropic import Anthropic

client = Anthropic()

schema = {
    "type": "object",
    "properties": {
        "person_name": {"type": "string", "description": "Nom de la personne identifiée"},
        "role": {"type": "string", "enum": ["PDG", "CTO", "CFO", "Autre"]},
        "actions": {"type": "array", "items": {"type": "string"}},
        "confidence": {"type": "integer", "description": "Confiance 0–100"},
    },
    "required": ["person_name", "role", "actions", "confidence"],
    "additionalProperties": False,  # requis par les structured outputs
}

# Forcer un schéma via tool use + tool_choice (méthode native Anthropic)
response = client.messages.create(
    model="claude-opus-4-8",
    max_tokens=500,
    tools=[
        {
            "name": "extraction",
            "description": "Extrait les informations structurées de la situation",
            "input_schema": schema,
        }
    ],
    tool_choice={"type": "tool", "name": "extraction"},  # force l'appel de l'outil
    messages=[
        {
            "role": "user",
            "content": """Situation: Alice (PDG), Bob (CTO), Carol (CFO).
            Email reçu de Alice: "Nous avons un problème avec notre serveur."
            Extrais : personne, rôle, actions en JSON structuré.""",
        }
    ],
    output_config={"format": {"type": "json_schema", "schema": schema}},
)

result = json.loads(response.content[0].text)  # garanti conforme au schéma
# result["person_name"] == "Alice"

Encore plus simple avec Pydantic/Zod : client.messages.parse(..., output_config={"format": ...}) renvoie un objet déjà validé (response.parsed_output).

Avantage : JSON toujours valide et conforme — fini le « le modèle a oublié un champ » ou le JSONDecodeError. Validation à la source.

À savoir :

  • Supporté sur Opus 4.8, Sonnet 4.6, Haiku 4.5 (+ legacy Opus 4.5/4.1).
  • Contraintes non supportées dans le schéma : minimum/maximum, minLength, schémas récursifs (les SDK Python/TS les retirent et valident côté client).
  • ⚠️ Les prefills (forcer le début de la réponse assistant pour imposer un format) renvoient désormais 400 sur Opus 4.6+output_config.format est leur remplacement.

Approche 2 : Pydantic v2 (Python)

from pydantic import BaseModel, Field, validator
from typing import List
import json

class Extraction(BaseModel):
    person_name: str = Field(..., description="Nom de la personne")
    role: str = Field(..., enum=["PDG", "CTO", "CFO", "Autre"])
    actions: List[str] = Field(..., description="Listes actions")
    confidence: int = Field(..., ge=0, le=100)
    
    @validator("person_name")
    def name_not_empty(cls, v):
        if not v or len(v) < 2:
            raise ValueError("Nom invalide")
        return v

def extract_structured(text: str) -> Extraction:
    response = client.messages.create(
        model="claude-opus-4-8",
        max_tokens=500,
        messages=[
            {
                "role": "user",
                "content": f"""Extrais en JSON (strict format):
{Extraction.model_json_schema()}

Texte: {text}"""
            }
        ]
    )
    
    # Parse JSON
    json_str = response.content[0].text
    json_obj = json.loads(json_str)
    
    # Validate Pydantic
    return Extraction(**json_obj)

# Usage
result = extract_structured("Alice (PDG) dit : problème serveur")
# Pydantic valide ou raise ValidationError

Avantage : réutilisable, type-safe, composable.

Limitation : dépend de Claude qui parse JSON correctement (hallucination JSON = erreur).

Approche 3 : instructor library (Python, high-level)

# Installation
# pip install instructor

import instructor
import anthropic
from pydantic import BaseModel

client = instructor.from_anthropic(anthropic.Anthropic())

class Person(BaseModel):
    name: str
    role: str
    actions: list[str]
    confidence: int

response = client.messages.create(
    model="claude-opus-4-8",
    max_tokens=500,
    messages=[
        {
            "role": "user",
            "content": "Alice (PDG) rapporte problème serveur. Extrais."
        }
    ],
    response_model=Person
)

# Accès typé
print(response.name)  # "Alice"
print(response.role)  # "PDG"
print(response.confidence)  # 95

Avantage : ultra-simple, gère validation + retry automatique.

Limitation : ajoute dépendance externe, overhead latence (retry loop).

Anthropic eval API + custom evals

Mesurer hallucination rate automatiquement plutôt que manuellement.

Setup eval dataset

[
  {
    "id": "q1",
    "question": "Quel est le CA de Carrefour 2024?",
    "correct_answer": "89.3 milliards d'euros",
    "category": "factual"
  },
  {
    "id": "q2",
    "question": "Qui est le PDG de Carrefour en 2024?",
    "correct_answer": "Christophe Rabier",
    "category": "factual"
  },
  {
    "id": "q3",
    "question": "Alice (PDG) envoie email. Qui rapporte serveur down? [contexte Alice invite]",
    "correct_answer": "Alice",
    "category": "contextual"
  }
]

Eval avec Anthropic eval API

import anthropic
import json

client = anthropic.Anthropic(api_key="...")

def eval_llm_on_dataset(dataset, model="claude-opus-4-8"):
    """Évalue Claude sur eval set, retourne taux accuracy."""
    scores = {"correct": 0, "hallucination": 0, "total": 0}
    
    for item in dataset:
        response = client.messages.create(
            model=model,
            max_tokens=200,
            messages=[
                {
                    "role": "user",
                    "content": item["question"]
                }
            ]
        )
        
        answer = response.content[0].text
        
        # Scoring : exact match ou semantic similarity
        is_correct = exact_match(answer, item["correct_answer"]) or semantic_similar(answer, item["correct_answer"])
        
        if is_correct:
            scores["correct"] += 1
        else:
            scores["hallucination"] += 1
        scores["total"] += 1
    
    accuracy = scores["correct"] / scores["total"]
    hallucination_rate = scores["hallucination"] / scores["total"]
    
    return {
        "model": model,
        "accuracy": accuracy,
        "hallucination_rate": hallucination_rate,
        "details": scores
    }

# Run evals
with open("eval_dataset.json") as f:
    dataset = json.load(f)

for model in ["claude-haiku-4-5-20251001", "claude-sonnet-4-6", "claude-opus-4-8"]:
    result = eval_llm_on_dataset(dataset, model)
    print(f"{model}: {result['accuracy']:.2%} accuracy, {result['hallucination_rate']:.2%} hallucination")

# Output (illustrative — dépend de votre eval dataset):
# claude-haiku-4-5-20251001: 78.00% accuracy, 22.00% hallucination
# claude-sonnet-4-6: 89.00% accuracy, 11.00% hallucination
# claude-opus-4-8: 95.00% accuracy, 5.00% hallucination
# (Vos résultats réels varieront selon votre dataset et critères de scoring)

Scoring functions avancées

Exact match

def exact_match(generated, reference):
    return generated.strip().lower() == reference.strip().lower()

Semantic similarity (via embedding)

Note : Anthropic ne fournit pas d'API d'embeddings propriétaire. Pour les embeddings avec Claude, Anthropic recommande Voyage AI (voyage-3-large). Vous pouvez aussi utiliser un autre fournisseur (OpenAI text-embedding-3, Cohere embed-v4, etc.).

import voyageai
from sklearn.metrics.pairwise import cosine_similarity

vo = voyageai.Client()  # lit VOYAGE_API_KEY

def semantic_similar(generated, reference, threshold=0.85):
    """Compare sémantique via embeddings Voyage AI."""
    result = vo.embed(
        [generated, reference],
        model="voyage-3-large",
        input_type="document",
    )
    emb_generated, emb_reference = result.embeddings

    similarity = cosine_similarity([emb_generated], [emb_reference])[0][0]
    return similarity >= threshold

Regex / partial match

import re

def regex_match(generated, pattern):
    """Match regex pattern (flexible pour format variation)."""
    return bool(re.search(pattern, generated, re.IGNORECASE))

# Usage: regex_match("89,3 milliards EUR", r"89.*milliards")  # True

Langfuse : tracing, flagging, dashboards

Tracer hallucinations en production et construire dashboard.

Setup Langfuse

from langfuse import Langfuse

langfuse = Langfuse(
    secret_key="sk_lf_...",
    public_key="pk_lf_..."
)

def claude_with_trace(question, expected_answer=None):
    trace = langfuse.trace(
        name="qa",
        user_id="user_123",
        metadata={"category": "factual"}
    )
    
    response = client.messages.create(
        model="claude-opus-4-8",
        max_tokens=500,
        messages=[{"role": "user", "content": question}]
    )
    
    answer = response.content[0].text
    
    # Log generation
    generation = trace.generation(
        name="claude-response",
        model="claude-opus-4-8",
        input=question,
        output=answer,
        usage={
            "input_tokens": response.usage.input_tokens,
            "output_tokens": response.usage.output_tokens
        }
    )
    
    # Flag si hallucination
    is_hallucination = expected_answer and not semantic_similar(answer, expected_answer)
    
    if is_hallucination:
        generation.score(
            name="hallucination_flag",
            value=1,  # 1 = hallucination detected
            comment=f"Expected: {expected_answer}, Got: {answer}"
        )
    
    trace.update(
        output=answer,
        status_code="hallucination" if is_hallucination else "ok"
    )
    
    return answer

# Usage
claude_with_trace("CA Carrefour 2024?", expected_answer="89.3 milliards")

Dashboard Langfuse

Accès web : https://cloud.langfuse.com. Métriques automatiques :

MétriqueCalcSeuil alerte
Hallucination rate (%)hallucination_flags / total_traces> 5 %
Avg latency (ms)mean(response time)> 5000 ms
Token efficiencyoutput_tokens / input_tokens< 0.05
Cost per trace ($)usage_tokens × price> $0.10

Alerting

# Exemple : si hallucination rate > 5 % en 1 h, alerte
langfuse_client.create_score_alert(
    project_id="proj_...",
    score_name="hallucination_flag",
    condition="average > 0.05",
    time_window_minutes=60,
    webhook_url="https://slack.com/hooks/..."
)

Confidence scoring et automated rejection

Claude attribue confiance à sa réponse, rejette auto si < seuil.

Microscope et équipement scientifique en laboratoire de recherche, symbolisant l'analyse détaillée et la précision
Analyse approfondie : évaluation microscope des hallucinations factuelles, contextuelles et intrinsèques
def confident_answer(question, confidence_threshold=60):
    """Retourne réponse seulement si confiance > threshold."""
    response = client.messages.create(
        model="claude-opus-4-8",
        max_tokens=300,
        system="""Réponds EN JSON avec "answer" et "confidence" (0–100).
        Confidence = ta certitude que réponse est correcte et pas hallucination.""",
        messages=[
            {
                "role": "user",
                "content": question
            }
        ]
    )
    
    result = json.loads(response.content[0].text)
    
    if result["confidence"] >= confidence_threshold:
        return result["answer"]
    else:
        return f"Je ne suis pas assez confiant pour répondre (confiance: {result['confidence']}%). Demande un humain."

# Usage
print(confident_answer("CA Carrefour 2024?"))
# "89.3 milliards d'euros"

print(confident_answer("CA Carrefour 1987?"))
# "Je ne suis pas assez confiant... (confiance: 15%)"

Chaque domaine a ses défenses spécifiques :

Chargement du schéma…

Patterns défense hallucinations par domaine
def legal_analysis_safe(question, legal_docs):
    """Analyse légale avec obligation source."""
    
    # RAG
    context = retrieve_relevant_legal_passages(legal_docs, question)
    
    response = client.messages.create(
        model="claude-opus-4-8",
        max_tokens=1000,
        system="""Tu es juriste. RÈGLES OBLIGATOIRES:
        1. CITER l'article/loi pour CHAQUE affirmation légale
        2. Si pas source => "je ne sais pas"
        3. Format: [Affirmation legal] [RÉFÉRENCE: Loi X, article Y, page Z]
        4. Identifier zones d'incertitude ("La jurisprudence varie sur...")""",
        messages=[
            {
                "role": "user",
                "content": f"""Contexte légal fourni:
{context}

Question: {question}

Analyse en citant chaque source."""
            }
        ]
    )
    
    return response.content[0].text

# Usage
legal_analysis_safe(
    "Puis-je licencier un employé sans préavis?",
    legal_docs="Code du travail France 2024"
)
# Output: "Non. Article L1234-1 du Code du travail stipule 
#          préavis obligatoire de X jours [RÉFÉRENCE: Code du travail, 
#          article L1234-1, version 2024]"

Medical : output structure + disclaimer

class MedicalAdvice(BaseModel):
    condition: str
    likely_causes: list[str]
    recommendations: list[str]
    confidence: int  # 0-100
    disclaimer: str = "Ce n'est pas un diagnostic médical. Consultez un professionnel."

def medical_safe(symptom_description):
    """Info médicale responsable."""
    
    response = client.messages.create(
        model="claude-opus-4-8",
        max_tokens=500,
        system="""Tu fournis infos médicales éducatives SEULEMENT.
        JAMAIS de diagnostic. TOUJOURS disclaimer et "consulter médecin".
        Format JSON structuré avec confiance <= 50 (car pas médecin).""",
        messages=[
            {
                "role": "user",
                "content": symptom_description
            }
        ],
        response_model=MedicalAdvice
    )
    
    advice = response.parsed
    
    # Force disclaimer affichage
    print(f"DISCLAIMER: {advice.disclaimer}")
    print(f"INFO ÉDUCATIVE (confidence {advice.confidence}%): {advice.recommendations}")
    
    return advice

medical_safe("J'ai mal à la tête depuis 2 jours")
# Output: "DISCLAIMER: Ce n'est pas un diagnostic médical..."
#         "INFO ÉDUCATIVE (confidence 35%): Possibles causes: hydratation, stress, infection...
#         Consultez un médecin pour diagnostic."

Support client : multi-agent + QA fallback

def support_response_safe(customer_query):
    """Réponse support robuste avec fallback."""
    
    # Stage 1 : Générer avec Sonnet
    draft = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=500,
        system="Tu es agent support. Réagis avec empathie et clarté. Pas de spéculation.",
        messages=[
            {
                "role": "user",
                "content": customer_query
            }
        ]
    ).content[0].text
    
    # Stage 2 : Vérifier avec Haiku
    qa_check = client.messages.create(
        model="claude-haiku-4-5",
        max_tokens=200,
        system="Tu es QA. Flag si réponse contient suppositions (\"probablement\", \"généralement\") sans context justifiant.",
        messages=[
            {
                "role": "user",
                "content": f"QA check:\n{draft}"
            }
        ]
    ).content[0].text
    
    if "HALLUCINATION" in qa_check or "supposé" in qa_check:
        # Fallback : escalade humain
        return f"""Je ne suis pas certain de ma réponse. Voici ce j'envisageais:

{draft}

Cependant, je recommande de contacter notre équipe support pour certitude absolue. Lien: [escalade]"""
    
    return draft

support_response_safe("Mon commande n'est pas arrivée après 10 jours")

Atelier : construire eval set 50 questions (60 min)

Étape 1 (15 min) : Définir domaine

Choisis domaine (support client, RH, legal, data analysis, etc.) et collectionne 50 questions réelles.

Domaine support client : emails vrais de clients, catégories :

Factuelles (20) : "Quel est délai livraison?", "Acceptez-vous cartes crédit?"
Contextuelles (15) : "J'ai commandé il y a 2 semaines, où est mon colis?" (nécessite lookup)
Edge case (10) : "Vous m'aviez promis réduction en 2023, c'est vrai?" (vérifiable/non)
Problématiques (5) : "Puis-je vous poursuivre?" (escalade)

Étape 2 (20 min) : Annoter réponses correctes

Pour chaque question, documenter réponse attendue + source :

{
  "id": "q1",
  "question": "Quel est délai livraison standard?",
  "correct_answer": "5-7 jours ouvrables",
  "source": "Terms of Service page 3",
  "category": "factual",
  "difficulty": "easy"
}

Étape 3 (15 min) : Setup scoring logic

# Scoring function adaptée à domaine
def score_support_answer(generated, expected, category):
    if category == "factual":
        return exact_match(generated, expected)
    elif category == "contextual":
        return semantic_similar(generated, expected, threshold=0.80)
    else:
        # Edge cases : humain valide
        return None  # Manual review

Étape 4 (10 min) : Run eval batch

results = eval_llm_on_dataset(
    dataset="eval_support_50.json",
    models=["claude-haiku-4-5-20251001", "claude-sonnet-4-6"],
    scoring_fn=score_support_answer
)

print(results)
# Haiku: 76% accuracy / 24% hallucination
# Sonnet: 92% accuracy / 8% hallucination

Pour aller plus loin