Sécurité et déploiement responsable#

Le chapitre précédent a traité de l’alignement des LLM, c’est-à-dire des techniques visant à faire en sorte que le comportement du modèle corresponde aux intentions de ses concepteurs et aux attentes de ses utilisateurs. Mais l’alignement, aussi soigné soit-il, ne suffit pas à garantir un déploiement sûr et responsable. Un modèle parfaitement aligné au sens du RLHF peut encore halluciner des faits inexistants, générer du contenu préjudiciable dans des cas limités, ou être exploité par des acteurs malveillants. Le passage du laboratoire à la production exige une couche supplémentaire de sécurité — technique, organisationnelle et réglementaire.

Ce chapitre aborde les six facettes de cette sécurité. Nous commencerons par les hallucinations, le problème le plus visible et le plus fréquemment rencontré en pratique : un LLM qui affirme avec assurance des informations fausses. Nous examinerons ensuite le filtrage de contenu, le watermarking du texte généré, le cadre réglementaire européen (EU AI Act), la gouvernance par la documentation, et enfin les bonnes pratiques de déploiement sécurisé en production.

L’approche est délibérément pratique : chaque section combine des définitions rigoureuses, des exemples de code exécutable en Python pur (sans appels à des API externes), et des diagrammes récapitulatifs. Le lecteur trouvera ici les outils conceptuels et techniques nécessaires pour déployer un système fondé sur un LLM de manière responsable.

Hallucinations : détection et mitigation#

Les LLM génèrent du texte en prédisant le token le plus probable conditionnellement aux tokens précédents. Ce mécanisme, purement statistique, n’intègre aucune notion de vérité factuelle. Le modèle ne « sait » pas si ce qu’il affirme est vrai ; il produit la continuation la plus vraisemblable compte tenu de ses données d’entrainement et du contexte. Cette propriété fondamentale est la source des hallucinations.

Définition 102 (Hallucination)

Une hallucination (hallucination) d’un LLM est une génération qui contient des informations fausses, non fondées ou incohérentes avec le contexte fourni, tout en étant présentée avec un niveau de confiance linguistique comparable à celui d’une génération correcte. Formellement, soit \(y = (y_1, \ldots, y_T)\) la séquence générée conditionnellement à un prompt \(x\). On dit que \(y\) contient une hallucination si \(y\) affirme une proposition \(p\) telle que :

  • \(p\) est factuellement fausse (hallucination factuelle), ou

  • \(p\) contredit le contexte \(x\) fourni au modèle (hallucination de fidélité), ou

  • \(p\) est non étayée par les sources accessibles au modèle (hallucination de fondement).

Les hallucinations sont inhérentes à la modélisation auto-régressive : le modèle optimise \(P(y_t \mid y_{<t}, x)\) et non \(P(y_t \text{ est vrai} \mid y_{<t}, x)\).

Remarque 99 (Taxonomie des hallucinations)

On distingue trois types principaux d’hallucinations :

  1. Hallucination factuelle (factual hallucination) : le modèle affirme un fait objectivement faux. Exemple : « La tour Eiffel a été construite en 1923 par Gustave Courbet. » Ce type est le plus étudié et le plus facile à détecter automatiquement.

  2. Hallucination de fidélité (faithfulness hallucination) : dans une tâche de résumé ou de QA avec contexte, le modèle génère une réponse qui contredit ou déforme le document source.

  3. Hallucination contextuelle (contextual hallucination) : le modèle extrapole au-delà de ce que le contexte permet de déduire, par exemple en inventant des chiffres pour un trimestre non couvert par un rapport financier fourni.

Les hallucinations factuelles relèvent des connaissances paramétriques du modèle, tandis que les hallucinations de fidélité et contextuelles relèvent du traitement du contexte en entrée.

Hide code cell source

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import seaborn as sns
import hashlib

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)
np.random.seed(42)

Hide code cell source

fig, ax = plt.subplots(figsize=(14, 7))
ax.set_xlim(0, 10)
ax.set_ylim(0, 8)
ax.axis("off")

ax.text(5, 7.2, "Hallucinations des LLM", fontsize=14, fontweight="bold",
        ha="center", va="center",
        bbox=dict(boxstyle="round,pad=0.5", facecolor="#4C72B0", edgecolor="white",
                  alpha=0.9), color="white")

