Aller au contenu principal
Leçon 7 sur 12 · Modération et Sécurité IA avec Mistral

Scores et seuils de modération

Leçon 7 sur 12 Mis à jour le 3 novembre 2026 Développeur
Sur cette page
  1. Interpréter les scores de modération
  2. Définir vos seuils : la matrice de décision
  3. Système de classification multi-niveaux
  4. Exemple concret : analyser et décider
  5. Ajuster les seuils dynamiquement
  6. Les pièges à éviter
  7. Points clés à retenir

Corrigé le 3 novembre 2026. Les profils de seuils utilisaient des catégories qui n’existent pas (hate, violence, unpredictable) : ils reprennent maintenant les onze clés de mistral-moderation-2603, dangerous compris. La grille de lecture des scores est présentée pour ce qu’elle est, un repère de formation, car Mistral n’en publie pas. Le moniteur levait KeyError sur toute catégorie absente du profil et utilisait datetime.utcnow(), déprécié depuis Python 3.12. Sources : Moderation & Guardrailing · datetime.utcnow.

Interpréter les scores de modération

Chaque catégorie retourne un score entre 0.0 et 1.0. Mais que signifient ces chiffres concrètement ? Et comment définir les seuils de décision pour votre application ? Cette leçon vous donne les clés pour transformer des scores bruts en décisions automatisées, c’est-à-dire pour passer d’une mesure à une politique.

Premier point à intégrer : Mistral ne présente pas ces scores comme des probabilités calibrées. Un score de 0.6 ne se lit pas « six chances sur dix que ce texte soit dangereux », mais comme un signal fort du classifieur. La documentation précise deux choses utiles. Les booléens categories de la réponse appliquent des seuils que Mistral a choisis pour la meilleure performance sur son jeu de test interne ; vous pouvez les utiliser, ou partir des scores bruts et fixer vos propres seuils. Et sur ce jeu de test, la précision des catégories se situe entre 0,8 et 1,0, le rappel entre 0,7 et 1,0 : des faux positifs comme des contenus manqués existent, d’où l’intérêt des seuils réglés à la main.

La grille ci-dessous n’est pas publiée par Mistral : c’est un repère de départ, à confronter à vos propres mesures.

PlageLecture
0.0 - 0.1Contenu clairement sûr, aucun signal détecté
0.1 - 0.3Signal faible, possiblement anodin mais ambigu
0.3 - 0.5Signal modéré, contenu potentiellement problématique
0.5 - 0.7Signal fort, contenu très probablement problématique
0.7 - 1.0Signal très fort, contenu manifestement dangereux

Définir vos seuils : la matrice de décision

Le choix du seuil est un compromis entre sécurité, bloquer le maximum, et utilisabilité, minimiser les faux positifs. Aucun réglage ne gagne sur les deux tableaux, et prétendre le contraire mène à des allers-retours permanents. La méthode qui fonctionne consiste à figer trois profils explicites et à rattacher chaque application à l’un d’eux.

Le profil strict vise une application grand public où des mineurs peuvent se trouver ; le profil équilibré correspond à une application professionnelle B2B avec des utilisateurs identifiés ; le profil permissif est réservé à un outil interne manipulé par des experts qui savent ce qu’ils demandent.

