AIREITER

329 commandes, 128 encore absentes : auditer une migration avec des ensembles, pas avec un modèle

Dernière mise à jour: 2026-07-31 07:49:12

Après deux cents traductions réussies, il est tentant de considérer une migration comme terminée. Le modèle a produit du code idiomatique dans le nouveau langage, les conventions de nommage sont respectées et les tests passent. Pourtant, ce constat ne dit qu’une chose : les fonctions traitées ont été traduites. Il ne prouve pas que l’ensemble du produit a migré.

La distinction est importante. Valider la traduction d’une fonction est précisément un domaine où un modèle est efficace. En revanche, vérifier que rien n’a été oublié à l’échelle d’une migration est un problème d’ensembles. Et c’est exactement le type de travail qu’il ne faut pas lui déléguer : il peut donner l’impression rassurante d’avoir tout comparé sans avoir produit un résultat exhaustif.

Voici le bilan d’une migration réelle de Go vers Python, et pourquoi seul un script doit établir ce bilan. Le modèle, lui, intervient ensuite pour expliquer les écarts.

Trois chiffres suffisent à poser le problème

Le registre Go contenait 23 plateformes et 329 commandes. Côté Python, l’ensemble dérivé des déclarations argparse partage exactement 201 commandes avec l’ancien registre. Il reste donc 128 commandes présentes uniquement côté Go : elles n’ont été ni migrées en Python, ni conservées sous forme de stub.

329 = 201 + 128. Cette soustraction n’a rien de technique, mais c’est la seule donnée qui répond réellement à la question « la migration est-elle finie ? ». Elle reste invisible lorsqu’on avance fonction par fonction. Une commande absente ne déclenche ni exception, ni erreur, ni test rouge : c’est une absence silencieuse. Elle n’a jamais été envoyée dans la fenêtre de chat, et deux cents validations vertes ne la feront pas apparaître.

Pourquoi il ne faut ni stubs ni proxys de compatibilité

À mi-parcours, la solution la plus séduisante consiste à laisser un emplacement réservé pour les commandes non traitées : un stub raise NotImplementedError, ou un proxy de compatibilité vers l’ancien binaire afin que « le catalogue d’endpoints semble complet ». C’est une mauvaise idée. Une coquille vide coûte davantage qu’un écart assumé, pour trois raisons.

D’abord, un stub fausse le rapprochement. Son nom entre dans l’ensemble côté nouveau système, le diff tombe à 0 et la migration paraît achevée. Un trou est un signal rouge honnête ; un stub transforme « 128 restantes » en un faux « tout est présent ».

Ensuite, un proxy de compatibilité pérennise une dépendance qui aurait dû disparaître. S’il redirige vers l’ancien binaire Go, l’ancien runtime ne pourra jamais être supprimé. Or le but de la migration est justement d’abandonner cette stack. Sous couvert de compatibilité temporaire, le proxy permet à l’ancienne stack de s’installer durablement.

Enfin, un endpoint à moitié construit trompe son appelant. Un Agent comme un humain consulte le catalogue, suppose que la commande fonctionne, l’appelle, puis se heurte à runtime_unavailable — ou pire, à un faux succès qui renvoie silencieusement un résultat vide.

Un écart explicite est donc la solution la moins coûteuse : le diff le signale immédiatement et tout le monde voit ce qu’il reste à faire. C’est le même principe que dans le seuil de preuve en rétro-ingénierie d’applications : déclarer qu’une fonctionnalité n’est pas encore utilisable coûte toujours moins cher que d’en livrer une version inachevée.

Un squelette de script pour le rapprochement

Le principe central tient en une ligne : les ensembles de commandes des deux côtés doivent être générés depuis les déclarations, sans aucune transcription manuelle. Maintenir à la main une liste des éléments « migrés », c’est créer une troisième source de vérité. Elle finira inévitablement par dériver du code, et ce sera le premier point de rupture.

Côté Python, la source de vérité est constituée des déclarations argparse dans le fichier cli.py de chaque plateforme. Un module catalog parcourt les sous-commandes, exporte un ensemble {platform/command} et l’émet via python -m reverse describe --format json. Le fonctionnement d’une déclaration comme source de vérité unique, ainsi que la génération automatique du catalogue, sont détaillés dans l’article sur l’interface as code. Côté Go, le registre est déjà une map platform -> command — une allowlist immuable compilée dans le binaire — et il est trivial d’en exporter un JSON de même forme.

Une fois les deux fichiers JSON disponibles, il ne reste que des opérations sur des ensembles :

# Les ensembles de commandes des deux côtés proviennent des déclarations, pas d'une transcription.
# Une liste écrite à la main ajoute une troisième source de vérité qui finira par dériver.
import json
from collections import Counter

def ids(path):
    doc = json.load(open(path))
    return {f"{p['name']}/{c['name']}"
            for p in doc["platforms"] for c in p["commands"]}

old = ids("go-registry.dump.json")      # ancien registre : allowlist immuable platform->command
new = ids("python-catalog.dump.json")   # python -m reverse describe --format json

