Hier j’ai découvert que la moitié d’un module de mon app était basée sur des données inventées. Pas par un junior distrait. Par mon IA.

Le pire n’est pas qu’elle ait inventé. Le pire c’est que tout compilait et que les 90 tests passaient.

La fiction cohérente

Je suis en train de construire BFClaude-9000, une app de barre de menu pour macOS qui surveille le quota de Claude Max. Une partie de la fonctionnalité nécessite de distinguer si un compte Claude est payant ou gratuit, en appelant l’API de claude.ai.

J’ai demandé à Claude Code d’implémenter la détection. Il l’a fait. Il m’a livré :

  1. Un DTO OrganizationInfo avec un champ activeFlags: [String]
  2. Une propriété calculée isPaid qui vérifie si activeFlags n’est pas vide
  3. Une enum OrganizationSelection qui classe les orgs en payantes et gratuites
  4. Des tests avec fixtures qui vérifient que tout fonctionne

Joli. Propre. Bien structuré. Tout inventé.

Le champ active_flags n’existe pas dans l’API réelle de Claude. Ou s’il existe, il ne fonctionne pas comme le code l’assumait. Quand je me suis connecté avec mon compte payant, l’app m’a dit que mon compte était gratuit.

Le modèle du château de cartes

L’insidieux n’est pas qu’il ait menti sur un champ de l’API. C’est le système complet qu’il a construit autour de ce mensonge :

// DTO avec champ inventé
struct OrganizationInfo: Decodable {
    let uuid: String
    let name: String
    let activeFlags: [String]  // ← Ceci n'existe pas

    var isPaid: Bool { !activeFlags.isEmpty }
}

// Logique qui dépend du champ inventé
enum OrganizationSelection {
    case paid(id: String, name: String)
    case noPaidOrg    // ← Cet état ne devrait pas exister
    case noOrgs
}

// Tests avec fixtures qui valident l'invention
let paidOrg = """
{"uuid": "abc", "name": "Acme", "active_flags": ["pro"]}
"""
// Test passe ✅ — mais valide fiction contre fiction

Vous voyez ? Ce n’est pas un champ mal placé. C’est un château de cartes : le DTO définit un champ faux, la logique dépend de ce champ, les tests valident que la logique fonctionne avec des fixtures qui sont aussi fausses. Chaque pièce confirme les autres. Tout s’ajuste. Rien n’est réel.

IEEE Spectrum a un nom pour ça : silent failure. Le code ne plante pas, ne lance pas d’erreurs, n’allume pas d’alarmes. Il fait simplement l’incorrect en silence.

Ce n’est pas un cas isolé

Il s’avère que la communauté a déjà un nom pour quand un LLM invente des paquets et dépendances : package hallucination. Une étude de Snyk a trouvé qu’entre 5% et 20% des recommandations de paquets des principaux LLMs sont inventées. Des paquets qui n’existent pas, publiés.

Mais le cas des paquets est le cas facile. Vous exécutez npm install paquet-inventé, ça échoue, vous vous en rendez compte. Un champ inventé dans un DTO qui parse du JSON avec try? et dégradation gracieuse… ça n’échoue pas. Ça fonctionne. Ça retourne nil ou un array vide. Et votre code continue, opérant sur des données fantômes.

Anthropic elle-même, dans sa documentation sur réduire les hallucinations, le dit sans détour :

“Claude can sometimes generate responses that contain fabricated information… presented in a confident, authoritative manner.”

Présenté de façon “autoritaire”. C’est ça la clé. Ce n’est pas qu’il doute et se trompe. C’est qu’il affirme avec totale assurance quelque chose qu’il vient d’inventer.

Pourquoi les tests ne vous sauvent pas

C’est là que ça fait mal. J’avais des tests. De bons tests. 90 tests en 12 suites. Tous au vert. Et alors ?

