Une erreur de compréhension d’un agent IA a failli tout casser. Voici comment une équipe a évité le désastre sans ralentir tout le monde.

Trois semaines après avoir déployé un agent de conversion texte en SQL pour son équipe d’analyse interne, quelqu’un lui a demandé de « nettoyer les lignes de test dans la table des promotions ». L’agent a interprété « nettoyer » comme « supprimer » et « lignes de test » comme toute ligne avec un drapeau is_test ou contenant le mot « test ». Résultat : il a généré une requête DELETE qui aurait effacé 40 % d’une table dont dépendaient plusieurs tableaux de bord.

Heureusement, cette requête n’a jamais été exécutée. Pourquoi ? Parce que l’entreprise avait déjà mis en place une règle simple : toute requête qui n’était pas un SELECT devait être validée par un humain avant exécution. Cette règle, imposée des mois plus tôt lors d’une revue de conception, a sauvé la mise. Le validateur a repéré l’erreur en posant une question et la requête est restée bloquée dans la file d’attente.

Une sécurité trop large ne protège pas mieux, elle ralentit tout.

L’INSTINCT QUI A TOUT FAIT CROIRE À LA SÉCURITÉ

La première version de la supervision humaine dans presque tous les systèmes d’agents ressemble à ceci : toute action qui n’est pas une lecture (un SELECT) doit être validée par une personne avant d’être exécutée. C’est une règle facile à écrire, facile à expliquer lors d’une revue de sécurité, et qui donne l’impression d’être prudent pendant les premières semaines.

Mais c’est aussi la version qui scale le moins bien. Et le problème n’est pas que les humains sont lents. C’est que la règle ne fait aucune différence entre une requête DELETE qui supprime 40 % d’une table utilisée par trois tableaux de bord et une requête DELETE qui supprime une seule ligne, demandée explicitement par un utilisateur quelques secondes plus tôt dans la même conversation. Les deux se retrouvent dans la même file d’attente, derrière toutes les autres requêtes. Le validateur n’a aucun signal pour savoir laquelle mérite cinq secondes d’attention et laquelle mérite cinq minutes. Résultat : soit il traite tout avec la même attention superficielle, soit il devient excessivement prudent sur tout. Aucune de ces options n’est ce qu’on veut vraiment.

OÙ LA FILE D’ATTENTE A TOUT CASSE

Après quelques jours, le temps d’attente médian pour une validation avait dépassé quinze minutes. Les validateurs ont commencé à approuver les requêtes par lots, en survolant cinq ou six requêtes à la fois. La qualité des validations a chuté.

Les gens cliquaient sur « approuver » sans avoir tout lu, parce que l’alternative était de prendre encore plus de retard. Et comme la plupart des requêtes étaient correctes, cette solution de contournement fonctionnait… jusqu’à ce qu’elle ne fonctionne plus. C’est le genre de problème qui n’apparaît jamais dans une revue de conception. Une sécurité trop large ne devient pas plus prudente sous la pression, elle devient plus rapide et plus superficielle. Exactement de la manière qui érode ce qu’elle était censée protéger.

Ce phénomène est souvent décrit comme la fatigue du tampon en caoutchouc : le mécanisme est simple, la vigilance est une ressource limitée, et si on l’utilise pour des choses qui n’en ont pas besoin, il n’en reste plus pour ce qui en a vraiment besoin.

ROUTER LES ACTIONS PAR RISQUE, PAS PAR TYPE D’OPÉRATION

La solution adoptée n’a pas été de « rendre la file d’attente plus rapide ». Elle a été d’accepter que la plupart des actions de l’agent n’ont pas besoin d’être validées par un humain du tout. Et de construire un système capable de faire la différence avant que l’action n’arrive sur l’écran de quelqu’un.

Cette approche s’appelle le routage basé sur le risque. Elle consiste à noter chaque action de l’agent à partir de plusieurs signaux, et à n’escalader que celles qui dépassent un seuil de risque. Tout ce qui est en dessous de ce seuil s’exécute immédiatement, sans que personne n’ait à intervenir.

La vraie question n’est pas « faut-il certaines actions sans validation humaine ? », mais « quelles actions avons-nous jamais vraiment validées attentivement ? »

Si la réponse est « aucune, on faisait juste du tampon en caoutchouc », alors vous avez déjà une auto-validation, mais vous payez un délai de quinze minutes pour faire semblant du contraire.

Une barrière systématique de validation humaine sur chaque action d’écriture peut devenir plus un bouclier de responsabilité qu’un contrôle de sécurité efficace. Ça ressemble à de la supervision lors d’une revue de conception, mais le volume garantit que personne ne lit vraiment de près après trois semaines.

CE DONT LE ROUTEUR A BESOIN POUR FONCTIONNER

Mettre au point le routeur a pris plus de temps que de construire la file d’attente. Et c’est normal : la file d’attente, c’est de la plomberie, le routeur, c’est le vrai jugement, automatisé et rendu explicite au lieu de dépendre de la personne qui se trouve par hasard en train de valider.

