Aller au contenu principal

Guardrails : valider entrées et sorties

Mis à jour le 28 juillet 2026

Qu’est-ce qu’un guardrail ?

Un guardrail est une couche de validation qui s’interpose entre l’utilisateur et le modèle en entrée, ou entre le modèle et l’utilisateur en sortie. La distinction avec la modération vue au chapitre précédent est nette et il faut la tenir : la modération répond à la question « ce contenu est-il offensant ? », question universelle dont la réponse ne dépend pas de votre métier. Les guardrails répondent à des questions que vous seul pouvez formuler — le message est-il dans le périmètre attendu, la réponse contient-elle des informations interdites, l’action demandée est-elle autorisée pour ce rôle ? Un assistant de banque et un assistant de librairie partagent la même modération et n’ont aucun guardrail en commun.

En 2026, les guardrails sont devenus un composant standard des architectures d’agents IA, indispensables dès qu’un modèle a accès à des outils ou des données sensibles.

Une architecture en pipeline

Le choix structurant est de traiter chaque guardrail comme un objet indépendant, doté d’une seule responsabilité, que l’on chaîne ensuite dans un ordre choisi. L’intérêt apparaît à la maintenance : ajouter une règle métier ne demande pas de toucher au code existant, et une règle devenue trop bruyante se retire d’une ligne.

Un troisième verdict s’ajoute à autoriser et bloquer : modifier. Il change la nature de l’exercice. Beaucoup de situations réelles ne justifient pas un refus — une réponse contenant un chemin serveur interne reste utile une fois ce chemin masqué. Bloquer aurait fait perdre l’information ; modifier la conserve en supprimant ce qui pose problème.

from dataclasses import dataclass
from enum import Enum

class Decision(Enum):
    AUTORISER = "autoriser"
    BLOQUER = "bloquer"
    MODIFIER = "modifier"

@dataclass
class ResultatValidation:
    decision: Decision
    raison: str
    contenu_modifie: str | None = None

class Guardrail:
    """Classe de base pour un guardrail."""

    def valider(self, contenu: str, contexte: dict) -> ResultatValidation:
        raise NotImplementedError

class PipelineGuardrails:
    """Enchaîne plusieurs guardrails en séquence."""

    def __init__(self):
        self.guardrails_entree: list[Guardrail] = []
        self.guardrails_sortie: list[Guardrail] = []

    def ajouter_entree(self, guardrail: Guardrail):
        self.guardrails_entree.append(guardrail)

    def ajouter_sortie(self, guardrail: Guardrail):
        self.guardrails_sortie.append(guardrail)

    def valider_entree(self, message: str, contexte: dict) -> ResultatValidation:
        for g in self.guardrails_entree:
            resultat = g.valider(message, contexte)
            if resultat.decision == Decision.BLOQUER:
                return resultat
            if resultat.decision == Decision.MODIFIER:
                message = resultat.contenu_modifie
        return ResultatValidation(Decision.AUTORISER, "Toutes les validations passées")

    def valider_sortie(self, reponse: str, contexte: dict) -> ResultatValidation:
        for g in self.guardrails_sortie:
            resultat = g.valider(reponse, contexte)
            if resultat.decision == Decision.BLOQUER:
                return resultat
            if resultat.decision == Decision.MODIFIER:
                reponse = resultat.contenu_modifie
        return ResultatValidation(Decision.AUTORISER, "OK", reponse)

Trois guardrails typiques

Le guardrail de périmètre garde la conversation dans son domaine. Il sert autant la sécurité que la réputation : un assistant de service client entraîné à répondre sur les livraisons n’a rien à gagner à commenter l’actualité politique, et une capture d’écran de cette réponse-là circule vite. La liste des sujets interdits doit être discutée avec le métier, jamais rédigée par la seule équipe technique.

class GuardrailPerimetre(Guardrail):
    """Vérifie que le message reste dans le périmètre autorisé."""

    def __init__(self, sujets_autorises: list[str], sujets_interdits: list[str]):
        self.sujets_autorises = sujets_autorises
        self.sujets_interdits = sujets_interdits

    def valider(self, contenu: str, contexte: dict) -> ResultatValidation:
        contenu_lower = contenu.lower()

        # Vérifier les sujets interdits
        for sujet in self.sujets_interdits:
            if sujet.lower() in contenu_lower:
                return ResultatValidation(
                    Decision.BLOQUER,
                    f"Sujet hors périmètre détecté : {sujet}"
                )

        return ResultatValidation(Decision.AUTORISER, "Dans le périmètre")

Le guardrail de format paraît trivial et rend pourtant le plus de services au quotidien. Il coûte quelques microsecondes, ne dépend d’aucun appel réseau, et coupe court aux messages démesurés par lesquels passent les injections volumétriques comme les tentatives d’épuisement de budget. Placez-le en premier dans la chaîne : rien ne sert de payer une modération API sur un message de 200 000 caractères que vous refuserez de toute façon.

class GuardrailFormat(Guardrail):
    """Valide le format et la longueur des messages."""

    def __init__(self, max_tokens: int = 4000, max_lignes: int = 100):
        self.max_tokens = max_tokens
        self.max_lignes = max_lignes

    def valider(self, contenu: str, contexte: dict) -> ResultatValidation:
        # Estimation grossière des tokens (1 token ~ 4 caractères en français)
        tokens_estimes = len(contenu) // 4
        if tokens_estimes > self.max_tokens:
            return ResultatValidation(
                Decision.BLOQUER,
                f"Message trop long : ~{tokens_estimes} tokens (max {self.max_tokens})"
            )

        nb_lignes = contenu.count("\n") + 1
        if nb_lignes > self.max_lignes:
            return ResultatValidation(
                Decision.BLOQUER,
                f"Trop de lignes : {nb_lignes} (max {self.max_lignes})"
            )

        return ResultatValidation(Decision.AUTORISER, "Format valide")