branches = [
    (1.8, 5.0, "Factuelle", "#DD8452", "Fait objectivement faux\nEx : date, nom, chiffre"),
    (5.0, 5.0, "Fidélité", "#55A868", "Contredit le document\nsource fourni en contexte"),
    (8.2, 5.0, "Contextuelle", "#C44E52", "Extrapole au-delà du\ncontexte disponible"),
]
for bx, by, label, color, desc in branches:
    ax.annotate("", xy=(bx, by + 0.6), xytext=(5, 6.7),
                arrowprops=dict(arrowstyle="->, head_width=0.3", color=color, lw=2))
    ax.text(bx, by, label, fontsize=12, fontweight="bold", ha="center", va="center",
            bbox=dict(boxstyle="round,pad=0.4", facecolor=color, edgecolor="white",
                      alpha=0.85), color="white")
    ax.text(bx, by - 1.2, desc, fontsize=9, ha="center", va="center",
            bbox=dict(boxstyle="round,pad=0.3", facecolor="#f0f0f0", edgecolor=color,
                      alpha=0.7))

ax.text(5, 1.5, "Stratégies de détection", fontsize=12, fontweight="bold",
        ha="center", va="center",
        bbox=dict(boxstyle="round,pad=0.4", facecolor="#8B6DAF", edgecolor="white",
                  alpha=0.9), color="white")
for sx, sy, slabel in [(1.5, 0.3, "Self-consistency"), (3.7, 0.3, "Citation\nverification"),
                        (6.3, 0.3, "Factual\ngrounding"), (8.5, 0.3, "NLI-based\ndétection")]:
    ax.annotate("", xy=(sx, sy + 0.5), xytext=(5, 1.0),
                arrowprops=dict(arrowstyle="->, head_width=0.2", color="#8B6DAF",
                                lw=1.5, alpha=0.6))
    ax.text(sx, sy, slabel, fontsize=8, ha="center", va="center",
            bbox=dict(boxstyle="round,pad=0.25", facecolor="#e8e0f0", edgecolor="#8B6DAF",
                      alpha=0.8))

ax.set_title("Taxonomie des hallucinations et stratégies de détection", fontsize=13, pad=15)
plt.show()
_images/87154b9c8471198b9eab55b7fe4846af19e71e9fe595afab3dade5de91afc0f1.png

Définition 103 (Groundedness (fondement))

Le fondement (groundedness) d’une génération \(y\) par rapport à un ensemble de documents sources \(\mathcal{D} = \{d_1, \ldots, d_K\}\) mesure dans quelle mesure chaque affirmation de \(y\) est étayée par au moins un document de \(\mathcal{D}\). On décompose \(y\) en propositions atomiques \(\{p_1, \ldots, p_M\}\) et on définit :

\[\text{Groundedness}(y, \mathcal{D}) = \frac{1}{M} \sum_{i=1}^{M} \mathbb{1}\left[\exists\, d \in \mathcal{D} : d \vDash p_i\right]\]

\(d \vDash p_i\) signifie que le document \(d\) étayé (entails) la proposition \(p_i\). Un score de 1 signifie que toutes les affirmations sont vérifiables ; un score faible signale des hallucinations de fidélité.

Remarque 100 (Stratégies de détection des hallucinations)

Plusieurs stratégies complémentaires permettent de détecter les hallucinations :

  1. Self-consistency (Wang et al., 2023) : générer \(N\) réponses indépendantes au même prompt et mesurer la concordance. Un fait réel, encodé de manière redondante dans les paramètres, sera reproduit de manière cohérente, tandis qu’un fait inventé variera d’une génération à l’autre.

  2. Vérification de citations : demander au modèle de citer ses sources, puis vérifier automatiquement que les citations existent et supportent les affirmations.

  3. Factual grounding via NLI : utiliser un modèle d’inférence en langage naturel pour vérifier si chaque proposition est impliquée (entailed) par les documents sources. Un verdict de contradiction signale une hallucination.

  4. Chain-of-Verification (Dhuliawala et al., 2023) : le modèle génère une réponse, produit des questions de vérification sur ses propres affirmations, y répond indépendamment, puis révise sa réponse initiale.

Aucune de ces strategies n’est parfaite ; une défense en profondeur combinant plusieurs approches est recommandée.

Exemple 71 (Détection d’hallucinations par self-consistency)

La méthode de self-consistency génère plusieurs réponses et mesure leur concordance :

def detect_hallucination_self_consistency(responses, threshold=0.5):
    """Detecte les hallucinations par self-consistency."""
    n = len(responses)
    scores = []
    for i in range(n):
        for j in range(i + 1, n):
            words_i = set(responses[i].lower().split())
            words_j = set(responses[j].lower().split())
            if len(words_i | words_j) > 0:
                overlap = len(words_i & words_j) / len(words_i | words_j)
                scores.append(overlap)
    concordance = np.mean(scores) if scores else 0.0
    return {"concordance": concordance,
            "hallucination_probable": concordance < threshold}

Hide code cell source

n_facts = 50
real_concordance = np.random.beta(8, 2, n_facts)
halluc_concordance = np.random.beta(2, 5, n_facts)

fig, axes = plt.subplots(1, 2, figsize=(13, 5))

axes[0].hist(real_concordance, bins=20, alpha=0.7, color="#55A868",
             label="Faits reels", edgecolor="white")
