Streaming dans les API d'IA : intégrer les réponses partielles sans les confondre avec des résultats finaux
Image générée par l’équipe éditoriale d’Inferama
01

Le problème de la réponse incrémentale

Recevoir une réponse en streaming signifie que le serveur envoie les informations par fragments séquentiels au fur et à mesure de leur génération, au lieu d'attendre la fin du processus complet. Cette technique réduit la latence perçue, permettant à l'utilisateur de voir le contenu apparaître progressivement, ce qui est particulièrement utile pour les réponses longues ou les processus de raisonnement complexes.

Le principal défi pour le développeur n'est pas seulement d'afficher le texte, mais de gérer l'état de la connexion. Un fragment reçu n'est pas nécessairement une unité sémantique complète et ne garantit pas que la réponse est terminée. L'application doit traiter chaque événement comme provisoire jusqu'à la réception d'un signal explicite de finalisation ou d'une erreur.

Le flux incrémental sépare deux moments que l’interface peut présenter presque simultanément : l’arrivée du contenu et la confirmation que la réponse est terminée. Le client peut afficher chaque fragment pendant que le serveur génère le résultat, ce qui réduit l’attente ressentie, mais cette rapidité visuelle ne transforme pas le contenu partiel en réponse utilisable à toutes fins. L’application doit préserver cette distinction dans son état interne comme dans sa présentation : le texte affiché peut encore évoluer, rester incomplet ou ne jamais être confirmé si une erreur survient avant le signal de fin.

Le signal de finalisation constitue un contrôle du protocole, et non une évaluation de la qualité. Il confirme que le fournisseur a marqué la fin de la transmission conformément au contrat de son API ; à lui seul, il ne prouve ni que le texte est exact, ni qu’il répond à la demande, ni qu’il respecte les règles métier de l’application. Il est donc utile de distinguer deux questions : « Le flux est-il terminé ? » dépend de l’événement terminal documenté ; « Le résultat peut-il être conservé ou utilisé ? » dépend des vérifications propres à l’application.

Le caractère provisoire des événements influence aussi leur accumulation. Un fragment peut ne contenir qu’une partie d’un mot, d’une phrase ou d’une structure que le client ne pourra interpréter qu’après avoir assemblé plusieurs événements. L’application doit respecter l’ordre de réception et éviter toute transformation qui présume que chaque fragment correspond à une unité de sens. Si l’interface applique une mise en forme pendant l’arrivée du texte, ces opérations ne doivent pas modifier le tampon utilisé pour reconstruire le contenu complet.

La baisse de latence ressentie n’entraîne pas nécessairement une réduction du temps total de génération. L’utilisateur peut commencer à lire avant la fin du travail du serveur, tandis que le flux continue. L’intérêt de cet affichage dépend donc aussi de la capacité du client à signaler clairement que le résultat est en cours et à gérer les situations où le flux se termine avec succès, échoue ou s’interrompt sans confirmation.

02

Cycle de vie du stream : états à modéliser

Pour une intégration robuste, le client doit modéliser explicitement les différents états du flux de données. Ces états déterminent les actions que l'application peut effectuer : afficher du texte, enregistrer dans une base de données ou exécuter une logique métier.

Les états critiques incluent la connexion initiale, la réception d'événements partiels, la finalisation réussie, les erreurs intermédiaires et l'annulation. Chacun nécessite une réponse distincte dans l'interface et dans le backend. Par exemple, pendant la transmission, le contenu est visible mais non définitif ; en cas d'erreur, le contenu reçu jusqu'à ce point peut être jetable ou récupérable selon la politique de l'application.

Modéliser explicitement le cycle de vie évite qu’une variable unique comme « chargement » représente des situations différentes. Attendre le premier octet, recevoir du texte partiel et avoir reçu le signal terminal sont des conditions distinctes. Les confondre peut activer trop tôt certaines actions ou laisser des ressources mobilisées après une interruption. L’interface et le backend peuvent réagir différemment à chaque état, mais ils doivent s’appuyer sur une interprétation cohérente des événements du protocole.

