Face à des données publiques issues d’une vingtaine de plateformes, le réflexe est presque automatique : dessiner un Post unique, un User unique, puis faire entrer chaque réponse API dans ces modèles. Après tout, une vidéo bilibili, une vidéo tiktok, une réponse zhihu ou une publication linkedin semblent toutes être « un contenu avec un auteur ». L’idée paraît évidente — et fonctionne plutôt bien pour les trois premières plateformes. À la vingtième, elle devient étouffante.
Au final, je n’ai donc pas créé ce modèle unifié. Sur plus de vingt plateformes et plus de deux cents commandes, l’organisation apparemment moins ambitieuse, où chaque plateforme gère son propre périmètre, est celle qui a tenu.
À quel moment le modèle unifié cesse d’aider
L’effondrement ne se produit pas d’un coup. Vers la huitième plateforme, votre Post traîne déjà une douzaine de champs optionnels : l’une expose un nombre de danmaku, une autre non ; la date de publication est parfois un timestamp à la seconde, parfois une chaîne comme « il y a 3 jours ». Au-delà de vingt plateformes, le modèle ne rend plus service. Ce n’est pas une erreur de compilation qui le trahit : c’est le fait que chaque consommateur des données doit d’abord déterminer si la plateforme a seulement renseigné tel champ. La logique de décision devient plus longue que la lecture de la réponse brute, et la couche unifiée finit par être un obstacle qu’il faut contourner pour avancer.
À l’écriture, le coût est le même. Chaque nouvelle intégration oblige à revenir dans ce modèle central pour faire entrer de nouveaux champs dans des cases conçues autour des plateformes précédentes.
Une plateforme, un contexte borné
Le découpage qui résiste consiste à faire exactement l’inverse : ne pas chercher un modèle global et laisser chaque plateforme gérer son domaine. Dans le catalogue, chaque plateforme correspond à un contexte <platform>_reverse/, propriétaire de quatre responsabilités qu’elle ne partage pas :
Validation des entrées. Seule la plateforme sait à quoi ressemblent ses identifiants et quelles combinaisons de paramètres sont valides.
Protocole. Requête HTTP directe ou fragment de signature JS locale, domaine cible, en-têtes : tout cela relève de sa logique privée.
Signature. Les mécanismes de signature varient énormément d’une plateforme à l’autre. Les entasser dans un signer commun ne produit qu’un monstre rempli de conditions.
Normalisation des réponses. La réponse brute est remise en forme dans une structure détenue par la plateforme, pas dans un schéma global.
Le quatrième point est celui qui prête le plus facilement à confusion. « Pas de modèle unifié » ne veut pas dire « pas de normalisation ». Chaque plateforme normalise évidemment ses données, mais elle définit elle-même sa structure cible plutôt que de se plier à un modèle partagé. L’unification reste pertinente lorsqu’elle correspond à un objet réellement identique : deux endpoints d’une même plateforme qui partagent une structure de publication peuvent parfaitement la mutualiser. L’erreur consiste à étendre cette unification interne à l’ensemble des plateformes.
La couche commune ne garde que les vrais invariants
Que faut-il placer dans la couche partagée ? Des capacités qui se comportent réellement de la même manière partout, pas des éléments qui ne font que se ressembler. Dans mon cas, elle ne contient que trois choses :
Le read-model de l’interface : un catalogue unifié des capacités est dérivé des déclarations argparse propres à chaque plateforme. Il unifie la découverte et la description des commandes, pas leurs retours. Le premier sujet est véritablement multiplateforme ; le second reste privé à chaque plateforme. (L’idée selon laquelle « la déclaration fait l’interface » est détaillée dans l’article sur l’interface-as-code.)
Le transport local loopback : les requêtes authentifiées passent par un service de session WebSocket local qui traite toutes les plateformes de la même façon, sans toucher à aucun de leurs champs métier.
Le point d’entrée de dispatch : il identifie la plateforme, lui transmet la commande dans son contexte, puis s’arrête là.
Le critère est simple : pour entrer dans la couche partagée, un composant doit se comporter de façon réellement identique sur chaque plateforme. Le transport, le dispatch et la génération des descriptions d’interface satisfont cette condition. En revanche, « un contenu » ne se comporte pas du tout de la même manière sur bilibili et linkedin ; il n’a donc rien à faire là. Les choses qui se ressemblent constituent le plus grand piège de l’abstraction : deux vidéos paraissent semblables, donc on veut les unifier. Mais une ressemblance de surface n’est pas une identité comportementale, et la confondre avec un domaine partageable est à l’origine de l’échec du modèle unifié.
La répartition des commandes montre où l’abstraction est rentable
Vous hésitez encore à abandonner le modèle unifié ? Regardez la distribution réelle des commandes. Il y a 22 plateformes et 241 commandes, réparties de manière très inégale :
Plateforme | Commandes |
|---|---|
tiktok | 34 |
bilibili | 26 |
18 | |
zhihu | 18 |
douyin | 17 |
xiaohongshu | 16 |
Les 16 autres plateformes | 1 à 13 chacune |
Les six premières plateformes cumulent 129 commandes, soit plus de la moitié du total. L’autre moitié se répartit entre 16 plateformes de longue traîne, dont beaucoup n’ont que deux ou trois commandes, et certaines une seule.
Cette distribution détermine l’économie de l’abstraction. Le coût d’un modèle unifié est fixe : chaque intégration doit renseigner des champs, gérer les valeurs nulles et contourner ses limites. Son bénéfice, lui, se répartit plateforme par plateforme. Pour une plateforme de longue traîne avec deux ou trois commandes, le bilan devient négatif : le code adaptateur nécessaire pour l’ajuster au modèle unifié prend plus de place que tout son code métier.
Ne pas abstraire pour une seconde implémentation hypothétique
Cette distribution conduit à une autre règle : ne réservez pas une abstraction pour une implémentation unique. Lorsqu’une plateforme n’a qu’une implémentation, inutile d’ajouter un repository, une factory ou une couche d’interface au cas où une autre apparaîtrait plus tard. Ajouter une plateforme doit simplement consister à ajouter un contexte <platform>_reverse/, sans devoir modifier une classe de base partagée au préalable.
La valeur d’une couche d’interface est de rendre plusieurs implémentations interchangeables. Avec une seule implémentation, sa valeur est nulle tandis que son coût de maintenance est réel. Prévoir une deuxième implémentation inexistante et prévoir un dénominateur commun multiplateforme inexistant relèvent de la même erreur. La migration inter-langages l’a confirmé : quelques centaines de commandes de l’ancien registre sont restées explicitement non migrées, sans stubs ni proxys de compatibilité, car une coquille vide coûte plus cher qu’un manque ; elle laisse croire au suivant que quelque chose existe. Une abstraction réservée produit le même effet.
Avec un modèle, normaliser plateforme par plateforme
Cette logique — séparer par plateforme et ne pas imposer de modèle unifié — vaut aussi lorsqu’un modèle sert à normaliser les données. Pour transformer les réponses brutes de plus de vingt plateformes en structures exploitables pour l’analyse, il est tentant de les confier à un modèle. L’erreur la plus facile consiste alors à reproduire celle de la couche de code : définir un schéma universel, envoyer le JSON brut de chaque plateforme et lui demander de le mapper vers ce schéma.
Cette approche échoue, car le modèle ne sait pas si le champ de nombre de lectures de bilibili et celui de tiktok désignent réellement la même chose. Le forcer dans un schéma du plus petit dénominateur commun le conduit soit à supprimer un champ dont la plateforme dépend, soit à le remplir de manière approximative.
La bonne méthode est de fournir le contexte propre à chaque plateforme : « voici bilibili, voici ce que signifient ses champs, voici la structure que je veux pour cette plateforme ». On normalise une plateforme à la fois, et l’agrégation multiplateforme reste l’affaire de la couche d’analyse. Le processus se découpe en plusieurs étapes, chacune demandant une capacité différente au modèle :
Étape | Capacité requise | Choix | model id |
|---|---|---|---|
Lire la structure complète de la réponse brute d’une plateforme | Contexte long, capable d’absorber à la fois la réponse complète et les notes sur les champs | Kimi K3 |
|
Définir la frontière de normalisation : quels champs sont réellement multiplateformes, lesquels sont spécifiques | Raisonnement solide, résistant à la sur-unification | Claude Opus 5 |
|
Extraire les champs par plateforme à grande échelle, élément par élément | Économique, avec des centaines à des milliers d’appels en forte concurrence | Claude Sonnet 5 |
|
Expliquer pourquoi des champs portant le même nom ne correspondent pas entre deux plateformes | Raisonnement intermédiaire, capable d’expliquer les écarts à partir des champs | GPT-5.6 Sol |
|
La deuxième étape est la seule où le changement de modèle modifie visiblement le résultat. Elle vérifie votre capacité à reconnaître que deux champs ne désignent pas réellement la même chose, exactement comme dans la section sur les contre-indices dans l’identification des familles d’algorithmes : un modèle faible suit votre consigne d’« unifier », tandis qu’un modèle solide signale la frontière.
Le vrai frein, c’est le coût du changement de modèle
Ces quatre niveaux font intervenir trois fournisseurs, trois SDK, trois systèmes d’authentification et trois formats d’erreur. Réécrire son client trois fois pour changer de modèle entre les étapes n’en vaut pas la peine. La plupart des équipes utilisent donc le même niveau tout au long du pipeline, souvent un niveau qui ne fait qu’aplatir les différences à l’étape de définition des frontières — et elles construisent alors un schéma qui s’effondre de nouveau au-delà de vingt plateformes.
AIReiter aplanit cette couche : une clé, une interface compatible OpenAI, les quatre niveaux derrière, et un changement de modèle qui se résume au champ model du corps de requête.
# Set the normalization boundary: the reasoning tier
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": "<one platform response sample + have it mark which fields are platform-specific>"}]
}'
# Extract fields per platform in bulk: change the model field, leave the rest
# "model": "claude-sonnet-5"
# Field-difference attribution:
# "model": "gpt-5.6-sol"
Si vous utilisez déjà le SDK OpenAI, pointez simplement base_url vers https://aireiter.com/api/v1, sans rien changer d’autre. Avec le SDK Anthropic, utilisez POST /api/v1/messages avec la même clé.
Côté prix, les modèles Claude bénéficient d’une remise de 30 % sur le tarif catalogue, les modèles GPT sont à moitié prix, et Kimi K3 est accessible avec cette même clé. La remise s’applique précisément là où se concentre la dépense : l’extraction massive des champs par plateforme est l’étape la plus dense en appels, avec plus de vingt plateformes comptant chacune des centaines à des milliers d’enregistrements, un appel par enregistrement, exécuté sur le Sonnet le moins cher avec 30 % de réduction supplémentaire. Lire une réponse longue complète avec Kimi K3 représente quelques centaines de milliers de tokens par entrée, soit un autre poste de coût. L’étape de raisonnement qui fixe les frontières demande peu d’appels et coûte donc très peu.
Essayer sans inscription : commencez par soumettre manuellement la réponse d’une plateforme pour vérifier si le modèle signale honnêtement les différences ou s’empresse de les aplatir, puis décidez s’il mérite d’être intégré.
Conclusion
Dans la collecte multiplateforme, le premier réflexe — abstraire un Post/User unifié — paraît séduisant à petite échelle, puis s’effondre inévitablement au-delà de vingt plateformes : son coût est fixe, son bénéfice est distribué par plateforme, et les commandes suivent une distribution de longue traîne très marquée. Le découpage qui tient est un contexte borné par plateforme, propriétaire de la validation des entrées, du protocole, de la signature et de la normalisation des réponses. La couche partagée ne garde que ce qui se comporte réellement de façon identique partout — transport, dispatch, génération des descriptions d’interface — et non un modèle métier qui ne fait que se ressembler. Elle ne réserve pas non plus d’abstraction pour une implémentation unique ou une communauté qui n’existe pas.
Avec un modèle, le principe est identique : normalisez avec le contexte de chaque plateforme, n’imposez pas un schéma unifié, et ne fusionnez les données entre plateformes qu’au niveau de l’analyse. Le workflow complet en quatre étapes détaille davantage cette répartition entre les quatre niveaux. Lorsqu’ils sont reliés par une interface unifiée, le coût du changement de modèle ne constitue plus une raison de s’en priver.