axes[0].hist(halluc_concordance, bins=20, alpha=0.7, color="#C44E52",
             label="Hallucinations", edgecolor="white")
axes[0].axvline(x=0.5, color="black", linestyle="--", linewidth=1.5,
                label="Seuil de détection")
axes[0].set_xlabel("Score de concordance (self-consistency)")
axes[0].set_ylabel("Nombre de propositions")
axes[0].set_title("Distribution des scores de concordance")
axes[0].legend(fontsize=9)

from sklearn.metrics import roc_curve, auc
y_true = np.concatenate([np.zeros(n_facts), np.ones(n_facts)])
y_scores = np.concatenate([real_concordance, halluc_concordance])
fpr, tpr, _ = roc_curve(y_true, 1 - y_scores)
roc_auc = auc(fpr, tpr)

axes[1].plot(fpr, tpr, color="#4C72B0", linewidth=2.5,
             label=f"Self-consistency (AUC = {roc_auc:.2f})")
axes[1].plot([0, 1], [0, 1], color="gray", linestyle="--", alpha=0.5)
axes[1].set_xlabel("Taux de faux positifs")
axes[1].set_ylabel("Taux de vrais positifs")
axes[1].set_title("Courbe ROC : détection d'hallucinations")
axes[1].legend(fontsize=10)

fig.suptitle("Détection d'hallucinations par self-consistency (données simulées)",
             fontsize=13, y=1.02)
plt.show()
_images/4691f1c6e7025126fdcbf5cde757c1b67a3fc1854e307a89ff11360c370659b2.png

Les principales stratégies de mitigation des hallucinations sont : le RAG (fournir des documents sources dans le contexte, chapitre 10), la chain-of-verification (auto-vérification des affirmations), la temperature basse (réduire la variance des générations) et le fine-tuning sur la calibration (entrainer le modèle à exprimer son incertitude).

Filtrage de contenu#

Même un modèle bien aligné peut générer du contenu indésirable : discours de haine, instructions dangereuses, contenu explicite, désinformation. Le filtrage de contenu constitue une couche de défense indépendante de l’alignement, opérant en amont (filtrage des entrées) et en aval (filtrage des sorties).

Définition 104 (Filtre de contenu)

Un filtre de contenu (content filter) est une fonction \(f : \mathcal{X} \to \{0, 1\}^C\) qui associe à un texte \(x\) un vecteur binaire de \(C\) catégories de contenu (violence, haine, sexuel, illégal, auto-mutilation). En pratique, le filtre produit des scores continus \(s_c(x) \in [0, 1]\) et un seuil \(\tau_c\) détermine la décision :

\[f_c(x) = \mathbb{1}[s_c(x) > \tau_c]\]

Le choix des seuils \(\tau_c\) détermine le compromis entre précision (éviter de bloquer du contenu légitime) et rappel (bloquer tout contenu problématique).

Hide code cell source

fig, ax = plt.subplots(figsize=(14, 5))
ax.set_xlim(0, 14)
ax.set_ylim(0, 6)
ax.axis("off")

boxes = [
    (1.0, 3.0, "Entrée\nutilisateur", "#4C72B0"), (3.5, 3.0, "Filtre\nd'entrée", "#DD8452"),
    (6.0, 3.0, "LLM", "#8B6DAF"), (8.5, 3.0, "Filtre\nde sortie", "#DD8452"),
    (11.0, 3.0, "Réponse\nutilisateur", "#55A868"),
]
for bx, by, label, color in boxes:
    rect = mpatches.FancyBboxPatch((bx - 0.8, by - 0.6), 1.6, 1.2,
                                    boxstyle="round,pad=0.15", facecolor=color,
                                    edgecolor="white", alpha=0.9, linewidth=2)
    ax.add_patch(rect)
    ax.text(bx, by, label, fontsize=10, fontweight="bold", ha="center",
            va="center", color="white")

for x_start, x_end in [(1.8, 2.7), (4.3, 5.2), (6.8, 7.7), (9.3, 10.2)]:
    ax.annotate("", xy=(x_end, 3.0), xytext=(x_start, 3.0),
                arrowprops=dict(arrowstyle="->, head_width=0.3", color="#333333", lw=2))

for bx, label_top in [(3.5, "Rejet / message\nd'erreur"), (8.5, "Réponse de\nremplacement")]:
    ax.annotate("", xy=(bx, 5.0), xytext=(bx, 3.7),
                arrowprops=dict(arrowstyle="->, head_width=0.2", color="#C44E52", lw=1.5))
    ax.text(bx, 5.2, label_top, fontsize=8, ha="center", va="center",
            bbox=dict(boxstyle="round,pad=0.3", facecolor="#f8d7da", edgecolor="#C44E52"))