L’état de connexion initial mérite d’être séparé de celui de transmission. Tant que le client attend le premier événement, il ne dispose d’aucun contenu à afficher et ne sait pas encore si le serveur a commencé à en produire. Un indicateur de chargement et un timeout permettent de représenter et de limiter cette attente, mais le timeout ne remplace pas le signal de finalisation. S’il expire, l’application doit traiter la situation comme une erreur ou une interruption selon sa politique, et non comme une réponse complète.

Pendant la transmission, le tampon peut s’enrichir à chaque fragment et alimenter l’affichage, mais son contenu reste provisoire. Le conserver en mémoire évite qu’une fermeture inattendue laisse une réponse incomplète enregistrée comme définitive. Si l’application choisit de préserver des fragments partiels pour les récupérer ou les examiner, elle doit les distinguer des résultats terminés et ne pas les présenter comme équivalents. Le traitement dépend alors de la politique de l’application.

Une erreur peut survenir après que l’utilisateur a déjà vu une partie du texte. L’application doit décider à l’avance si elle efface ce contenu visible, le laisse à l’écran avec une indication d’incomplétude ou autorise une reprise lorsque le protocole et l’implémentation le permettent. La réception de texte ne suffit pas à trancher : cette décision dépend de la politique du produit et des garanties propres à l’API. Un nouvel essai ne doit pas non plus être considéré automatiquement comme la continuation exacte de la transmission précédente.

États du cycle de vie et actions autorisées

Tableau de décision pour la gestion des états côté client.

ÉtatDescriptionAction UIAction Backend
ConnexionEn attente du premier octetAfficher un indicateur de chargementDémarrer un minuteur de timeout
En cours de transmissionRéception de fragments partielsAfficher le texte provisoireAccumuler en mémoire (ne pas persister)
TerminéSignal de succès reçuMarquer comme completPersister et exécuter les actions
ErreurÉchec pendant la transmissionAfficher un message d'erreurJeter ou réessayer selon la logique
AnnuléInterruption par l'utilisateurArrêter l'animationNettoyer les ressources et l'état
03

Traitement des événements progressifs

Les fournisseurs d'IA envoient généralement des événements JSON séquentiels. Dans le cas de Vertex AI Search, par exemple, la méthode streamAnswer renvoie une série d'objets où chacun contient un état (comme STREAMING) et des fragments de texte ou des métadonnées. Il est crucial de ne pas supposer que chaque fragment est une phrase complète ; le texte peut être divisé à n'importe quel point, y compris au milieu d'un mot.

L'accumulation doit être strictement séquentielle. Si le protocole le permet, les événements peuvent contenir des métadonnées de référence ou des étapes de raisonnement avant le texte final. La documentation de Google Cloud avertit explicitement qu'il ne faut pas supposer un ordre fixe entre les types d'événements (steps, references, answerText), de sorte que l'analyseur doit être capable de gérer des champs optionnels et absents à chaque itération.

Un analyseur robuste distingue la structure de l’événement du contenu textuel qu’il transporte. À chaque itération, un champ peut être présent, absent ou associé à un autre type d’événement ; le client doit donc vérifier les données reçues avant d’y accéder. Traiter chaque objet comme s’il contenait nécessairement du texte peut provoquer des erreurs lorsque le fournisseur envoie des références, des étapes ou d’autres métadonnées. L’absence d’un champ dans un événement ne signifie pas nécessairement que la réponse est terminée ou que l’événement est invalide.

La séquence doit être reconstruite dans l’ordre de réception, sans exiger que chaque élément constitue une unité linguistique complète. Dans l’exemple, « Span » et « ner is Google's... » forment un texte continu une fois réunis, alors que leurs limites ne correspondent ni à des mots ni à des phrases. Le client peut ajouter chaque fragment au tampon et mettre à jour l’affichage tout en conservant séparément l’état transmis par l’événement. Il évite ainsi de déduire la fin du flux de l’apparence du texte ou du fait qu’un fragment semble clore une phrase.

