Le modèle qui perd des chiffres : choisir l'architecture d'un LLM par maillon agentique

Il y a quelques jours, j'ai cliqué sur un lien fabriqué par mon assistant local. 404. C'était une URL de tweet : https://x.com/karpathy/status/204990382105354523. Le tweet d'origine, lui, était bien là sous une URL voisine : https://x.com/karpathy/status/2049903821095354523. La différence : dix-huit chiffres dans la première, dix-neuf dans la seconde. Un chiffre avait disparu au milieu de l'identifiant.

L'assistant, c'est mAIstrow, le client desktop que j'utilise au quotidien et qui tourne sur mon propre moteur d'inférence (herbert-rs) avec Granite-4.1-30B en Q4 comme backbone. L'outil web_fetch a reçu l'URL tronquée et il a fait ce qu'il devait faire : il a appelé l'URL et il a renvoyé un 404.

Première réaction : c'est mon code. Je suis en train d'écrire un moteur d'inférence à partir de zéro, j'ai du sampling, du tokenizer et de la gestion de KV cache à debugger en permanence. Une perte d'un chiffre au milieu d'une chaîne, ça ressemble à une erreur de pointeur ou à un sampler mal câblé. J'ai ouvert le bug bundle, j'ai regardé la trace : la réponse du modèle était déjà arrivée tronquée du côté de mAIstrow. Le sampler n'avait rien à se reprocher. Le modèle, en sortie de génération, avait écrit littéralement dix-huit chiffres au lieu de dix-neuf.

Avant le bench, un mot sur le titre. Quand je parle de « système d'agents IA », je parle d'un système qui enchaîne plusieurs appels au modèle pour une même requête utilisateur (extraire un argument, appeler un outil, synthétiser un résultat, suivre un format), pas d'une orchestration de plusieurs agents qui collaborent. L'article défend une seule idée : l'archi du modèle qui domine n'est pas la même d'un maillon à l'autre. Ce qui a cassé sur mAIstrow, c'est exactement le maillon de la copie verbatim.

Reproduction minimale

Pour sortir herbert-rs de l'équation, j'ai relancé exactement le même prompt sous llama-server (llama.cpp build b8680), avec le même quant Q4_K_M de Granite-4.1-30B et temperature=0. Même réponse. Un chiffre en moins, au même endroit.

Donc ce n'est pas mon code. Ce n'est pas non plus du sampling stochastique : à temperature=0, le modèle est déterministe sur ce prompt. Si je relance, j'ai la même mauvaise sortie. Toujours.

Le prompt, lui, est explicite à un niveau presque insultant pour le modèle :

Extract the numeric status ID from this URL and return it as JSON in the form {"id": "<digits>"}. Copy the digits EXACTLY: do not abbreviate, round, or omit any digit. Respond with the JSON only, no prose.

L'URL est dans le contexte. Trois tokens plus haut. Le modèle a juste à pointer dessus. Il rate.

Ce n'est pas une hallucination de connaissance : c'est une faute de copie sur une chaîne présente dans le contexte. Cette catégorie d'erreur a un nom et elle a une littérature.

Le bench

J'ai écrit un bench minimal en cinquante lignes de Python. Pour cinq longueurs (8, 12, 16, 19, 22 chiffres), vingt identifiants aléatoires par longueur, le même prompt, un seul tour utilisateur, temperature=0. Comparaison caractère par caractère. Et pour ne pas rester sur du synthétique, j'ai aussi pris vingt-et-une vraies URLs de tweets publics (@tenstorrent, @elonmusk, @ggerganov, @karpathy) que n'importe qui peut cliquer pour vérifier l'identifiant attendu.

Voilà ce que ça donne sur sept modèles, tous via le même backend llama-server :

ModèleArchitectureQuantErreur (vraies URLs)
granite-4.1-30BMamba2-hybride (36/4)Q4_K_M24% (5/21)
granite-4.1-8BMamba2-hybrideBF1688%
granite-4.0-1BTransformer denseBF160%
Qwen3-VL-2BTransformer denseQ4_K_M0%
Qwen3.6-27BHybride DeltaNet 3:1Q4_K_M0%
LFM2-2.6BHybride shortconv (~33% attn)Q4_K_M0%
gpt-oss-20bTransformer dense (MoE)F160%
gemma-4-26B-A4BTransformer dense (MoE)Q4_K_M0%
SmolLM3-3BTransformer denseQ4_K_M0%