Le troisième surveille la sortie et illustre l’usage du verdict « modifier ». Les quatre familles qu’il recherche partagent une propriété commode : elles ont une forme reconnaissable indépendamment du sens. Une clé d’API, une adresse interne, une IP privée ou un chemin serveur se repèrent à leur structure, sans qu’il faille comprendre la phrase qui les contient.

import re

class GuardrailAntiExfiltration(Guardrail):
    """Empêche la fuite de données sensibles dans les sorties."""

    def __init__(self):
        self.patterns_sensibles = {
            "cle_api": r"(sk|pk|api[_-]?key)[_-][a-zA-Z0-9]{16,}",
            "email_interne": r"[a-zA-Z0-9.]+@(entreprise|internal|corp)\.\w+",
            "ip_privee": r"\b(10\.\d{1,3}\.\d{1,3}\.\d{1,3}|192\.168\.\d{1,3}\.\d{1,3})\b",
            "chemin_serveur": r"(/data/|/etc/|/home/|C:\\Users\\)",
        }

    def valider(self, contenu: str, contexte: dict) -> ResultatValidation:
        for type_donnee, pattern in self.patterns_sensibles.items():
            if re.search(pattern, contenu):
                # Masquer au lieu de bloquer
                contenu_nettoye = re.sub(pattern, "[MASQUÉ]", contenu)
                return ResultatValidation(
                    Decision.MODIFIER,
                    f"Données sensibles masquées : {type_donnee}",
                    contenu_nettoye,
                )
        return ResultatValidation(Decision.AUTORISER, "Aucune fuite détectée")

Assembler le tout

Le montage complet montre où les pièces s’emboîtent. L’asymétrie saute aux yeux : deux guardrails en entrée, un seul en sortie. Elle est fréquente et elle est saine — on filtre beaucoup avant l’appel au modèle, parce que c’est là que le coût est évitable, et on garde en sortie le minimum nécessaire pour éviter d’abîmer des réponses correctes. Le message renvoyé lors d’un blocage en sortie relève de la même prudence : volontairement vague, il ne dit pas ce qui a été détecté, information qui servirait surtout à celui qui cherche à cartographier vos défenses.

from openai import OpenAI

def creer_assistant_securise():
    """Crée un assistant avec un pipeline complet de guardrails."""
    client = OpenAI()
    pipeline = PipelineGuardrails()

    # Guardrails d'entrée
    pipeline.ajouter_entree(GuardrailFormat(max_tokens=2000))
    pipeline.ajouter_entree(GuardrailPerimetre(
        sujets_autorises=["produits", "commandes", "livraison", "retours"],
        sujets_interdits=["politique", "religion", "concurrent"],
    ))

    # Guardrails de sortie
    pipeline.ajouter_sortie(GuardrailAntiExfiltration())

    def traiter(message: str) -> str:
        contexte = {"role_utilisateur": "client"}

        # Validation entrée
        check_entree = pipeline.valider_entree(message, contexte)
        if check_entree.decision == Decision.BLOQUER:
            return f"Je ne peux pas traiter cette demande : {check_entree.raison}"

        message_valide = check_entree.contenu_modifie or message

        # Appel au modèle
        response = client.chat.completions.create(
            model="gpt-5.6-sol",
            messages=[
                {"role": "system", "content": "Assistant service client Acme Corp."},
                {"role": "user", "content": message_valide},
            ],
        )
        reponse = response.choices[0].message.content

        # Validation sortie
        check_sortie = pipeline.valider_sortie(reponse, contexte)
        if check_sortie.decision == Decision.BLOQUER:
            return "Je ne suis pas en mesure de répondre à cette question."

        return check_sortie.contenu_modifie or reponse

    return traiter

Quand la règle ne s’écrit pas en regex

Certaines règles métier résistent à toute formalisation par motifs. « La réponse ne doit pas constituer un conseil d’investissement », « le message ne doit pas contenir de données de santé » : aucune expression régulière ne capture cela. Un second modèle, rapide et peu coûteux, peut alors jouer le rôle de juge, avec une réponse contrainte au format JSON pour rester exploitable par le code appelant. Le compromis est explicite — vous gagnez en finesse ce que vous perdez en latence et en déterminisme, ce qui réserve cette approche aux règles qui le justifient vraiment.

def guardrail_llm(message: str, regle: str) -> ResultatValidation:
    """Utilise un modèle rapide pour valider la conformité."""
    client = OpenAI()
    response = client.chat.completions.create(
        model="gpt-5.6-terra",
        messages=[
            {"role": "system", "content": f"""Vous êtes un validateur de conformité.
Règle à vérifier : {regle}
Répondez UNIQUEMENT par JSON : {{"conforme": true/false, "raison": "..."}}"""},
            {"role": "user", "content": f"Message à valider :\n{message}"},
        ],
    )
    import json
    resultat = json.loads(response.choices[0].message.content)
    if resultat["conforme"]:
        return ResultatValidation(Decision.AUTORISER, resultat["raison"])
    return ResultatValidation(Decision.BLOQUER, resultat["raison"])

Points clés à retenir

  • Les guardrails valident la conformité métier, pas seulement le contenu offensant
  • L’architecture en pipeline permet de combiner plusieurs couches de validation
  • Chaque guardrail a une responsabilité unique : périmètre, format, exfiltration, etc.
  • Les guardrails de sortie sont aussi importants que ceux d’entrée
  • Pour les cas complexes, un LLM rapide peut servir de validateur