Le problème c’est que les tests valident la cohérence interne, pas la correspondance avec la réalité. Si le DTO dit que le champ s’appelle active_flags, que la fixture a un active_flags, et que le test vérifie que le DTO parse la fixture… tout passe. Fiction contre fiction. Vert phosphorescent.

C’est comme si un étudiant inventait une formule de physique, écrivait un examen basé sur cette formule, et se mettait un 20 à lui-même. Chaque étape est internalement cohérente. Le résultat n’a aucun contact avec la réalité.

Réalité: champ X n'existe pas dans l'API
   ↓ (invisible)
DTO:     définit champ X         ← inventé
Fixture: inclut champ X          ← inventé pour valider le DTO
Test:    fixture parse bien      ← valide invention contre invention
Résultat: ✅ Tout vert          ← fiction cohérente

Il n’y a aucun point dans cette chaîne où on vérifie contre l’API réelle. Et c’est ça le trou.

Toutes les mesures actuelles sont préventives

Si vous cherchez ce que vous pouvez faire pour éviter ça, la littérature et l’expérience vous offrent une liste de mesures. Toutes sont préventives :

MesureTypeProblème
Instructions dans CLAUDE.md : “n’invente pas”PréventiveExécutées par le même agent qui ment
Chain of thought : “cite tes sources”PréventivePeut citer des sources inventées
Température bassePréventiveRéduit la créativité, n’élimine pas l’invention
Grounding avec documentsPréventiveSeulement si vous avez le bon document
Interdictions explicitesPréventiveLe LLM peut “rationaliser” les exceptions
RAG (Retrieval Augmented Generation)PréventiveDépend de la complétude de la base de données

Vous voyez le modèle ? Toutes tentent d’éviter que l’IA invente. Aucune ne détecte quand elle l’a déjà fait.

C’est comme mettre un panneau “interdit de voler” dans un magasin sans caméras, sans alarmes et sans vigile. Ça peut marcher. Ça peut ne pas marcher. Vous n’avez aucun moyen de le savoir jusqu’à ce que vous comptiez la caisse.

Ce qui manque : détection réactive

Ce dont nous avons besoin et qui n’existe pas aujourd’hui, ce sont des mesures réactives : des systèmes qui détectent l’invention après qu’elle soit arrivée, idéalement avant qu’elle arrive en production.

Imaginez :

  • Contract testing contre des APIs réelles : un test qui appelle la vraie API (avec des credentials de test) et compare le schéma réel avec le DTO. Si le DTO a des champs que l’API ne retourne pas, alarme.

  • Validation de fixtures : un linter qui vérifie que les fixtures de test correspondent à des données réelles capturées, pas à des données écrites à la main (ou générées par l’IA). Quelque chose comme le snapshot testing mais contre des réponses réelles de production.

  • Smoke tests avec données réelles : avant de merger, une étape de CI qui exécute les appels contre un sandbox de l’API et vérifie que les DTOs parsent des données réelles sans perte silencieuse.

  • Détection d’anomalies dans le parsing : si un champ optionnel retourne nil 100% du temps en production, quelque chose sent mauvais. Un moniteur qui détecte les champs qui sont toujours nil et les signale comme suspects d’être inventés.

  • Diff sémantique post-génération : un deuxième modèle (ou le même avec un prompt différent) qui révise le code généré et signale les champs ou structures qu’il ne peut pas vérifier contre la documentation connue.

Rien de tout ça n’existe aujourd’hui comme produit. Certaines équipes implémentent des pièces à la main (le contract testing est une pratique connue, par exemple). Mais il n’y a pas de HallucinationTracker que vous branchez à votre CI et qui vous dit “hé, ce champ active_flags n’apparaît dans aucune documentation ni réponse réelle de l’API”.

Et oui, il y a un paper de l’Université de Washington (HallucinationTracker) qui propose des métriques pour détecter les confabulations. Mais c’est en phase de recherche, ce n’est pas quelque chose que vous pouvez brew install.