Le pattern saute aux yeux. Tout ce qui est Granite-4-H (les hybrides Mamba2 vanilla) rate. Tout le reste passe, indépendamment de la taille (de 2B à 27B), du quant (Q4 ou BF16) ou de la famille.

La ligne qui isole vraiment la variable, c'est Granite-4.0-1B : le plus petit modèle de la même famille, en BF16, en Transformer dense classique (40 couches d'attention, zéro Mamba). Cent pour cent juste sur le bench. Même tokenizer, même pipeline d'entraînement, même équipe. Seule différence : l'architecture.

Sur les longueurs synthétiques, le profil de Granite-4.1-30B suit une courbe régulière : 10% d'erreur à 8 chiffres, 50% à 16 chiffres, 55% à 19 chiffres, 30% à 22 chiffres. Granite-4.1-8B est encore pire, jusqu'à 100% sur certaines longueurs. Le modèle plus petit en pleine précision se trompe plus que le grand modèle quantifié. La quantification n'est pas le problème.

Quatre hypothèses falsifiées

Avant d'arriver à "c'est l'architecture", j'ai traversé quatre hypothèses. Aucune n'a tenu plus de dix minutes de bench, mais chacune était plausible quand je l'ai posée.

Le tokenizer. Granite utilise un BPE numérique en chunks de 1 à 3 chiffres (regex \p{N}{1,3}), comme GPT-3.5 et GPT-4. Llama-3, Qwen3 et Gemma-4 sont sur du single-digit (\p{N}). On sait depuis Singh & Strouse 2024 (arXiv:2402.14903) que ce choix dégrade l'arithmétique. Hypothèse : il dégrade aussi la copie. Falsification : gpt-oss-20b utilise exactement la même regex (vérifié dans son tokenizer.json) et copie 100/100. Le tokenizer seul n'explique pas un taux d'erreur de 36%.

La quantification. Q4_K_M est agressif. Peut-être que les poids déquantifiés perdent trop de précision pour porter une chaîne de dix-neuf chiffres exacte. Falsification : Granite-4.1-8B en BF16, donc sans aucune quantification, fait pire (88%) que Granite-4.1-30B en Q4 (36%). Le modèle plus petit en pleine précision rate plus que le grand modèle quantifié.

La taille. Peut-être que Granite est juste un modèle de moyenne gamme un peu faible sur cette tâche et qu'à 30B on n'a pas assez de capacité. Falsification : Granite-4.0-1B, le plus petit de la famille, fait 0%. Et Qwen3-VL-2B, deux fois plus petit que Granite-4.0-1B, fait aussi 0%. La taille n'est pas la variable.

L'entraînement. Peut-être que la famille Granite a été optimisée pour des tâches d'instruction et de RAG, pas pour de la copie verbatim. Falsification partielle : Granite-4.0-1B, avec exactement le même pipeline d'entraînement, copie parfaitement. Donc la famille sait copier. Ce sont les variantes hybrides qui n'y arrivent pas.

Reste une variable que je n'avais pas isolée : l'architecture. Granite-4.0-1B est un Transformer dense pur. Granite-4.1-8B et 30B sont en Mamba2-hybride avec un ratio de 36 couches Mamba2 pour 4 couches d'attention.

Le théorème qui prédit le bug

À ce stade je me suis dit qu'il devait y avoir de la littérature. Il y en a. Trois papiers convergent.

Jelassi, Brandfonbrener, Kakade, Malach (Harvard / Kempner Institute, ICML 2024). Repeat After Me: Transformers are Better than State Space Models at Copying (arXiv:2402.01032). Le résultat principal est formel et fort :

"We prove two results. First, we show that a small Transformer can be used to copy extremely long sequences. Second, we show that any language model with fixed-size memory (i.e., any SSM) fails to copy long random strings."

Et leur tâche d'évaluation s'appelle phonebook retrieval : on donne un annuaire au modèle, on lui demande le numéro d'une personne. C'est strictement la même tâche que la mienne (extraire un identifiant numérique long depuis un contexte). Le 88% d'erreur de Granite-4.1-8B est la réalisation empirique de leur théorème.