Les états STREAMING et SUCCEEDED illustrent une distinction propre à la réponse présentée pour Vertex AI Search ; ils ne constituent pas automatiquement des noms d’état universels pour les autres fournisseurs. Le client doit interpréter les valeurs définies par l’API qu’il utilise et déterminer laquelle signale une finalisation réussie. Une chaîne qui paraît complète, ou la réception d’un événement contenant du texte, ne remplace pas cette condition documentée.

La présence de références et d’étapes apporte une autre distinction utile : le flux peut transporter des informations qui ne font pas partie du texte affiché. Le client doit traiter chaque type de donnée selon sa fonction, sans supposer d’ordre fixe entre steps, references et answerText. Même si l’interface n’affiche que le texte, les métadonnées peuvent compter pour la logique de l’application. Les ignorer sans risque suppose de reconnaître leur type et de tolérer qu’elles arrivent à des itérations différentes ou qu’elles soient absentes d’une itération donnée.

04

Conception de l'interface et du backend

Dans l'interface utilisateur, l'objectif est d'afficher le contenu de manière fluide sans que l'utilisateur ne confonde le texte partiel avec le résultat final. Il est recommandé d'utiliser des indicateurs visuels subtils (comme un curseur clignotant ou un texte atténué) tant que l'état est « En cours de transmission ». Une fois l'événement de finalisation reçu, le texte doit se stabiliser et toute interaction ultérieure doit être activée.

Côté backend, la règle d'or est : ne pas persister ni exécuter d'actions avant de valider la finalisation. Si l'application doit enregistrer la réponse dans une base de données ou déclencher une notification, ces opérations doivent être bloquées jusqu'à ce que l'état du stream soit « Terminé ». Cela prévient la corruption des données ou l'exécution de logique basée sur des informations incomplètes.

L’interface peut rendre le progrès visible sans présenter le texte partiel comme définitif. Un curseur, un style atténué ou une indication de transmission peuvent signaler que le contenu est encore susceptible de changer, à condition que l’état visuel soit actualisé à la réception du signal terminal ou d’une erreur. L’indicateur ne doit pas dépendre uniquement de la présence de texte dans le tampon : une déconnexion peut interrompre les événements sans que le fournisseur ait confirmé la réussite. Le message présenté à l’utilisateur doit refléter l’état réel du flux plutôt que de le déduire de l’apparence du contenu.

Une fois le protocole terminé, stabiliser l’affichage signifie que l’application cesse de traiter le texte comme une séquence en cours ; cela ne certifie pas son exactitude. Si le produit propose des interactions ultérieures, il peut les activer après la finalisation du protocole, puis appliquer les validations qui lui sont propres. Cette séparation évite d’interpréter un événement SUCCEEDED comme une garantie de justesse sémantique ou comme une assurance que toute opération suivante est sûre.

Le tampon en mémoire forme une frontière entre le contenu provisoire et la persistance définitive. Avant l’enregistrement, le backend peut vérifier qu’il a reçu l’événement terminal attendu et que la séquence accumulée peut être traitée conformément aux règles du système. Si la transmission se termine par une erreur ou si la connexion se ferme sans ce signal, le processus ne doit pas traiter le contenu partiel comme s’il avait satisfait la condition de finalisation. Le choix de le préserver pour une récupération éventuelle est distinct de celui de l’enregistrer comme réponse complète.

Les actions secondaires dépendent elles aussi du moment où le résultat est confirmé. Une notification ou une métrique annonçant une réponse terminée serait trompeuse si elle était déclenchée dès le premier fragment ; lorsque l’action représente une opération achevée, elle doit donc attendre la condition de finalisation. Le backend peut toutefois enregistrer séparément les erreurs ou les annulations si sa conception le prévoit, sans confondre ces traces opérationnelles avec la persistance d’une réponse complète.

