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 étourdi. Par mon IA.

Le pire n’est pas qu’elle ait inventé. Le pire est que tout compilait et 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. Un enum OrganizationSelection qui classe les orgs en payantes et gratuites
  4. Des tests avec des fixtures qui vérifient que tout fonctionne

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

Le champ active_flags n’existe pas dans la vraie API 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 motif du château de cartes

Ce qui est 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 :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// 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

Tu vois ? 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 cadre. Rien n’est réel.

IEEE Spectrum a un nom pour ceci : silent failure. Le code ne crashe pas, ne lance pas d’erreurs, n’allume pas d’alarmes. Il fait simplement la mauvaise chose 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. Tu exécutes npm install paquet-invente, ça échoue, tu t’en aperçois. Un champ inventé dans un DTO qui parse du JSON avec try? et une dégradation gracieuse… ça n’échoue pas. Ça fonctionne. Ça retourne nil ou un tableau vide. Et ton 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 ambiguïté :

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

Présenté de manière “autoritaire”. C’est la clé. Ce n’est pas qu’il doute et se trompe. C’est qu’il affirme avec une totale assurance quelque chose qu’il vient d’inventer.

Pourquoi les tests ne te sauvent pas

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

Le problème 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, la fixture a un active_flags, et 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 20/20 à lui-même. Chaque étape est cohérente en interne. 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 au vert       ← fiction cohérente

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

Toutes les mesures actuelles sont préventives

Si tu cherches ce que tu peux faire pour éviter ceci, la littérature et l’expérience t’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 créativité, n’élimine pas invention
Grounding avec documentsPréventiveSeulement si tu as le bon document
Interdictions explicitesPréventiveLe LLM peut “rationaliser” des exceptions
RAG (Retrieval Augmented Generation)PréventiveDépend de la complétude de la base de données

Tu remarques le motif ? Toutes tentent d’empêcher que l’IA invente. Aucune ne détecte quand elle l’a déjà fait.

C’est comme mettre un panneau “interdiction de voler” dans un magasin sans caméras, sans alarmes et sans vigile. Ça peut marcher. Ça peut ne pas marcher. Tu n’as aucun moyen de le savoir jusqu’à ce que tu comptes 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 se soit produite, idéalement avant qu’elle n’arrive en production.

Imagine :

  • Contract testing contre de vraies APIs : 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 à de vraies données 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 de vraies réponses 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 second 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 ceci n’existe aujourd’hui en tant que produit. Certaines équipes implémentent des pièces à la main (contract testing est une pratique connue, par exemple). Mais il n’y a pas de HallucinationTracker que tu branches sur ton CI et qui te dit “hé, ce champ active_flags n’apparaît dans aucune documentation ni vraie réponse de l’API”.

Et oui, il y a un papier 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 tu peux 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 tu mets “n’invente pas de données” dans ton CLAUDE.md, tu le dis au même modèle qui va inventer des données. C’est comme demander à l’accusé d’être aussi le juge. Ça peut marcher, mais tu n’as pas de garanties.

Les mesures préventives (bonnes instructions, température basse, grounding) réduisent la probabilité d’invention. Mais 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 ni rationaliser ni esquiver, parce que ce n’est pas le modèle qui l’exécute.

Tant que cela n’existe pas 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 m’arrivera pas à moi”.

Ce que je fais en attendant

Pour être honnête, voici les mesures qui me fonctionnent aujourd’hui. Aucune n’est parfaite :

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

  2. Demander “d’où sors-tu ça ?” Surtout 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 code et tests et que tout passe du premier coup, ce n’est pas bon signe — c’est signe qu’elle a probablement validé fiction contre fiction.

  5. Capturer de vraies réponses comme fixtures. Au lieu de laisser l’IA écrire les fixtures, sauvegarder les vraies réponses de l’API et les utiliser comme fixture. Si le DTO ne parse pas la vraie réponse, ç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.

Pas besoin que ce soit parfait. Il faut que ça existe. 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 cadre pas.

Aujourd’hui, si ton IA invente un champ d’API et l’enrobe dans des tests cohérents, la seule défense c’est toi lisant le code avec un œil clinique. Demain, il devrait y avoir une machine qui le fasse pour toi.

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


En relation : Ce post est le troisième chapitre d’une série involontaire. D’abord il y a eu les 44 e-mails 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 enrobe 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.