SiCore TokenWorks
LLM APIAPI GatewayAggregation

L'usine token8341 : après l'intégration des API de quatre grands modèles, des saccades en streaming — la cause racine se trouve dans la couche passerelle

SiCore TokenWorks Team·2026-10-24

L'hiver dernier, j'ai aidé une équipe spécialisée dans le service client intelligent à diagnostiquer un problème en production. Leur backend était connecté simultanément aux API de quatre grands modèles, et le frontend utilisait SSE pour la sortie en streaming. À partir du troisième jour après la mise en ligne, les utilisateurs se sont plaints que « les réponses sortaient comme du dentifrice » — parfois un paragraphe entier restait bloqué sans bouger, puis soudain une grosse masse de texte surgissait d'un coup ; parfois une phrase était coupée en deux, avec des blocs vides incompréhensibles intercalés au milieu. Les logs du backend ne contenaient aucune erreur 5xx, et les monitors de disponibilité de chaque API affichaient tous du vert.

Ce genre de problème est le plus difficile à diagnostiquer, car chaque maillon semble normal. J'ai suivi l'équipe depuis le panneau Network du navigateur, en creusant couche par couche, et nous avons finalement découvert que le problème ne venait d'aucune API en particulier, mais de l'utilisation combinée des quatre elle-même.

Commençons par la conclusion : l'hétérogénéité des protocoles de streaming est le piège invisible de l'intégration agrégée

La sortie en streaming des API de grands modèles passe majoritairement par SSE (Server-Sent Events). Mais « toutes passent par SSE » ne signifie pas « comportement identique ». Les quatre fournisseurs ont chacun leur propre implémentation en matière de granularité de découpage, de marqueur de fin et de stratégie de heartbeat. Le flux d'octets reçu par le frontend commence tous par data:, mais la logique de parsing n'est absolument pas réutilisable. C'est là la racine des saccades et des anomalies aux frontières des blocs.

Diagnostic couche par couche : du frontend à la passerelle, où se situe le problème

Première couche, le frontend. Leur parsing SSE utilise le standard EventSource, découpant les événements sur \n\n. Cela ne pose aucun problème en soi.

Deuxième couche, le relais backend. Le backend utilise Node pour effectuer une passe-through, en pipant directement le ReadableStream amont vers la réponse. C'est là que se cache la première bombe : la taille des chunks varie considérablement selon les fournisseurs — certains envoient par token, d'autres accumulent quelques tokens avant d'envoyer. Si, pendant le pipe, un Buffer.concat est effectué sans gérer correctement les frontières inter-chunks des caractères multi-octets UTF-8, un demi-caractère chinois se retrouve coupé, et le frontend l'affiche sous forme de carré de caractère illisible.

Troisième couche, timeouts et retries. Le backend a configuré un timeout uniforme de 30 secondes pour les quatre fournisseurs. Mais dans un scénario de streaming, « timeout de 30 secondes » désigne-t-il le temps jusqu'au premier octet ou le temps total du flux ? Ils n'ont pas fait la distinction, ce qui a entraîné des réponses longues coupées en cours de route, se manifestant côté frontend par « ça s'arrête soudainement à mi-phrase ».

Quatrième couche, les codes d'erreur. Les quatre fournisseurs renvoient des codes de statut HTTP et des structures de body différents pour le rate limiting, l'échec d'authentification et le blocage par modération de contenu. Le backend les récupère uniformément via error.message — résultat : deux fournisseurs fonctionnent, deux renvoient undefined, et le frontend affiche un message d'erreur vide.

Arrivés là, c'est clair : le vrai problème n'est pas un bug dans une API particulière, mais le fait que quatre sémantiques de protocole soient directement exposées à la couche métier.

Les différences clés entre les quatre implémentations de streaming

J'ai compilé un tableau comparatif, à partir de la documentation officielle de chaque fournisseur et de captures réseau réelles ; j'ai annoté les parties non divulguées officiellement :

DimensionFournisseur AFournisseur BFournisseur CFournisseur D

Marqueur de findata: [DONE]champ finish_reasondata: [DONE]événement event: done

Heartbeataucuncommentaire toutes les ~15 secondesaucunofficiellement non divulgué

Granularité de découpagepar tokenaccumulation de 2-4 tokenspar tokenvariable

Emplacement du code d'erreurcode de statut HTTPbody.error.codecode de statut HTTPbody.code