ax.set_title("Pipeline de filtrage de contenu multi-couches", fontsize=13, pad=15)
plt.show()
_images/b05177464ef04c9327dfedd9b44594dd6feec50555cd7f03021c18d9050a44a1.png

Les deux grandes familles d’approches sont les approches par classifieur (un Transformer fine-tune détecte les catégories problématiques — OpenAI Moderation API, LLaMA Guard) et les approches par règles (expressions régulières, listes de mots-clés). En pratique, les systèmes de production combinent les deux en cascade : un filtre par règles rapide effectue un premier tri, suivi d’un classifieur plus précis sur les cas ambigus.

Watermarking du texte généré#

A mesure que les LLM génèrent du texte indistinguable du texte humain, la question de la traçabilite devient cruciale. Le watermarking consiste à introduire un signal statistique imperceptible dans le texte généré, permettant de détecter à posteriori qu’un texte a été produit par un modèle spécifique.

Définition 105 (Watermarking de texte LLM)

Le watermarking (tatouage numérique) du texte généré par un LLM modifie le processus de génération pour introduire un signal statistique détectable mais imperceptible. L’approche de Kirchenbauer et al. (2023) fonctionne comme suit :

  1. Partition du vocabulaire : pour chaque token \(y_t\), le token précédent \(y_{t-1}\) sert de graine à un PRNG qui partitionne \(\mathcal{V}\) en une liste verte \(G_t\) (\(\gamma |\mathcal{V}|\) tokens, \(\gamma \approx 0{,}5\)) et une liste rouge \(R_t = \mathcal{V} \setminus G_t\).

  2. Biais de génération : un biais \(\delta > 0\) est ajouté aux logits des tokens verts :

\[\begin{split}\tilde{\ell}_t(v) = \begin{cases} \ell_t(v) + \delta & \text{si } v \in G_t \\ \ell_t(v) & \text{si } v \in R_t \end{cases}\end{split}\]

Le texte watermarké contient donc une proportion de tokens verts significativement supérieure a \(\gamma\).

Propriété 23 (Détection du watermark par z-test)

Soit \(T\) le nombre de tokens et \(|G|\) le nombre de tokens verts observés. Sous \(H_0\) (texte non watermarké), chaque token a une probabilité \(\gamma\) d’être vert. Le z-score :

\[z = \frac{|G| - \gamma T}{\sqrt{T \gamma (1 - \gamma)}}\]

mesure l’écart à l’attendu. Un \(z > 4\) (soit \(p < 3 \times 10^{-5}\)) indique avec forte confiance que le texte est watermarké. La puissance du test croit avec \(T\) : plus le texte est long, plus le watermark est facile à détecter.

Hide code cell source

# Implémentation du watermarking de Kirchenbauer et al. (2023)

def create_green_red_lists(prev_token_id, vocab_size, gamma=0.5, seed_prefix=42):
    """Partitionne le vocabulaire en listes verte et rouge."""
    seed_bytes = f"{seed_prefix}_{prev_token_id}".encode()
    hash_val = int(hashlib.sha256(seed_bytes).hexdigest(), 16)
    rng = np.random.RandomState(hash_val % (2**31))
    perm = rng.permutation(vocab_size)
    n_green = int(gamma * vocab_size)
    return set(perm[:n_green]), set(perm[n_green:])

def generate_watermarked(logits_seq, prev_tokens, vocab_size,
                         delta=2.0, gamma=0.5, temperature=1.0):
    """Génère des tokens avec watermark (biais delta vers les tokens verts)."""
    tokens, green_flags = [], []
    for t in range(len(logits_seq)):
        prev_id = prev_tokens[t] if t < len(prev_tokens) else tokens[-1]
        green_list, _ = create_green_red_lists(prev_id, vocab_size, gamma)
        biased = logits_seq[t].copy()
        for v in green_list:
            biased[v] += delta
        biased = biased / temperature
        biased -= biased.max()
        probs = np.exp(biased) / np.exp(biased).sum()
        token = np.random.choice(vocab_size, p=probs)
        tokens.append(token)
        green_flags.append(token in green_list)
    return tokens, green_flags

# Simulation
vocab_size, seq_len = 1000, 200
logits = np.random.randn(seq_len, vocab_size) * 2.0
prev_init = [np.random.randint(vocab_size)]

tokens_wm, flags_wm = generate_watermarked(logits, prev_init, vocab_size, delta=2.0)
tokens_no, flags_no = generate_watermarked(logits, prev_init, vocab_size, delta=0.0)

print(f"Tokens verts (watermarke)     : {sum(flags_wm)}/{seq_len} = {sum(flags_wm)/seq_len:.1%}")
print(f"Tokens verts (sans watermark) : {sum(flags_no)}/{seq_len} = {sum(flags_no)/seq_len:.1%}")
Tokens verts (watermarke)     : 183/200 = 91.5%
Tokens verts (sans watermark) : 109/200 = 54.5%