L’équipe est revenue sur les logs de validation des six premières semaines et a posé une question simple : qu’est-ce qui séparait vraiment les requêtes où un validateur a repéré un problème de celles qui n’étaient que du bruit dans la file ?

Quatre signaux revenaient sans cesse :

1. L’IMPACT POTENTIEL (blast radius)

Ce n’est pas « est-ce une écriture ? » mais « combien de lignes cette requête touche-t-elle, et à quel point est-ce réversible ? ». Une requête DELETE ciblant une clé primaire est d’un niveau de risque différent d’une requête DELETE avec une clause WHERE qui touche des milliers de lignes, même si les deux sont syntaxiquement du même type de requête.

Au début, l’équipe essayait d’obtenir ce nombre à partir de EXPLAIN, mais ça les a fait trébucher : les estimations de lignes du planificateur deviennent rapidement peu fiables sur des colonnes déséquilibrées ou des prédicats corrélés. C’est exactement le genre de requête qu’un agent est susceptible de générer sans connaître la distribution des données. Ce qu’ils font maintenant est plus simple et plus honnête : exécuter la clause WHERE de la requête comme un vrai comptage, avec un plafond fixe, par exemple COUNT(*) jusqu’à 50 000 lignes, puis arrêter. Ça donne un nombre réel, avec un coût prévisible et borné, au lieu d’une estimation déguisée en certitude.

2. LA SENSIBILITÉ DE LA TABLE

Une liste blanche statique, maintenue par les propriétaires du schéma. Les tables qui touchent la facturation, l’authentification ou tout ce qui a des exigences de conservation réglementaire reçoivent un score de risque plancher, quel que soit l’aspect de la requête. C’est la seule partie du routeur que l’équipe ne ferait jamais purement apprise, car certaines tables ne devraient jamais être considérées comme à faible risque par défaut. Encoder cela comme une règle fixe est plus honnête que de compter sur un modèle pour le faire systématiquement.

3. L’ÉCART SÉMANTIQUE PAR RAPPORT AUX REQUÊTES DÉJÀ APPROUVÉES

L’équipe conserve un index d’embeddings des intentions des requêtes déjà approuvées et vérifie à quel point une nouvelle requête est proche de cet ensemble. Une requête qui ressemble de près à cinquante requêtes approuvées précédemment est d’un risque différent d’une requête sémantiquement nouvelle, non pas parce que la nouveauté est dangereuse en soi, mais parce que c’est exactement là qu’un agent est le plus susceptible d’avoir mal interprété l’intention.

4. L’ACCORD ENTRE LES RÉGÉNÉRATIONS

Au début, l’équipe a essayé d’utiliser la confiance au niveau des tokens du modèle lui-même. Puis ils ont abandonné cette idée. Les grands modèles de langage sont mal calibrés sur leur propre incertitude : un modèle peut sembler très confiant tout en ayant mal interprété la requête. Ce qui a fonctionné, et c’est moins cher qu’il n’y paraît : régénérer la même requête deux ou trois fois avec une température légèrement plus élevée et vérifier si les sorties sont d’accord. Si elles ne le sont pas, ce désaccord est un signal bien plus fort d’une ambiguïté réelle que tout ce que le modèle rapporte sur lui-même. Ça attrape exactement le type d’échec qui intéresse l’équipe : les requêtes où la compréhension de l’intention par l’agent pouvait raisonnablement mener à deux interprétations différentes.

def computeriskscore(queryplan, resamples, embeddingindex):
    blastradius = boundedrowcount(queryplan, cap=50_000)  # comptage réel, pas une estimation du planificateur
    tablefloor = SENSITIVETABLEFLOOR.get(queryplan.target_table, 0.0)
    novelty = 1 - maxsimilarity(queryplan.intentembedding, embeddingindex)
    disagreement = 1 - resampleagreement(queryplan, resamples)  # 2-3 régénérations
    # les poids sont ajustés sur un ensemble étiqueté de validations/rejets passés,
    # pas choisis à la main
    score = (
        0.40 * normalise(blast_radius)
        + 0.25 * table_floor
        + 0.20 * novelty
        + 0.15 * disagreement
    )
    return max(score, table_floor)  # les tables sensibles ne descendent jamais sous leur seuil plancher

Les poids ne sont pas le point important : les vôtres seront différents, et honnêtement, les leurs ont changé deux fois depuis le premier réglage. Ce qui compte, c’est la structure : blast radius et la sensibilité de la table dominent parce que ce sont les deux signaux qui corrèlent vraiment avec « quelque chose de mauvais arrive si c’est faux ». Tout le reste est là pour attraper ce que ces deux-là ratent.

CE QUE L’UTILISATEUR VOIT PENDANT QU’UNE ACTION EST EN ATTENTE

Dans la version naïve, la requête de l’utilisateur reste en suspens, l’agent se tait, l’interface tourne, et du point de vue de l’utilisateur, il n’y a aucune différence entre « un humain est en train de valider » et « le système est bloqué ». L’équipe est passée à quelque chose de plus proche d’un modèle de ticket : une action escaladée est immédiatement reconnue, l’utilisateur est informé explicitement que cette requête nécessite un examen et environ combien de temps cela prend généralement, et il peut continuer à travailler sur tout ce qui ne dépend pas du résultat. La validation, quand elle arrive, est envoyée sous forme de notification plutôt que quelque chose que l’utilisateur doit surveiller en attendant.

