Aetherio

Streaming IA côté frontend : implémenter l'UX de réponse progressive (Vercel AI SDK)

12 minutes min de lecture

Partager l'article

Introduction

Un utilisateur pose une question complexe à une intelligence artificielle. Face à un écran figé durant de longues secondes, son expérience se dégrade, l'incertitude monte et le risque d'abandon augmente. C'est précisément là que le streaming IA côté frontend intervient, transformant radicalement la manière dont les utilisateurs interagissent avec les modèles de langage de grande taille (LLM).

Chez Aetherio, votre partenaire pour le développement d'applications web sur-mesure à Lyon, nous assistons à une révolution dans l'UX des interfaces pilotées par l'IA. Finis les délais d'attente interminables : le streaming permet d'afficher les réponses des LLM caractère par caractère ou mot par mot, offrant une UX de réponse progressive qui captive l'attention et simule une conversation humaine naturelle. Sur une interface conversationnelle, c'est désormais le comportement que les utilisateurs attendent. Dans ce guide complet, nous allons explorer pourquoi et comment implémenter cette technologie essentielle, en nous concentrant sur l'efficacité et la simplicité du Vercel AI SDK dans des environnements modernes comme Next.js et Nuxt.js. Que vous cherchiez à intégrer l'IA dans vos applications web ou à améliorer un chatbot existant, cet article vous fournira les connaissances et les outils pratiques pour y parvenir.

Implémentation du streaming IA côté frontend avec Vercel AI SDK

Pourquoi le streaming IA est un impératif pour l'expérience utilisateur (UX)

L'intégration de l'Intelligence Artificielle (IA), et particulièrement des modèles de langage avancés (LLM), dans les applications web a explosé. Cependant, la complexité de ces modèles peut entraîner des latences significatives. Une réponse qui prend 5 à 10 secondes à se charger entièrement peut sembler une éternité pour l'utilisateur, même si le traitement est bien réel en arrière-plan. L'expérience utilisateur est primordiale pour la rétention et l'engagement, surtout dans un contexte où la satisfaction est directement liée à la fluidité et à la réactivité de l'interface.

L'impact psychologique de la réponse progressive

Le streaming IA frontend résout ce problème en affichant la réponse de l'IA en temps quasi réel, à mesure qu'elle est générée. Plutôt qu'une attente passive, l'utilisateur voit le texte apparaître progressivement, ce qui crée une illusion de vitesse supérieure et maintient son attention. Cette méthode est similaire à la façon dont un humain compose une réponse, rendant l'interaction plus naturelle et moins frustrante.

Le streaming des réponses réduit la friction et rend l'interaction plus dynamique. Pour les applications métier ou les plateformes SaaS avec des fonctionnalités IA critiques, cette fluidité est un avantage concurrentiel majeur.

Quand opter pour le streaming IA ?

Le streaming IA est particulièrement pertinent pour les cas d'usage où la rapidité de la première interaction est cruciale :

  • Chatbots et assistants virtuels : C'est le cas d'usage le plus évident, où une conversation en temps réel est attendue. Le fait de voir les mots apparaître un par un rend l'échange beaucoup plus engageant.
  • Génération de contenu : Lorsque l'IA génère des rapports, des articles ou des résumés, pouvoir visualiser le contenu au fur et à mesure de sa création est un plus.
  • Recherche sémantique avancée : Pour les moteurs de recherche qui utilisent l'IA pour synthétiser des réponses, le streaming permet à l'utilisateur de commencer à lire les premiers résultats tandis que le système affine les suivants.

Pour des conseils plus approfondis sur la conception d'expériences utilisateur performantes, consultez notre page consacrée au design UI/UX.

Implémentation du streaming IA avec Vercel AI SDK

Le Vercel AI SDK s'est imposé comme un outil incontournable pour les développeurs souhaitant implémenter le streaming IA côté frontend de manière efficace et avec un minimum d'effort. Il est conçu pour fonctionner nativement avec les frameworks modernes comme Next.js (React) et Nuxt.js (Vue.js), simplifiant la communication avec les API de LLM (OpenAI, Anthropic, Google Gemini, etc.) et la gestion du flux de données en streaming.