Hide code cell source

# Detection du watermark par z-test
from scipy import stats

def detect_watermark(tokens, prev_init, vocab_size, gamma=0.5):
    """Détecte la présence d'un watermark par z-test."""
    green_count, T = 0, len(tokens)
    for t in range(T):
        prev_id = prev_init[0] if t == 0 else tokens[t - 1]
        green_list, _ = create_green_red_lists(prev_id, vocab_size, gamma)
        if tokens[t] in green_list:
            green_count += 1
    z = (green_count - gamma * T) / np.sqrt(T * gamma * (1 - gamma))
    return {"green_proportion": green_count / T, "z_score": z,
            "p_value": 1 - stats.norm.cdf(z)}

for label, tok in [("Watermarké", tokens_wm), ("Non watermarké", tokens_no)]:
    r = detect_watermark(tok, prev_init, vocab_size)
    verdict = "WATERMARK DETECTE" if r["z_score"] > 4 else "Pas de watermark"
    print(f"{label:18s} | verts: {r['green_proportion']:.1%} | "
          f"z = {r['z_score']:6.2f} | p = {r['p_value']:.2e} | {verdict}")
Watermarké         | verts: 91.5% | z =  11.74 | p = 0.00e+00 | WATERMARK DETECTE
Non watermarké     | verts: 54.5% | z =   1.27 | p = 1.02e-01 | Pas de watermark

Exemple 72 (Démonstration du watermarking)

Le mécanisme de Kirchenbauer biaise la distribution de probabilité des tokens. Pour chaque position, le vocabulaire est divisé en tokens « verts » et « rouges » à partir du token précédent :

green_list, red_list = create_green_red_lists(
    prev_token_id=42, vocab_size=1000, gamma=0.5
)
# Le biais delta = 2.0 augmente la probabilité des tokens verts
# sans rendre la génération perceptiblement différente pour un
# lecteur humain.

La robustesse du watermark dépend de la longueur du texte : avec \(T = 200\) tokens et \(\delta = 2{,}0\), le z-score est typiquement supérieur a 10. Pour des textes très courts (\(T < 25\)), la puissance du test est insuffisante.

Hide code cell source

fig, axes = plt.subplots(1, 2, figsize=(13, 5))

# Tokens verts/rouges (50 premiers)
sample_idx = np.arange(50)
colors_wm = ["#55A868" if flags_wm[i] else "#C44E52" for i in sample_idx]
axes[0].bar(sample_idx, [1] * 50, color=colors_wm, edgecolor="white", linewidth=0.5)
axes[0].set_xlabel("Position du token")
axes[0].set_title("Texte watermarke (50 premiers tokens)")
axes[0].set_yticks([])
axes[0].legend(handles=[mpatches.Patch(color="#55A868", label="Vert"),
                         mpatches.Patch(color="#C44E52", label="Rouge")], fontsize=9)

# Distribution du z-score sous H0 et H1
z_h0 = [(np.random.binomial(seq_len, 0.5) - 0.5 * seq_len) / np.sqrt(seq_len * 0.25)
         for _ in range(1000)]
z_h1 = [(np.random.binomial(seq_len, 0.85) - 0.5 * seq_len) / np.sqrt(seq_len * 0.25)
         for _ in range(1000)]

axes[1].hist(z_h0, bins=40, alpha=0.7, color="#4C72B0", label="$H_0$ (non watermarké)",
             density=True, edgecolor="white")
axes[1].hist(z_h1, bins=40, alpha=0.7, color="#DD8452", label="$H_1$ (watermarké)",
             density=True, edgecolor="white")
axes[1].axvline(x=4, color="black", linestyle="--", linewidth=2, label="Seuil $z = 4$")
axes[1].set_xlabel("Z-score")
axes[1].set_ylabel("Densité")
axes[1].set_title("Distribution du z-score sous $H_0$ et $H_1$")
axes[1].legend(fontsize=9)

fig.suptitle("Watermarking : distribution des tokens et détection statistique",
             fontsize=13, y=1.02)
plt.show()
_images/0718258d533783cfee39ec1860b5c46ade46f645b9a3a8987e0753bf5f3f398f.png

Les limites du watermarking incluent : (1) la fragilité face à la paraphrase — un texte watermarké peut perdre son signal après reformulation par un autre LLM ; (2) l’impossibilité de watermarker des textes très courts ; (3) le risque de dégradation de la qualité si \(\delta\) est trop élevé ; (4) les questions éthiques liées à la surveillance. Néanmoins, le watermarking reste l’une des rares techniques permettant une détection fiable du texte généré par un LLM spécifique.

Cadre réglementaire : EU AI Act et LLM#

L’Union européenne a adopté en 2024 le AI Act (Règlement sur l’intelligence artificielle), premier cadre réglementaire complet au monde pour l’IA. Ce règlement, entrant en vigueur progressivement entre 2024 et 2027, a des implications directes pour les développeurs et déployeurs de LLM.