# Trois profils de seuils
PROFILS = {
    "strict": {
        # Application grand public, enfants possibles
        "sexual": 0.1,
        "hate_and_discrimination": 0.1,
        "violence_and_threats": 0.1,
        "dangerous": 0.1,
        "criminal": 0.1,
        "selfharm": 0.1,
        "health": 0.2,
        "financial": 0.2,
        "law": 0.2,
        "pii": 0.1,
        "jailbreaking": 0.1,
    },
    "equilibre": {
        # Application professionnelle B2B
        "sexual": 0.2,
        "hate_and_discrimination": 0.15,
        "violence_and_threats": 0.25,
        "dangerous": 0.15,
        "criminal": 0.15,
        "selfharm": 0.15,
        "health": 0.4,
        "financial": 0.4,
        "law": 0.4,
        "pii": 0.2,
        "jailbreaking": 0.15,
    },
    "permissif": {
        # Outil interne pour experts
        "sexual": 0.3,
        "hate_and_discrimination": 0.2,
        "violence_and_threats": 0.4,
        "dangerous": 0.2,
        "criminal": 0.2,
        "selfharm": 0.2,
        "health": 0.7,
        "financial": 0.7,
        "law": 0.7,
        "pii": 0.3,
        "jailbreaking": 0.2,
    },
}

Comparez les colonnes health d’un profil à l’autre : 0.2, puis 0.4, puis 0.7. Un même échange sur une posologie sera bloqué dans le premier cas, accepté dans le dernier. C’est le contexte d’usage qui tranche, pas la nature du texte.

Système de classification multi-niveaux

Bloquer ou accepter est une vision appauvrie de la réalité. Entre le contenu manifestement sûr et le contenu manifestement fautif s’étend une zone grise où la bonne décision est d’afficher tout en signalant. La fonction ci-dessous introduit ce troisième état : au-dessus du seuil on bloque, au-dessus de 60 % du seuil on avertit et on journalise, en dessous on laisse passer.

from enum import Enum

class NiveauRisque(Enum):
    SAFE = "safe"          # Vert : contenu sûr
    WARNING = "warning"    # Orange : contenu à surveiller
    BLOCKED = "blocked"    # Rouge : contenu bloqué

def classifier_contenu(scores: dict, seuils: dict) -> dict:
    """Classifie le contenu en 3 niveaux de risque."""
    violations_bloquantes = []
    avertissements = []

    for categorie, seuil in seuils.items():
        score = scores.get(categorie, 0)

        if score > seuil:
            violations_bloquantes.append({
                "categorie": categorie,
                "score": round(score, 3)
            })
        elif score > seuil * 0.6:
            # Zone d'avertissement : 60% du seuil
            avertissements.append({
                "categorie": categorie,
                "score": round(score, 3)
            })

    if violations_bloquantes:
        return {
            "niveau": NiveauRisque.BLOCKED,
            "violations": violations_bloquantes,
            "action": "bloquer_et_logger"
        }
    elif avertissements:
        return {
            "niveau": NiveauRisque.WARNING,
            "avertissements": avertissements,
            "action": "afficher_et_logger"
        }
    else:
        return {
            "niveau": NiveauRisque.SAFE,
            "action": "afficher"
        }

L’intérêt de la zone orange est autant opérationnel que pédagogique : elle vous montre ce qui frôle vos seuils. Si une catégorie génère beaucoup d’avertissements sans jamais franchir la ligne, vous savez qu’un ajustement de quelques centièmes basculerait un volume important de trafic.

Exemple concret : analyser et décider

L’assemblage tient en une fonction : appel au classifieur, application du profil, décision. La comparaison qui suit vaut démonstration — une posologie de paracétamol est bloquée en profil strict au titre de health et acceptée en profil permissif, sans qu’une seule ligne du texte n’ait changé.

from mistralai.client import Mistral
import os

client = Mistral(api_key=os.environ["MISTRAL_API_KEY"])

def moderer_et_decider(texte: str, profil: str = "equilibre") -> dict:
    """Modère un texte et prend une décision selon le profil."""
    # Appel à l'API de modération
    response = client.classifiers.moderate(
        model="mistral-moderation-2603",
        inputs=[texte]
    )
    scores = response.results[0].category_scores or {}

    # Appliquer les seuils du profil
    seuils = PROFILS[profil]
    classification = classifier_contenu(scores, seuils)

    return {
        "texte": texte[:80],
        "profil": profil,
        "scores_bruts": {k: round(v, 3) for k, v in scores.items()},
        "decision": classification
    }