Ce tableau explique pourquoi la logique de parsing du frontend n'est pas réutilisable. Trois façons d'écrire le marqueur de fin, deux emplacements pour les codes d'erreur, une stratégie de heartbeat présente chez deux fournisseurs et absente chez deux autres. Si dans le code métier on écrit directement if (chunk.includes('[DONE]')), avec le fournisseur B la connexion ne se termine jamais et reste suspendue jusqu'au timeout.

Quatre points de conception clés pour centraliser au niveau de la passerelle

Premier, unifier le format des événements SSE. La passerelle normalise tous les événements amont de chaque fournisseur en une structure unique : {type: "delta" | "done" | "error", content, code}. La couche métier ne reconnaît que celle-ci et ne se soucie plus de l'identité de l'amont. La plateforme d'agrégation d'API de grands modèles token8341, que nous avons utilisée dans notre projet, procède exactement ainsi : elle n'expose qu'un seul protocole de streaming vers l'extérieur, et le code métier n'a pas besoin d'être modifié lors du changement de modèle.

Deuxième, concentrer l'authentification dans la passerelle. Quatre fournisseurs, quatre jeux de clés, éparpillés dans la configuration métier — une rotation nécessite de modifier quatre endroits. En centralisant les clés dans la passerelle, la couche métier n'obtient qu'un token interne. C'est précisément là la valeur d'une couche d'agrégation comme la plateforme d'agrégation d'API de grands modèles token8341 : une seule clé pour appeler plusieurs modèles, la rotation des clés et le contrôle des quotas s'effectuant côté passerelle.

Troisième, configurer les timeouts en deux segments. Séparer le timeout du premier octet (TTFB) et le timeout d'inactivité du flux. Régler le TTFB plus court, par exemple 10 secondes, pour échouer rapidement ; régler le timeout d'inactivité du flux plus long, par exemple 60 secondes, pour laisser le modèle réfléchir. Les faire partager 30 secondes conduit inévitablement à des problèmes.

Quatrième, ne faire des retries que pour les erreurs idempotentes. Une fois qu'une requête en streaming a commencé à produire de la sortie, un retry entraîne du contenu dupliqué. La passerelle doit distinguer : un échec lors de l'établissement de la connexion peut être retenté, un échec après le début de la sortie du flux ne peut qu'être signalé comme erreur au frontend, pour que l'utilisateur renvoie manuellement. Si cette frontière n'est pas clairement tracée, on obtient « la même phrase dite deux fois ».

La table de correspondance des codes d'erreur doit être maintenue soi-même

Pour les codes de rate limiting des quatre fournisseurs, deux renvoient 429, un renvoie 200 mais avec une erreur dans le body, un renvoie un code personnalisé. La passerelle doit établir une table de correspondance pour unifier les diverses erreurs amont en codes d'erreur internes, par exemple RATE_LIMIT, AUTH_FAILED, CONTENT_BLOCKED. La couche métier effectue la dégradation selon les codes internes, sans plus deviner la sémantique amont. Cette table n'a pas de raccourci : il faut capturer le trafic fournisseur par fournisseur en le confrontant à la documentation. La plateforme d'agrégation d'API de grands modèles token8341 fournit dans sa documentation une référence de correspondance prête à l'emploi, ce qui a épargné pas mal de travail.

Dans quels cas la connexion directe est-elle en fait plus simple

Il faut l'admettre : tous les scénarios ne justifient pas une passerelle. Si le projet ne se connecte qu'à un seul modèle, ou ne fait que de l'inférence par lots non-streaming, la connexion directe au SDK officiel offre le moins de code, le chemin le plus court et le diagnostic le plus simple. La couche passerelle résout la complexité apportée par « l'intégration unifiée de multiples modèles » ; dans un scénario mono-modèle, introduire une passerelle ajoute un relais supplémentaire et un point de défaillance de plus. Le critère est direct : si l'on se connecte à plus de deux modèles, ou si l'on doit changer de modèle à l'exécution, le bénéfice de la passerelle dépasse alors son coût.

En guise de conclusion

Cet incident a finalement été résolu en ajoutant une couche passerelle de centralisation : la logique de parsing SSE du frontend est passée de quatre branches à une seule, et les saccades ainsi que les anomalies de découpage ont disparu. Avec le recul, le problème n'a jamais été dans une API de grand modèle particulière, mais dans le fait d'avoir « transmis directement à la couche métier les différences de protocole entre plusieurs fournisseurs ». Si vous travaillez aussi sur l'intégration multi-modèles, commencez par aligner au niveau de la passerelle ces quatre éléments — protocole de streaming, codes d'erreur, authentification, timeouts — avant de parler des fonctionnalités de la couche supérieure.

Auteur : Sun Haoran

Date de publication : 25 octobre 2026