Définition 106 (AI Act et classification des risques)

L”EU AI Act (Règlement (UE) 2024/1689) établit un cadre fondé sur une classification par niveaux de risque :

  1. Risque inacceptable : systèmes interdits. Exemples : notation sociale, manipulation subliminale, identification biométrique à distance en temps réel (sauf exceptions).

  2. Risque élevé : obligations strictes avant mise sur le marché. Exemples : IA dans le recrutement, l’éducation, la justice. Obligations : évaluation de conformité, gestion des risques, gouvernance des données, transparence, supervision humaine, robustesse.

  3. Risque limité : obligations de transparence. Exemples : chatbots, systèmes génératifs. L’utilisateur doit être informé qu’il interagit avec une IA.

  4. Risque minimal : pas d’obligations spécifiques. Exemples : filtres anti-spam, jeux video.

Les modèles GPAI (General-Purpose AI), dont les LLM, font l’objet de dispositions spécifiques (Titre VIII bis) avec des obligations proportionnées à leur impact.

Hide code cell source

fig, ax = plt.subplots(figsize=(12, 8))
ax.set_xlim(0, 12)
ax.set_ylim(0, 10)
ax.axis("off")

levels = [
    (6, 1.5, 10.0, 1.5, "Risque minimal", "#55A868",
     "Pas d'obligations spécifiques\nEx : filtres anti-spam, jeux video"),
    (6, 3.5, 8.0, 1.5, "Risque limité", "#4C72B0",
     "Obligations de transparence\nEx : chatbots, contenu généré par IA"),
    (6, 5.5, 6.0, 1.5, "Risque elevé", "#DD8452",
     "Conformité, audit, supervision humaine\nEx : recrutement, éducation, justice"),
    (6, 7.5, 4.0, 1.5, "Risque inacceptable", "#C44E52",
     "INTERDIT\nEx : notation sociale, manipulation subliminale"),
]
for cx, cy, width, height, label, color, desc in levels:
    rect = mpatches.FancyBboxPatch((cx - width / 2, cy - height / 2), width, height,
                                    boxstyle="round,pad=0.1", facecolor=color,
                                    edgecolor="white", alpha=0.85, linewidth=2)
    ax.add_patch(rect)
    ax.text(cx, cy + 0.15, label, fontsize=12, fontweight="bold", ha="center",
            va="center", color="white")
    ax.text(cx, cy - 0.35, desc, fontsize=8, ha="center", va="center",
            color="white", alpha=0.95)

ax.annotate("Modèles GPAI\n(dont LLM)", xy=(10.5, 4.5), fontsize=10,
            fontweight="bold", ha="center", va="center",
            bbox=dict(boxstyle="round,pad=0.4", facecolor="#8B6DAF",
                      edgecolor="white", alpha=0.9), color="white")
ax.annotate("", xy=(9.0, 3.5), xytext=(10.0, 4.0),
            arrowprops=dict(arrowstyle="->", color="#8B6DAF", lw=2))
ax.annotate("", xy=(9.0, 5.5), xytext=(10.0, 5.0),
            arrowprops=dict(arrowstyle="->", color="#8B6DAF", lw=2))
ax.set_title("EU AI Act : classification des risques", fontsize=14, pad=15)
plt.show()
_images/076ac7c2de44dfa4fbb8997a17b1f277e016e3355fd4d4b004f180d1eb2026cc.png

Exemple 73 (Obligations spécifiques aux LLM selon l’AI Act)

Obligations pour tous les modèles GPAI :

  • Documentation technique détaillée (architecture, données d’entrainement, évaluations)

  • Politique de respect du droit d’auteur européen

  • Résumé détaillé des données d’entrainement

  • Désignation d’un représentant dans l’UE pour les fournisseurs hors-UE

Obligations supplémentaires pour les modèles à risque systémique (\(> 10^{25}\) FLOPs) :

  • Evaluations de modèle conformes à des protocoles standardisés

  • Evaluation et mitigation des risques systémiques

  • Tests adversariaux (red-teaming, chapitre 17)

  • Notification des incidents graves à la Commission européenne

  • Garanties de cybersécurité et reporting energétique

Le seuil de \(10^{25}\) FLOPs correspond approximativement à un modèle de la taille de GPT-4. Les modèles open-source bénéficient d’exemptions partielles, sauf s’ils présentent un risque systémique.

Remarque 101 (Checklist de conformité)