Principes de base du Vercel AI SDK

Le SDK gère les complexités du streaming en coulisses, vous permettant de vous concentrer sur l'interface utilisateur. Il fournit des hooks ou des composables (selon le framework) pour recevoir les fragments de texte générés par le LLM et les afficher progressivement. L'architecture est généralement la suivante :

  1. Requête Frontend : L'utilisateur envoie une requête depuis l'interface.
  2. API Route / Serveur Backend : La requête est transmise à une route API (souvent une fonction Edge ou Serverless) qui interagit avec le LLM.
  3. Appel LLM : Le backend appelle l'API du LLM avec le mode streaming activé.
  4. Streaming Retour : Le LLM envoie des "chunks" (fragments de données) de la réponse. Le backend les relaye au frontend.
  5. Affichage Frontend : Le Vercel AI SDK intercepte ces chunks et met à jour l'interface utilisateur en temps réel.

Cela permet une UX réponse IA progressive sans que vous ayez à manipuler directement les flux ReadableStream complexes.

Exemple d'implémentation avec Next.js

Pour Next.js, le Vercel AI SDK offre l'intégration la plus fluide. Commençons par installer le SDK :

npm install ai @ai-sdk/react @ai-sdk/openai

Les exemples de cet article utilisent la version 6 du SDK. L'API côté interface a beaucoup changé avec la version 5 (messages composés de parts, sendMessage à la place de handleSubmit) : méfiez-vous des tutoriels plus anciens, dont le code ne compile plus.

La route API qui streame la réponse

Côté serveur, streamText appelle le modèle en mode streaming et toUIMessageStreamResponse transforme le flux en réponse HTTP que le hook du frontend sait lire (des Server-Sent Events).

// app/api/chat/route.ts
import { openai } from '@ai-sdk/openai';
import { convertToModelMessages, streamText, type UIMessage } from 'ai';

// Durée maximale d'exécution de la fonction sur Vercel : une longue réponse ne doit pas être coupée.
export const maxDuration = 30;

export async function POST(req: Request) {
  const { messages }: { messages: UIMessage[] } = await req.json();

  const result = streamText({
    model: openai('gpt-4o-mini'),
    system: "Tu es l'assistant du support client. Réponds en français, de façon concise.",
    messages: await convertToModelMessages(messages),
    maxOutputTokens: 1000,
    // Si l'utilisateur clique sur "Arrêter" ou ferme la page, l'appel au fournisseur est annulé aussi.
    abortSignal: req.signal,
  });

  return result.toUIMessageStreamResponse({
    // Le SDK masque le détail des erreurs côté client : on journalise et on renvoie un message lisible.
    onError: (error) => {
      console.error('Erreur pendant le streaming', error);
      return 'La réponse a été interrompue. Vous pouvez réessayer.';
    },
  });
}

Cette route est volontairement minimale. En production, elle doit être authentifiée et protégée par un rate limiting et des quotas, et elle ne doit pas faire confiance à l'historique envoyé par le navigateur : un client malveillant peut y glisser de faux messages d'assistant ou de système. La bonne pratique consiste à stocker la conversation côté serveur et à n'accepter du client que le dernier message de l'utilisateur.

Le composant client avec useChat

Le hook useChat gère l'envoi, la réception des fragments, l'état de la conversation et l'annulation. Son status passe par quatre valeurs : submitted (requête envoyée, aucun token reçu), streaming (le texte arrive), ready et error. C'est lui qui pilote toute l'UX.

// app/chat/page.tsx
'use client';

import { useChat } from '@ai-sdk/react';
import { useState } from 'react';