missing = old - new     # uniquement côté ancien : chaque élément nécessite une décision conserver ou abandonner
added   = new - old     # uniquement côté nouveau : nouvelle capacité, journalisée séparément
kept    = old & new     # intersection : migré, mais à vérifier pour détecter une dérive sémantique

assert missing | kept == old            # chaque élément de l'ancien côté est classé, aucun n'est perdu

by_platform = Counter(pc.split("/")[0] for pc in missing)  # directement exploitable dans le tableau README

Le calcul prend quelques millisecondes, ne coûte rien, est déterministe et correct à 100 %. missing correspond aux 128 commandes absentes ; regroupées par plateforme, elles donnent ce tableau :

Plateforme

Commandes non migrées

xiaohongshu

33

tiktok

30

hotspot

21

douyin

19

reddit

8

weibo

7

bilibili

5

zhihu

3

linkedin

1

netease_music

1

Total

128

Le modèle n’a aucun rôle à jouer à cette étape.

La fausse bonne idée : faire comparer les listes par un modèle

Si vous sautez le script, collez les deux listes dans une fenêtre de chat et demandez « lesquelles des 329 ne figurent pas parmi ces 201 ? », trois problèmes apparaissent systématiquement.

Le modèle omet des éléments. Face à une longue liste, il ne calcule pas une différence d’ensembles élément par élément : il se contente souvent d’une approximation qui semble plausible. Les éléments situés en fin de liste se diluent, et vous obtenez un rapport apparemment complet auquel il manque une douzaine de commandes. Il en invente aussi : il peut signaler comme absente une commande présente dans les deux ensembles, ou considérer comme migrée une commande réellement manquante. Il reproduit la forme attendue d’un rapport de rapprochement, sans effectuer la différence. Enfin, le résultat n’est pas reproductible : envoyez deux fois les mêmes données, et la liste des manques change. Un rapprochement qui donne un résultat différent à chaque exécution n’est pas un rapprochement.

Le coût ne justifie pas non plus cette approche. Le script s’exécute en quelques millisecondes ; demander la comparaison à un modèle représente plusieurs centaines de milliers de tokens, plusieurs cycles d’auto-vérification, et au final un résultat coûteux, lent et peu fiable. Confier les opérations sur des ensembles à l’outil qui sait les faire est probablement l’affirmation la moins discutable de cet article.

Le bon rôle du modèle : expliquer les écarts, pas décider de la migration

Le script vous livre 128 faits : ces commandes n’ont pas migré. Mais un fait n’est pas une conclusion. Pour chacune, il faut décider de la conserver ou de l’abandonner, et cette décision doit être justifiée. C’est là que le modèle devient utile.

Il peut analyser, commande par commande, la raison de l’absence. S’agit-il de code mort ? D’un endpoint amont retiré ? D’une fonctionnalité reportée ? Ou du cas le plus délicat : la commande n’a pas été supprimée, mais absorbée par une autre. Le nom a disparu tandis que la capacité existe toujours. Cette correspondance cachée — fusionnée, non supprimée — ne se déduit pas de la seule liste des manques. Il faut lire les deux registres simultanément pour la repérer.

Les 201 éléments de l’intersection ne sont pas pour autant à l’abri. Une commande migrée n’a pas nécessairement conservé sa sémantique : une valeur par défaut modifiée discrètement, une sémantique de pagination inversée, deux codes d’erreur fusionnés en un seul. C’est la dérive sémantique, plus insidieuse qu’une absence puisque le diff est vert et que l’élément n’apparaît jamais dans missing. La détecter exige que le modèle lise les deux implémentations et juge l’équivalence des comportements, avant confirmation par tests différentiels — la comparaison de fixtures de la troisième étape du workflow en quatre phases. Cette capacité à regarder une traduction déjà considérée comme réussie et à conclure malgré tout « le comportement a changé ici » est précisément le sujet de la section consacrée aux contre-indices dans l’article sur le fingerprinting. Un modèle faible se contentera d’affirmer que la migration est réussie.

La répartition est donc nette : le script établit si l’élément existe ; le raisonnement sert à déterminer s’il doit rester et si son comportement a changé. Cette fois-ci, 4 commandes signalées en rouge par le diff se sont avérées nécessaires après revue et ont été rétablies comme de véritables nouvelles commandes. Le script constate, le modèle explique, l’humain décide : trois couches, chacune à sa place.

Quel modèle utiliser selon l’étape

Les quatre niveaux ci-dessous appartiennent tous à la couche d’explication. La couche de décision — le diff — n’utilise aucun modèle. C’est ce qui distingue cette approche de la plupart des articles sur les « migrations par IA ».

Étape

Capacité requise

Choix

model id

Lire les deux registres simultanément et repérer les correspondances « fusionné ailleurs, non supprimé »

Contexte long, lecture complète des déclarations des deux côtés

Kimi K3

kimi-k3

Premier tri conserver ou abandonner sur 128 éléments manquants, brouillon structuré