Flux de validation avant persistance

Étapes pour garantir l'intégrité des données avant leur enregistrement.

  1. 01Recevoir le fragment de texte et l'ajouter au buffer en mémoire.
  2. 02Mettre à jour l'UI avec le nouveau contenu provisoire.
  3. 03Vérifier si l'événement contient le signal de finalisation (ex. state: SUCCEEDED).
  4. 04S'il ne s'agit pas d'une finalisation, attendre l'événement suivant.
  5. 05S'il s'agit d'une finalisation, valider l'intégrité du buffer accumulé.
  6. 06Persister le résultat complet dans la base de données.
  7. 07Exécuter les actions secondaires (notifications, métriques).
05

Tests et limites de l'intégration

Les tests automatisés doivent couvrir les scénarios de fragmentation variable, de coupures de connexion et d'annulations. Il est essentiel de vérifier que l'application n'enregistre pas de données si la connexion est fermée sans le signal de finalisation. Il faut également tester le comportement en cas d'erreurs intermédiaires : tout le contenu reçu est-il jeté ou conservé partiellement ?

Le streaming améliore l'expérience de livraison incrémentale, mais ne garantit pas la correction du contenu ni ne réduit nécessairement le temps total de génération. De plus, le comportement en cas d'annulation peut varier : chez certains fournisseurs, la connexion est fermée immédiatement, tandis que chez d'autres, une fermeture propre est autorisée. Ces détails doivent être vérifiés dans la documentation primaire de chaque API spécifique, car il n'existe pas de contrat universel pour toutes les API d'IA.

Les tests doivent vérifier toute la séquence des états, et pas seulement l’apparition du texte à l’écran. Une réponse peut arriver en fragments de tailles variables et être coupée à des endroits inattendus, y compris au milieu d’un mot ; le résultat assemblé doit respecter l’ordre reçu et correspondre à la concaténation des fragments. Il est également utile de vérifier que l’interface conserve l’indication de caractère provisoire jusqu’à l’événement terminal et qu’elle n’active pas avant celui-ci les opérations réservées à une réponse terminée.

Les scénarios de fermeture sans signal final permettent de détecter une confusion fréquente entre « la connexion s’est arrêtée » et « le fournisseur a confirmé la réussite ». Si le transport est interrompu alors que le client attend d’autres événements, l’application doit enregistrer un état d’erreur ou d’annulation correspondant à ce qu’elle peut réellement établir. L’absence de nouvelles données ne prouve ni que le dernier fragment est complet, ni que le contenu accumulé peut être conservé comme résultat final.

Les tests portant sur les erreurs intermédiaires doivent définir le traitement attendu du contenu déjà reçu : le supprimer, le garder visible avec une indication d’incomplétude ou le préserver d’une autre manière autorisée par l’application. Aucune de ces options ne s’impose dans tous les cas. Le comportement à vérifier est que l’application ne confond pas ce contenu avec une transmission confirmée et que le backend applique la même politique que celle communiquée par l’interface.

L’annulation nécessite également des vérifications particulières, car interrompre l’expérience de l’utilisateur et fermer effectivement la connexion ne correspondent pas toujours à la même opération. Selon l’API et son implémentation, le client peut cesser d’afficher la génération alors que la requête sous-jacente est encore en cours de fermeture. Les tests doivent observer l’état que le système peut confirmer et libérer les ressources locales sans présumer d’un mécanisme de fermeture identique chez tous les fournisseurs. La documentation primaire de l’API concernée détermine les signaux et comportements auxquels le client peut s’attendre.

06

Poursuivre l’exploration

Sources consultées

03

Corrections et transparence

Si vous repérez une information incorrecte ou obsolète, envoyez-nous la page et la source à vérifier.

Proposer une correction