async def handleagentaction(queryplan, riskscore, threshold, user_session):
    if risk_score < threshold:
        result = await execute(query_plan)
        return AgentResponse(status="completed", result=result)
    ticket = await approval_queue.enqueue(query_plan, risk_score)
    await notify_user(
        user_session,
        f"This needs a quick review before it runs — usually under "
        f"{approval_queue.p90_wait_minutes()} minutes. I'll message you "
        f"when it's done.",
    )
    return AgentResponse(status="pending_review", ticket_id=ticket.id)

Aucune de ces modifications ne réduit le temps réel de validation. Ce qu’elle fait, c’est empêcher que le temps de validation ne soit perçu comme une défaillance du système. Une attente de quarante minutes, annoncée à l’avance et qui ne bloque rien d’autre, semble complètement différente d’une attente de quarante minutes qui ressemble à un blocage.

CE QUE LES LOGS ONT RÉVÉLÉ QUAND LE ROUTEUR A ÉTÉ MIS EN LIGNE

Après quelques semaines de fonctionnement du routeur, l’équipe a enfin pu examiner les logs de validation et poser la question qui compte vraiment : sur les requêtes qui ont été escaladées, est-ce que les validateurs ont vraiment attrapé quelque chose, ou est-ce qu’ils cliquaient juste sur « approuver » ?

Le schéma qui est apparu était plus clair que prévu. Les validateurs étaient genuinely utiles sur les requêtes où l’interprétation de l’intention par l’agent était plausible mais incorrecte : une requête formulée de manière à ce qu’un collègue humain l’interpréterait d’une façon et l’agent d’une autre. Une personne attrape cela rapidement parce qu’elle n’évalue pas la syntaxe SQL, elle évalue si « nettoyer les lignes de test » peut raisonnablement signifier « supprimer 40 % de cette table ». C’est un jugement que les modèles font encore mal quand l’ambiguïté réside dans l’intention plutôt que dans la requête elle-même.

En revanche, ils n’étaient pas très utiles sur les requêtes où la requête était mécaniquement correcte et l’ambiguïté, si ambiguïté il y avait, avait déjà été résolue plus tôt dans la conversation. Une requête UPDATE bien ciblée sur une seule ligne, générée en réponse à une instruction sans ambiguïté, qui traînait dans une file d’attente pour qu’un humain la survole et l’approuve : personne n’ajoutait de valeur là. L’équipe payait un délai pour un tampon en caoutchouc, exactement le type d’échec qui a déclenché toute cette histoire, mais maintenant appliqué à un ensemble plus petit et mieux choisi de requêtes au lieu de tout.

La revue humaine apporte beaucoup plus de valeur sur les actions ambiguës et à fort impact potentiel que sur les actions routinières et à faible impact.

Tout le monde est d’accord avec cette affirmation en théorie. Pourtant, presque aucune barrière de validation n’est vraiment construite autour de cette idée.

CELA NE RÉSOUT PAS TOUT, MAIS C’EST UNE MEILLEURE QUESTION

Aucune de ces modifications n’élimine le problème de la revue. Elle le déplace. Au lieu de demander à une personne de juger chaque écriture, on demande à une fonction de score de juger quelles écritures méritent une personne. C’est une question plus étroite et plus honnête, mais ce n’est pas une question résolue.

Les poids du routeur nécessitent un réajustement périodique, et l’équipe n’a pas encore de bonne réponse sur la fréquence à laquelle le faire. Les schémas de requêtes évoluent au fur et à mesure que le produit change, l’index d’embeddings des « intentions de requêtes approuvées » doit être élagué, sinon il commence à traiter des schémas anciens et plus pertinents comme familiers. Un routeur bien calibré au premier mois peut tranquillement dériver vers un comportement trop permissif ou trop restrictif au quatrième mois, sans que personne ne s’en aperçoive jusqu’à ce qu’un incident force à y regarder de plus près.

L’étape suivante évidente est de boucler la boucle : intégrer automatiquement les résultats rejetés et approuvés dans le réglage des poids, pour que le routeur s’auto-ajuste. Une boucle de rétroaction automatisée sur un seuil lié à la sécurité est elle-même une chose qui a besoin de supervision, et il y a quelque chose que l’équipe ne fait pas confiance dans un système qui devient moins prudent de lui-même, basé sur rien d’autre qu’une série récente de validations sans incident. C’est généralement exactement dans ces conditions que l’incident suivant se produit. Pour l’instant, ils réajustent manuellement selon un calendrier, avec quelqu’un qui examine ce qui a changé et pourquoi avant que ça ne soit déployé. C’est plus lent, mais l’équipe pense que c’est encore le compromis le plus honnête.

Sources :
  • Towards Data Science

L'indépendance de CLODCO est votre garantie.

Pour que l'actualité de l'IA reste sans filtre et sans concession, votre soutien est indispensable. Votre contribution est le seul moteur de notre liberté éditoriale.

Soutenir CLODCO