Pour un déploiement de LLM conforme a l’AI Act :

  • Le système est-il classé à risque élevé ? Si oui, évaluation de conformité obligatoire.

  • L’utilisateur est-il informé qu’il interagit avec une IA ?

  • Le contenu généré par IA est-il étiqueté comme tel ?

  • Une documentation technique est-elle disponible (model card, datasheet) ?

  • Les données d’entrainement respectent-elles le droit d’auteur europeen ?

  • Un mécanisme de supervision humaine est-il en place ?

  • Les risques de biais et de discrimination ont-ils été évalués ?

  • Le modèle a-t-il fait l’objet de tests adversariaux (red-teaming) ?

  • La consommation énergétique est-elle documentée ?

Cette checklist constitue un point de départ, non un substitut à une analyse de conformité complète.

Gouvernance et documentation#

La gouvernance des systèmes d’IA repose sur une documentation rigoureuse et standardisée. Trois instruments sont devenus des standards de fait : les model cards, les datasheets for datasets et les évaluations d’impact.

Exemple 74 (Model card pour un LLM)

Une model card (Mitchell et al., 2019) accompagne un modèle d’IA et décrit : (1) détails du modèle (architecture, taille, date), (2) utilisation prévue et hors périmètre, (3) métriques et évaluations, (4) données d’entrainement, (5) biais connus, (6) limites. Exemple :

# Model Card : MonLLM-7B
## Détails
- Transformer décodeur, 32 couches, d_model = 4096, 7.2B paramètres
- Entrainement : 2T tokens (70% EN, 15% FR, 15% autres)
## Utilisation prévue
- Génération de texte, résumé, QA
- NON recommandé : diagnostic médical, conseil juridique
## Evaluations
- MMLU : 62.4% | HumanEval : 35.2% | TruthfulQA : 48.1%

La documentation couvre trois niveaux : modèle (model card), données (datasheet for datasets, Gebru et al., 2021) et système (évaluation d’impact sur les droits fondamentaux). La traçabilité complète constitue le registre d’audit (audit trail), essentiel pour la responsabilité et la conformité réglementaire.

Déploiement sécurisé en production#

Le déploiement d’un LLM en production dépasse la mise à disposition d’un endpoint API. Un système robuste nécessite une architecture de sécurité multicouche couvrant le contrôle d’accès, la surveillance, la gestion des incidents et la supervision humaine.

Hide code cell source

fig, ax = plt.subplots(figsize=(14, 8))
ax.set_xlim(0, 14)
ax.set_ylim(0, 10)
ax.axis("off")

categories = [
    (2.5, 8.5, "Rate Limiting", "#4C72B0",
     ["Limites par utilisateur/IP", "Quotas journaliers", "Token budgets"]),
    (7.0, 8.5, "Monitoring", "#DD8452",
     ["Latence et débit", "Qualité des réponses", "Dérivé de distribution"]),
    (11.5, 8.5, "Alerting", "#C44E52",
     ["Seuils d'anomalie", "Escalade automatique", "Dashboard temps réel"]),
    (2.5, 4.5, "Incident\nResponse", "#55A868",
     ["Runbooks documentés", "Post-mortems", "Classification de sévérité"]),
    (7.0, 4.5, "Human\nOversight", "#8B6DAF",
     ["Review des cas limites", "Feedback loop", "Audit périodique"]),
    (11.5, 4.5, "Kill Switch", "#E24A33",
     ["Arrêt d'urgence", "Rollback automatique", "Fallback déterministe"]),
]
for cx, cy, label, color, items in categories:
    rect = mpatches.FancyBboxPatch((cx - 1.8, cy - 0.5), 3.6, 1.0,
                                    boxstyle="round,pad=0.15", facecolor=color,
                                    edgecolor="white", alpha=0.9, linewidth=2)
    ax.add_patch(rect)
    ax.text(cx, cy, label, fontsize=11, fontweight="bold", ha="center",
            va="center", color="white")
    for i, item in enumerate(items):
        ax.text(cx, cy - 1.0 - i * 0.45, item, fontsize=8, ha="center", va="center",
                bbox=dict(boxstyle="round,pad=0.2", facecolor="#f5f5f5",
                          edgecolor=color, alpha=0.6))

ax.set_title("Déploiement sécurisé : composants essentiels", fontsize=14, pad=15)
plt.show()
_images/1ef35125d9feb7f6ef4384c2ec676475ee28f649f5fdfa6a8fb17b9b9f61e4da.png

Rate limiting. Le contrôle du débit protège contre les abus et maitrise les coûts. Les limites sont définies par utilisateur, par clé API, par IP et par fenêtre temporelle. Un budget en tokens complète les limites en nombre de requêtes.

Monitoring. La surveillance couvre les métriques techniques (latence, débit, taux d’erreur) et les métriques de qualité (toxicité, hallucinations, satisfaction). La dérivé de distribution des requêtes peut signaler une tentative d’exploitation.

Alerting. Des alertes automatiques se déclenchent lorsque les métriques dépassent des seuils prédéfinis, avec escalade documentée.