Économique, centaines d’appels en forte concurrence

Claude Sonnet 5

claude-sonnet-5

Évaluer la dérive sémantique : migré, mais comportement modifié ; lecture des deux implémentations

Raisonnement solide, capacité à conclure « ceci a changé »

Claude Opus 5

claude-opus-5

Une commande migrée ne correspond pas à la fixture : expliquer l’écart à partir des paramètres ou de la structure de réponse

Attribution avec raisonnement intermédiaire

GPT-5.6 Sol

gpt-5.6-sol

Le troisième niveau est celui qui mérite le plus d’être testé. L’évaluation de la dérive sémantique répond exactement à la question : « le modèle sait-il contester une traduction déjà jugée réussie ? » C’est aussi l’étape où le changement de modèle modifie le plus le résultat. Voici le protocole :

  1. Prenez une migration réelle entre deux langages dans votre propre codebase et exécutez le script pour obtenir l’ensemble missing. Aucun modèle à cette étape.

  2. Constituez un échantillon de contrôle en annotant vous-même 10 à 15 éléments : abandonner, conserver, fusionné ailleurs ou reporté.

  3. Soumettez exactement le même prompt — expliquer, pour chaque élément, la décision de conservation ou d’abandon — à claude-opus-5 et à un niveau économique. Évaluez deux points : la justification s’appuie-t-elle sur un fait précis du code, ou se limite-t-elle à un vague « probablement déprécié » ? Et combien de correspondances fusionnées ailleurs chaque modèle détecte-t-il ?

  4. Le nombre de correspondances cachées trouvées constitue votre base pour décider si vous pouvez lui confier le premier passage.

Le vrai problème n’est pas le choix du modèle, mais le coût du changement

Quatre modèles, trois fournisseurs, trois SDK et trois schémas d’authentification, sans compter trois formats d’erreurs. Réécrire le client à chaque changement de niveau n’en vaut pas la peine. La plupart des équipes utilisent donc le même modèle de bout en bout ; lors de la revue de dérive sémantique, qui réclame pourtant le niveau de raisonnement, elles emploient un modèle économique qui ne produit que des généralités et laissent passer toutes les dérives pourtant signalées en vert.

AIReiter simplifie cette couche : une seule clé, une interface compatible OpenAI, les quatre niveaux derrière, et un changement de modèle qui se résume au champ model dans le corps de la requête.

# Revue de dérive sémantique / décision conserver ou abandonner par élément : niveau raisonnement
curl https://aireiter.com/api/v1/chat/completions \
  -H "Authorization: Bearer $AIREITER_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "claude-opus-5",
    "messages": [{"role": "user", "content": "<les deux implémentations + le statut de migration de cette commande, demander si le comportement est équivalent>"}]
  }'

# Premier passage en lot sur les 128 éléments manquants : changer uniquement le champ model
#   "model": "claude-sonnet-5"
# Attribution d'un écart lorsqu'une fixture ne correspond pas :
#   "model": "gpt-5.6-sol"

Si vous utilisez déjà le SDK OpenAI, pointez base_url vers https://aireiter.com/api/v1 sans rien modifier d’autre. Avec le SDK Anthropic, utilisez POST /api/v1/messages avec la même clé.

La tarification s’aligne sur ce workflow : le premier passage traite simultanément des centaines d’éléments et est relancé à chaque cycle de migration ; c’est là que claude-sonnet-5, grâce à sa forte concurrence, est le moins cher. La revue de dérive sémantique porte à répétition sur une douzaine de cas difficiles avec claude-opus-5, le plus coûteux par élément. Ces deux niveaux étant des modèles Claude, la réduction de 30 % s’applique exactement aux étapes les plus denses et les plus coûteuses. gpt-5.6-sol sert à attribuer les écarts, avec GPT à moitié prix.

  • Obtenir une clé API

  • Essayer sans inscription : commencez par analyser manuellement quelques éléments manquants pour vérifier s’il repère ceux qui ont été « fusionnés ailleurs », puis décidez si vous souhaitez l’intégrer.

À retenir

« Traduit » est une impression donnée par une fonction isolée. « Migré » se tranche avec un diff. Les opérations sur des ensembles reviennent au script, les explications au modèle et les décisions à l’humain. Cet ordre ne doit pas être inversé, et certainement pas en laissant le modèle décider.

Il reste une étape facile à négliger : la liste des éléments manquants doit figurer dans le README et rester visible dans la durée. Le nombre 128 doit y rester jusqu’à atteindre 0, ou jusqu’à ce que chaque commande porte une justification écrite du type « non migrée, car X ». Un rapprochement enfoui dans une discussion de PR n’en est pas un : la personne qui reprendra le projet ne le verra pas et retombera sur les 128 mêmes problèmes. C’est le terrain commun entre cet article, l’article sur l’interface as code et l’article expliquant pourquoi ne pas construire un Model de réponse unifié : laisser la source de vérité unique s’exprimer d’elle-même, plutôt que d’éparpiller les conclusions dans la mémoire des personnes.