export default function ChatPage() {
  const [input, setInput] = useState('');
  // Sans configuration, useChat envoie les messages vers /api/chat.
  const { messages, sendMessage, status, stop, error, regenerate } = useChat();
  const busy = status === 'submitted' || status === 'streaming';

  return (
    <div>
      <div aria-live="polite" aria-busy={busy}>
        {messages.map((message) => (
          <div key={message.id} className={message.role === 'user' ? 'msg-user' : 'msg-ai'}>
            {message.parts.map((part, i) => (part.type === 'text' ? <span key={i}>{part.text}</span> : null))}
          </div>
        ))}
        {status === 'submitted' && <p className="typing">L'assistant réfléchit…</p>}
      </div>

      {error && (
        <div role="alert">
          La réponse n'a pas pu aboutir.{' '}
          <button type="button" onClick={() => regenerate()}>Réessayer</button>
        </div>
      )}

      <form
        onSubmit={(e) => {
          e.preventDefault();
          if (!input.trim() || busy) return;
          sendMessage({ text: input });
          setInput('');
        }}
      >
        <input value={input} onChange={(e) => setInput(e.target.value)} placeholder="Posez votre question" />
        {busy ? (
          <button type="button" onClick={() => stop()}>Arrêter</button>
        ) : (
          <button type="submit" disabled={!input.trim()}>Envoyer</button>
        )}
      </form>
    </div>
  );
}

Chaque message contient une liste de parts : du texte, mais aussi, selon la configuration, des appels d'outils, du raisonnement ou des fichiers. Ce découpage permet d'afficher, par exemple, un indicateur « recherche dans la base documentaire » pendant qu'un agent appelle un outil, puis le texte de la réponse.

Exemple d'implémentation avec Nuxt

Le SDK fournit un paquet dédié à Vue. Côté Nuxt, la route vit dans server/api et la clé d'API reste côté serveur grâce à runtimeConfig.

npm install ai @ai-sdk/vue @ai-sdk/openai
// server/api/chat.post.ts
import { createOpenAI } from '@ai-sdk/openai';
import { convertToModelMessages, streamText, type UIMessage } from 'ai';

export default defineLazyEventHandler(() => {
  // runtimeConfig.openaiApiKey, alimentée par la variable d'environnement NUXT_OPENAI_API_KEY
  const openai = createOpenAI({ apiKey: useRuntimeConfig().openaiApiKey });

  return defineEventHandler(async (event) => {
    const { messages } = await readBody<{ messages: UIMessage[] }>(event);

    const result = streamText({
      model: openai('gpt-4o-mini'),
      system: "Tu es l'assistant du support client. Réponds en français, de façon concise.",
      messages: await convertToModelMessages(messages),
      maxOutputTokens: 1000,
    });

    return result.toUIMessageStreamResponse();
  });
});

Côté composant, @ai-sdk/vue expose une classe Chat dont les propriétés (messages, status, error) sont réactives :

<!-- app/components/AssistantChat.vue -->
<script setup lang="ts">
import { Chat } from '@ai-sdk/vue';
import { computed, ref } from 'vue';

const input = ref('');
const chat = new Chat({}); // envoie par défaut vers /api/chat
const busy = computed(() => chat.status === 'submitted' || chat.status === 'streaming');

function send() {
  if (!input.value.trim() || busy.value) return;
  chat.sendMessage({ text: input.value });
  input.value = '';
}
</script>

<template>
  <div>
    <div aria-live="polite" :aria-busy="busy">
      <div v-for="message in chat.messages" :key="message.id" :class="message.role === 'user' ? 'msg-user' : 'msg-ai'">
        <template v-for="(part, i) in message.parts" :key="`${message.id}-${i}`">
          <span v-if="part.type === 'text'">{{ part.text }}</span>
        </template>
      </div>
      <p v-if="chat.status === 'submitted'" class="typing">L'assistant réfléchit…</p>
    </div>

    <div v-if="chat.error" role="alert">
      La réponse n'a pas pu aboutir.
      <button type="button" @click="chat.regenerate()">Réessayer</button>
    </div>

    <form @submit.prevent="send">
      <input v-model="input" placeholder="Posez votre question" />
      <button v-if="busy" type="button" @click="chat.stop()">Arrêter</button>
      <button v-else type="submit" :disabled="!input.trim()">Envoyer</button>
    </form>
  </div>
</template>

La logique est identique à la version React : seul l'adaptateur change. C'est l'un des intérêts du SDK pour une équipe qui maintient plusieurs frontends, ou qui développe avec Nuxt après avoir prototypé en Next.js.

Soigner l'UX de la réponse progressive

Afficher des tokens au fil de l'eau ne suffit pas à rendre une interface agréable. Les détails suivants font la différence entre un chat qui paraît fluide et un chat qui clignote.

  • Distinguer l'attente de la génération. Entre l'envoi et le premier token (submitted), il peut s'écouler plusieurs secondes, surtout avec un modèle de raisonnement ou un appel d'outil. Un indicateur dédié rassure l'utilisateur ; il disparaît dès que le texte arrive.
  • Toujours proposer d'arrêter. Un bouton « Arrêter » pendant la génération évite d'attendre la fin d'une réponse partie dans la mauvaise direction. Avec abortSignal: req.signal côté serveur, l'arrêt coupe aussi l'appel au fournisseur au lieu de laisser la génération se poursuivre dans le vide.
  • Rendre le Markdown partiel sans casse. Les LLM répondent souvent en Markdown, et un texte en cours de génération contient des blocs incomplets : un bloc de code non fermé, un tableau à moitié écrit. Utilisez un rendu tolérant à ces états intermédiaires ; côté React, Vercel publie la bibliothèque streamdown, conçue pour ce cas. Si votre rendu est coûteux, évitez de re-parser tout le message à chaque fragment.
  • Ne pas voler le défilement. Faites défiler automatiquement vers le bas uniquement si l'utilisateur s'y trouve déjà. S'il est remonté relire un passage, le ramener de force à chaque token est insupportable.
  • Penser aux lecteurs d'écran. Une zone aria-live="polite" avec aria-busy pendant la génération évite que chaque fragment soit annoncé : le contenu est lu une fois la réponse terminée.
  • Garder le texte partiel en cas d'erreur. Si le flux s'interrompt, laissez la réponse partielle visible et proposez de régénérer, plutôt que de tout effacer derrière un message d'erreur générique.
  • Enregistrer la conversation côté serveur. Le callback onFinish de toUIMessageStreamResponse permet de persister les messages en fin de flux, et indique via isAborted si l'utilisateur a interrompu la réponse.

Ce qui casse le streaming en production

Un chat qui streame parfaitement en local peut afficher toute la réponse d'un bloc une fois déployé. Les causes sont presque toujours dans l'infrastructure, pas dans le code.

  • Un proxy qui bufferise. Nginx, par exemple, met les réponses en tampon par défaut : le navigateur reçoit tout à la fin. Désactivez proxy_buffering sur la route de chat, ou vérifiez que l'en-tête X-Accel-Buffering: no arrive bien jusqu'au proxy.
  • Une compression mal configurée. Certains middlewares de compression attendent d'avoir assez de données avant d'envoyer quoi que ce soit. Excluez le type text/event-stream de la compression.
  • Des timeouts trop courts. Les fonctions serverless ont une durée maximale d'exécution (maxDuration sur Vercel), et les load balancers coupent les connexions inactives au-delà d'un délai. Une réponse longue, ou un premier token qui tarde, peut dépasser ces limites : ajustez-les à la longueur réelle de vos réponses.
  • Un CDN qui met en cache. Une route de chat ne doit jamais être mise en cache : vérifiez vos règles pour les requêtes POST vers /api/chat.

Pour aller plus loin sur l'architecture d'ensemble d'un produit qui embarque un LLM, notre guide pour intégrer l'IA dans une application web couvre le choix du modèle, le RAG et la maîtrise des coûts.

Conclusion

Le streaming ne rend pas un LLM plus rapide : il rend l'attente visible et utile. L'utilisateur commence à lire dès les premiers tokens au lieu de fixer un écran vide, et il garde la main pour interrompre une réponse qui ne lui convient pas.

Avec le Vercel AI SDK, l'essentiel tient en deux fichiers : une route qui appelle streamText et un composant qui consomme le flux avec useChat ou Chat. Le vrai travail est ailleurs : états d'attente distincts, rendu du Markdown partiel, gestion des erreurs, et une infrastructure qui ne bufferise pas la réponse. Si vous voulez intégrer une interface conversationnelle dans votre produit, notre équipe développe des applications IA sur mesure, du prototype à la mise en production.

FAQ - Questions fréquentes