Incident response. Un plan documente les procédures en cas de défaillance. Chaque incident fait l’objet d’un post-mortem analysant les causes racines.

Human oversight. La supervision humaine (exigence de l’AI Act pour les systèmes à risque élevé) prend la forme de revues manuelles des cas limites, de boucles de feedback et d’audits périodiques.

Kill switch. Un mécanisme d’arrêt d’urgence permet de désactiver le système en quelques secondes. Le mode dégradé remplace les réponses du LLM par des réponses prédefinies ; le rollback automatique revient à une version antérieure.

Hide code cell source

hours = np.arange(0, 168)

latency = 120 + 15 * np.random.randn(len(hours))
latency[72:78] = 350 + 50 * np.random.randn(6)
error_rate = 0.5 + 0.3 * np.abs(np.random.randn(len(hours)))
error_rate[72:78] = 8 + 2 * np.random.randn(6)
requests = np.clip(500 + 100 * np.sin(2 * np.pi * hours / 24)
                   + 50 * np.random.randn(len(hours)), 0, None)
toxicity = 0.02 + 0.01 * np.abs(np.random.randn(len(hours)))
toxicity[100:106] = 0.15 + 0.03 * np.random.randn(6)

fig, axes = plt.subplots(2, 2, figsize=(14, 8))

for ax, data, color, ylabel, title, thresh in [
    (axes[0, 0], latency, "#4C72B0", "Latence (ms)", "Latence P50", 250),
    (axes[0, 1], error_rate, "#DD8452", "Taux d'erreur (%)", "Taux d'erreur", 5),
    (axes[1, 1], toxicity, "#8B6DAF", "Score de toxicité", "Toxicité des réponses", 0.1),
]:
    ax.plot(hours, data, color=color, linewidth=1.2, alpha=0.8)
    ax.axhline(y=thresh, color="#C44E52", linestyle="--", alpha=0.7, label="Seuil d'alerte")
    ax.fill_between(hours, 0, data, where=data > thresh, color="#C44E52", alpha=0.2)
    ax.set_ylabel(ylabel)
    ax.set_title(title)
    ax.legend(fontsize=8)

axes[1, 0].plot(hours, requests, color="#55A868", linewidth=1.2, alpha=0.8)
axes[1, 0].set_ylabel("Requêtes / heure")
axes[1, 0].set_title("Volume de requêtes")
axes[1, 0].set_xlabel("Heures")
axes[1, 1].set_xlabel("Heures")

fig.suptitle("Dashboard de monitoring : une semaine de production (données simulées)",
             fontsize=13, y=1.02)
plt.show()
_images/5dce82597e4f2d579370fcd098c4383746c153e7fcb581d12ce5decf5d4f2a86.png

Résumé#

Ce chapitre a présenté les six facettes de la sécurité et du déploiement responsable des LLM.

  1. Les hallucinations sont un comportement inhérent aux LLM, qui génèrent du texte statistiquement probable plutôt que factuellement vrai. On distingue les hallucinations factuelles, de fidélité et contextuelles. Les stratégies de détection incluent la self-consistency, la vérification de citations et le factual grounding par NLI. Les mitigations principales sont le RAG, la chain-of-verification et la réduction de la température.

  2. Le filtrage de contenu constitue une couche de défense indépendante de l’alignement. Un pipeline complet comporte un filtrage en entrée, un filtrage en sortie et une classification thématique. Les approches par classifieur et par règles se complètent dans une architecture en cascade.

  3. Le watermarking de Kirchenbauer et al. (2023) biaise la génération vers des tokens « verts » déterminés par le token précédent. La détection repose sur un z-test mesurant l’excès de tokens verts. La puissance du test croit avec la longueur du texte, mais le watermark est fragile face à la paraphrase.

  4. L”EU AI Act (2024) établit un cadre réglèmentaire fondé sur quatre niveaux de risque (inacceptable, élevé, limité, minimal). Les modèles GPAI font l’objet d’obligations de documentation, de transparence et, au-delà de \(10^{25}\) FLOPs, d’évaluations de risque systémique et de red-teaming.

  5. La gouvernance repose sur trois piliers documentaires : les model cards (niveau modèle), les datasheets for datasets (niveau données) et les évaluations d’impact (niveau système). Le registre d’audit assure la traçabilite des décisions à chaque étape du cycle de vie.

  6. Le déploiement sécurisé exige une architecture multicouche : rate limiting, monitoring, alerting avec escalade, plan de réponse aux incidents, supervision humaine et kill switch. La dérivé de distribution des requêtes doit être surveillée comme indicateur précoce de tentatives d’exploitation.

  7. La sécurité des LLM est un problème de défense en profondeur : aucune couche unique (alignement, filtrage, watermarking, réglementation) ne suffit à elle seule. C’est la combinaison de mesures techniques, organisationnelles et réglementaires qui permet un déploiement responsable.