Scores et seuils de modération
Sur cette page
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 demistral-moderation-2603,dangerouscompris. 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 levaitKeyErrorsur toute catégorie absente du profil et utilisaitdatetime.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.
| Plage | Lecture |
|---|---|
| 0.0 - 0.1 | Contenu clairement sûr, aucun signal détecté |
| 0.1 - 0.3 | Signal faible, possiblement anodin mais ambigu |
| 0.3 - 0.5 | Signal modéré, contenu potentiellement problématique |
| 0.5 - 0.7 | Signal fort, contenu très probablement problématique |
| 0.7 - 1.0 | Signal 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