L'intuition est information-théorique, pas implémentaire.

L'attention d'un Transformer alloue une mémoire de taille O(n × d) : chaque token du contexte a son propre slot. Copier un identifiant trois tokens plus haut, c'est juste pointer le bon slot.

Un SSM type Mamba a un état latent de taille fixe (d_state=256 pour Granite-4). Compresser une chaîne aléatoire de dix-neuf chiffres dans cet état puis la ressortir bit-perfect est borné par le principe des tiroirs. Le modèle doit apprendre à allouer son état précisément ; cet apprentissage ne généralise pas au-delà de la distribution d'entraînement. On ne corrige pas ça avec du meilleur C++. C'est une propriété de l'architecture.

Yang, Kautz, Hatamizadeh (déc 2024). Gated Delta Networks: Improving Mamba2 with Delta Rule (arXiv:2412.06464). Ce papier précise une nuance importante : Mamba2 vanilla est connu pour être faible en retrieval. Il existe une variante (Gated DeltaNet) qui répare ce trou en combinant un mécanisme de gating et une règle de mise à jour delta. Qwen3-Next, Qwen3.5 et Qwen3.6 utilisent Gated DeltaNet à un ratio 3:1 (75% DeltaNet, 25% attention). Mon bench le confirme : Qwen3.6-27B copie sans erreur.

Et LFM2 de LiquidAI prend une troisième voie : il intercale ses couches d'attention avec des convolutions causales courtes (shortconv, l_cache=3, donc chaque conv ne voit que les trois derniers tokens). Ces conv-layers ne cherchent pas à faire du retrieval, elles sont trop courtes pour ça. Ce sont les couches d'attention (33% du modèle) qui s'en chargent. Résultat : 0% d'erreur sur le même bench.

La lecture qui colle aux quatre points de données :

ArchitectureComposant non-attentionTente du retrieval ?Bench
Transformer dense(aucun)(sans objet)
LFM2 (LiquidAI)shortconv l_cache=3Non, trop court
Qwen3-Next / 3.5 / 3.6Gated DeltaNetOui, conçu pour
Granite-4-HMamba2 vanillaOui, mais ne sait pas

"Hybride = mauvais" est trop grossier. Le bon résumé : si le composant non-attention tente d'absorber le retrieval, il a intérêt à être conçu pour (DeltaNet) ; sinon, on le garde superficiel (LFM2) et on laisse l'attention faire son travail. Granite-4-H tombe dans le trou : Mamba2 vanilla (le SSM le plus faible en recall) plus le ratio d'attention le plus bas (10%). C'est le double pire choix.

Et IBM le savait. Dans la doc officielle Granite (ibm.com/granite/docs/models/granite), table des variantes :

Granite-4.0-Micro / 1B / 350M (Traditional, Dense): "Alternative option for users when Mamba2 support is not yet optimized (e.g. llama.cpp, PEFT, etc)"

IBM publie exprès des variantes denses dans la même famille parce qu'ils savent que les hybrides cassent dans les stacks d'inférence actuels. Le phrasing parle d'optimisation d'implémentation, mais le résultat de Harvard de 2024 prédisait déjà le problème indépendamment de l'implémentation. J'avais Granite-4-H comme backbone principal de mAIstrow depuis des semaines. J'avais raté la note dans la doc.

Un système d'agents IA est une chaîne de maillons

Un système d'agents IA, ce n'est pas un appel à un LLM. C'est une chaîne d'appels spécialisés qui se passent le résultat l'un à l'autre. Quand l'utilisateur dit "résume-moi ce thread Twitter et range-le dans mon dossier veille", il y a typiquement :

  1. Une compréhension d'intention (le modèle décide quel outil appeler).
  2. Une extraction d'arguments (l'URL du thread, le nom du dossier).
  3. Un appel à un outil externe (web_fetch qui va chercher le thread).
  4. Une compression / synthèse (le modèle résume ce qui revient).
  5. Un suivi de format (sortir une note structurée selon un template).
  6. Un dernier appel d'outil pour écrire le fichier.