Le problème de fond

Le problème de fond est profondément inconfortable : les règles sont exécutées par le même système qui les viole.

Quand vous mettez “n’invente pas de données” dans votre CLAUDE.md, vous le dites au même modèle qui va inventer des données. C’est comme demander à l’accusé d’être aussi le juge. Ça peut fonctionner, mais vous n’avez pas de garanties.

Les mesures préventives (bonnes instructions, température basse, grounding) réduisent la probabilité d’invention. Mais elles ne l’éliminent pas. Et quand ça arrive, il n’y a pas de sirène qui sonne.

Ce dont nous avons besoin c’est que la détection soit faite par quelque chose d’externe au modèle : un test contre des données réelles, un linter de schémas, un moniteur en production. Quelque chose que le modèle ne peut pas rationaliser ni esquiver, parce que ce n’est pas le modèle qui l’exécute.

Jusqu’à ce que ça existe comme quelque chose de mature et facile à utiliser, nous sommes dans la même situation que la sécurité informatique avant les firewalls : nous savons qu’il y a un problème, nous avons des mesures partielles, et nous espérons que “ça ne va pas m’arriver”.

Ce que je fais moi en attendant

En étant honnête, voici les mesures qui me fonctionnent aujourd’hui. Aucune n’est parfaite :

  1. Lire le code généré comme s’il était d’un inconnu. Ne pas assumer qu’il est correct parce qu’il compile. C’est épuisant, mais c’est ce qu’il y a.

  2. Demander “d’où as-tu sorti ça ?” Spécialement pour les champs d’API, noms de paquets, et toute donnée que je ne peux pas vérifier en regardant le code.

  3. Contract tests manuels. Avant de valider un DTO, faire un appel réel à l’API et comparer. C’est fastidieux. C’est nécessaire.

  4. Se méfier des tests qui passent du premier coup. Si l’IA génère du code et des tests et que tout passe du premier coup, ce n’est pas bon signe — c’est le signe qu’elle a probablement validé fiction contre fiction.

  5. Capturer des réponses réelles comme fixtures. Au lieu de laisser l’IA écrire les fixtures, sauvegarder les réponses réelles de l’API et les utiliser comme fixture. Si le DTO ne parse pas la réponse réelle, ça casse immédiatement.

Ces mesures sont manuelles, lentes, et dépendent de ma discipline. Elles ne passent pas à l’échelle. Mais aujourd’hui c’est le mieux que j’aie.

Ce qui devrait exister demain

Si quelqu’un cherche un vrai problème à résoudre, en voici un :

Un système de vérification post-génération qui soit externe au modèle, automatique, et qui s’intègre dans CI/CD.

Il n’a pas besoin d’être parfait. Il a besoin d’exister. Que quelqu’un construise l’équivalent d’un linter pour hallucinations : quelque chose qui analyse le code généré, le croise avec des sources vérifiables (documentation d’APIs, schémas OpenAPI, réponses capturées), et signale ce qui ne colle pas.

Aujourd’hui, si votre IA invente un champ d’API et l’enveloppe dans des tests cohérents, la seule défense c’est vous qui lisez le code avec un œil clinique. Demain, il devrait y avoir une machine qui le fait pour vous.

Mais aujourd’hui elle n’existe pas. Et c’est ça le plus préoccupant de tout.


Connexe : Ce post est le troisième chapitre d’une série involontaire. D’abord ce fut les 44 emails inventés (l’IA qui agit sans permission). Puis MEMORY.md (l’IA qui oublie ce qu’elle a appris). Maintenant, l’IA qui invente des données et les enveloppe dans une fiction qui passe les tests. Trois pannes différentes, un dénominateur commun : nous faisons trop confiance à un système qui ne comprend pas ce qu’il fait.

Cet article a été rédigé en espagnol et traduit avec l’aide de l’IA.