# Test avec différents profils
texte = "Prenez 2 comprimés de paracétamol toutes les 6 heures"

# Profil strict : probablement bloqué (health)
resultat_strict = moderer_et_decider(texte, "strict")

# Profil permissif : probablement accepté
resultat_permissif = moderer_et_decider(texte, "permissif")

Ajuster les seuils dynamiquement

En production, vos seuils doivent évoluer, et cette évolution se pilote avec des chiffres. Le moniteur ci-dessous écrit chaque décision dans un fichier JSONL et tient à jour, catégorie par catégorie, les compteurs de blocages, d’avertissements et de passages. Le rapport qui en découle vous donne un taux de blocage par catégorie — la seule donnée qui permette de dire si un seuil est trop bas.

import json
from datetime import datetime, timezone

class ModerationMonitor:
    """Collecte les métriques de modération pour ajuster les seuils."""

    def __init__(self, log_file: str = "moderation_logs.jsonl"):
        self.log_file = log_file
        self.stats = {cat: {"blocked": 0, "warned": 0, "passed": 0}
                     for cat in PROFILS["equilibre"].keys()}

    def log_decision(self, scores: dict, seuils: dict, decision: str):
        """Enregistre une décision de modération."""
        entry = {
            "timestamp": datetime.now(timezone.utc).isoformat(),
            "scores": scores,
            "seuils": seuils,
            "decision": decision
        }
        with open(self.log_file, "a") as f:
            f.write(json.dumps(entry) + "\n")

        # Mettre à jour les compteurs
        for cat, seuil in seuils.items():
            score = scores.get(cat, 0.0)
            self.stats.setdefault(cat, {"blocked": 0, "warned": 0, "passed": 0})
            if score > seuil:
                self.stats[cat]["blocked"] += 1
            elif score > seuil * 0.6:
                self.stats[cat]["warned"] += 1
            else:
                self.stats[cat]["passed"] += 1

    def rapport(self) -> dict:
        """Génère un rapport pour ajuster les seuils."""
        rapport = {}
        for cat, stats in self.stats.items():
            total = sum(stats.values())
            if total > 0:
                rapport[cat] = {
                    "taux_blocage": round(stats["blocked"] / total, 3),
                    "taux_warning": round(stats["warned"] / total, 3),
                    "total": total
                }
        return rapport

Les pièges à éviter

Le premier piège est le seuil unique appliqué à toutes les catégories. Chaque catégorie a sa propre distribution de scores : un seuil de 0.3 pour hate_and_discrimination ne signifie pas la même chose que 0.3 pour health, où les contenus légitimes vivent naturellement plus haut dans l’échelle. Ajustez chaque catégorie individuellement, même si cela demande onze décisions au lieu d’une.

Le deuxième est d’ignorer les faux positifs. Un taux de blocage élevé dégrade la confiance des utilisateurs beaucoup plus vite qu’un incident isolé, parce qu’il touche des gens de bonne foi. Suivez le taux de blocage par catégorie et relevez les seuils quand les données le justifient.

Le troisième, plus insidieux, consiste à ne pas journaliser les décisions. Sans logs, vous ne pouvez pas ajuster vos seuils de manière informée et vous en revenez aux discussions d’opinion. Enregistrez systématiquement les scores, les seuils appliqués et les décisions prises — c’est ce corpus qui rendra la révision suivante rapide et incontestable.

Points clés à retenir

  • Les scores vont de 0.0 à 1.0 ; ce ne sont pas des probabilités calibrées, et Mistral invite à fixer vos propres seuils
  • Définissez des profils de seuils adaptés à votre contexte (strict, équilibré, permissif)
  • Implémentez un système à 3 niveaux : safe, warning, blocked
  • Loggez toutes les décisions pour ajuster vos seuils en continu
  • Chaque catégorie nécessite son propre seuil — pas de valeur unique