Une requête utilisateur, six maillons, six compétences pivot « résume-moi ce thread Twitter et range-le dans mon dossier veille » 1. Intention choisir l'outil compétence : raisonnement 2. Extraction copier l'URL, les arguments compétence : copie verbatim 3. Tool call web_fetch(URL) compétence : signatures, types 4. Synthèse résumer le thread compétence : compression 5. Format note structurée compétence : discipline syntax. 6. Write file_write(path) compétence : signatures, types Zoom sur le maillon 2 — c'est celui qui a cassé sur mAIstrow même tâche, même prompt, même backend ; seul le modèle change ❌ Granite-4.1-30B (Mamba2-hybride) URL d'entrée : .../status/2049903821095354523 URL en sortie : .../status/204990382105354523 un chiffre perdu au milieu, 24 à 88% d'erreur sur le bench cause : le composant Mamba2 vanilla compresse l'état dans 256 dim et ne sait pas le ressortir bit-perfect ✅ Qwen3-VL-2B Q4 (Transformer dense, ~1 GB) URL d'entrée : .../status/2049903821095354523 URL en sortie : .../status/2049903821095354523 0% d'erreur sur 21 vraies URLs et 100 IDs synthétiques, 0,15 s/extraction cause : l'attention alloue un slot par token, copier = pointer le bon slot

Chacun de ces maillons mobilise une compétence différente. L'extraction d'arguments demande de la copie verbatim. La synthèse demande du raisonnement et de la compression. Le suivi de format demande de la discipline syntaxique. Le tool calling demande une connaissance précise des signatures et des types.

Ces compétences ne sont pas alignées sur les benchmarks génériques. Un modèle qui domine MMLU ou GPQA peut très bien échouer en copie verbatim. Granite-4.1-30B fait des scores honnêtes en raisonnement et 24 à 88% d'erreur en copie. Inversement, un modèle 2B en Q4 qui ferait pâle figure sur GPQA est parfaitement adéquat pour extraire un identifiant numérique d'un contexte de mille tokens.

L'erreur que j'ai faite et qui est très facile à faire quand on commence à industrialiser, c'est de prendre un modèle et de le coller à toutes les étapes. Granite-4.1-30B est un bon modèle généraliste. Il est compétitif en raisonnement, en code et en multilingue. Il s'écroule sur la copie. Si l'extraction d'arguments est sur le chemin critique de mon système, je n'ai pas besoin d'un meilleur prompt : j'ai besoin d'un autre modèle pour ce maillon-là.

Quand cette discipline en vaut la peine

Sectionner un système par maillon a un coût : maintenir plusieurs modèles, les charger, les router, garder les benchmarks à jour. Quand est-ce que ça en vaut la peine ?

Pour moi, le critère est simple : est-ce que le système fait plusieurs appels de la même structure, plusieurs fois par jour, sur des entrées qui se ressemblent ? Si oui, on est dans la zone où la discipline rapporte plus qu'elle ne coûte. Si non, "un seul gros modèle pour tout" est plus que raisonnable.

Concrètement :

La méthode de bench, elle, est triviale : on prend la tâche réelle (pas une approximation), on la fait tourner sur trois ou quatre modèles candidats, on mesure ce qu'on veut mesurer (taux de réussite, latence, coût). Cinquante lignes de Python suffisent. Le piège n'est jamais technique : il est cognitif, c'est de croire qu'on sait avant d'avoir mesuré. Sur cette histoire de copie, j'aurais juré jusqu'à dix minutes avant le bench que la quantification ou le tokenizer étaient en cause. Les deux étaient faux.

Pour les autres maillons

Pour les autres maillons d'un système d'agents IA, voilà les archis qui dominent les benchmarks publics, à titre de pointeurs (pas de démonstration ici) :

J'entretiens une liste plus complète des modèles open-weight exploitables (licence commerciale, moins de 200B paramètres, sortis après avril 2024) sur github.com/xigh/open-weight-models. Le bench complet sur la copie verbatim, avec les sources et la méthodo détaillée, est sur github.com/xigh/llm-copy-bench. herbert-rs travaille sur l'autre bout de la chaîne : faire tourner ces modèles localement, en Rust et en assembleur, pour ne pas dépendre d'une API.

Le bug que j'ai vu en cliquant sur un lien 404 est un théorème de 2024 que j'aurais pu connaître avant. Une raison de plus pour qu'un assistant agentique sérieux ne repose pas sur un seul modèle pour tous ses maillons.


Des questions sur cet article ou votre propre projet ? Réserver une consultation