- 01Le streaming affiche la réponse au fur et à mesure : perçu comme bien plus rapide qu'une réponse d'un bloc
- 02La clé API reste côté serveur : le client parle à TA route, jamais directement au fournisseur
- 03Côté client, on lit le corps de la réponse comme un flux et on concatène les morceaux
Une réponse LLM qui apparaît d'un bloc après 5 secondes de vide donne une impression de lenteur. En streaming, le texte s'affiche token par token dès les premiers mots. À la fin de ce tuto, tu auras un chatbot qui répond en direct, avec la clé API bien planquée côté serveur. Exemple en Next.js (App Router), transposable à tout framework.
Étape 1 — Le principe
Le client ne parle jamais directement au fournisseur (sinon ta clé API fuite dans le navigateur). Le flux est :
Navigateur → TA route serveur → API du LLM (en streaming)
← flux relayé ←
Ta route reçoit le message, appelle le LLM en mode stream, et relaie les morceaux au navigateur au fur et à mesure.
Étape 2 — La route serveur qui relaie le flux
// app/api/chat/route.ts
export async function POST(req: Request) {
const { message } = await req.json();
const upstream = await fetch("https://api.mistral.ai/v1/chat/completions", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.MISTRAL_API_KEY}`,
},
body: JSON.stringify({
model: "mistral-small-latest",
messages: [{ role: "user", content: message }],
stream: true,
}),
});
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
const reader = upstream.body!.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
// Le flux arrive en lignes "data: {...}"
for (const line of buffer.split("\n")) {
const trimmed = line.trim();
if (!trimmed.startsWith("data:")) continue;
const payload = trimmed.slice(5).trim();
if (payload === "[DONE]") { controller.close(); return; }
try {
const delta = JSON.parse(payload).choices[0]?.delta?.content;
if (delta) controller.enqueue(encoder.encode(delta));
} catch { /* ligne partielle, on attend la suite */ }
}
buffer = buffer.slice(buffer.lastIndexOf("\n") + 1);
}
controller.close();
},
});
return new Response(stream, {
headers: { "Content-Type": "text/plain; charset=utf-8" },
});
}
On extrait le champ delta.content de chaque événement et on n'envoie au client que le texte, pas le JSON brut.
Étape 3 — Le client qui lit le flux
async function envoyer(message: string, onChunk: (t: string) => void) {
const res = await fetch("/api/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message }),
});
const reader = res.body!.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
onChunk(decoder.decode(value, { stream: true }));
}
}
Dans ton composant, onChunk concatène le morceau à l'état affiché :
let reponse = "";
await envoyer(question, (t) => { reponse += t; setReponse(reponse); });
Adapter à ton cas
- Historique de conversation : envoie tout le tableau
messages(pas juste le dernier) pour garder le contexte. - Autre fournisseur : OpenAI utilise le même format SSE
data:; Claude a un format d'événements différent (event:+data:) — adapte le parsing de l'étape 2. - Arrêt manuel : passe un
AbortControlleraufetchclient pour couper la génération. - Markdown : rends la réponse avec un rendu Markdown côté client pour les listes et le code.
En cas de souci
- Rien ne s'affiche avant la fin : un proxy ou un middleware tamponne la réponse. Vérifie que rien ne met la réponse en buffer, et que tu n'attends pas
res.text()(qui attend tout). - Texte tronqué ou JSON cassé dans les logs : un événement SSE peut arriver coupé en deux lectures. Le
bufferde l'étape 2 gère ça — ne parse jamais une ligne sans vérifier qu'elle est complète. - La clé API apparaît dans le navigateur : tu appelles le fournisseur depuis le client. Repasse par ta route serveur, toujours.
- Timeouts sur longues réponses : sur certaines plateformes serverless, augmente la durée max d'exécution de la route, sinon le flux est coupé.
Articles liés
Obtenir du JSON fiable d'un LLM : structured outputs et validation
Arrêter de parser du texte au petit bonheur : forcer un LLM à répondre en JSON valide, valider la sortie contre un schéma et gérer proprement les cas malformés.
Le function calling : laisser une IA déclencher des actions
Construis pas à pas un assistant qui appelle vraiment tes fonctions : on part d'un appel vide, on ajoute un outil, on exécute le code, et on boucle. Script complet et réutilisable à la fin.
Faire tourner un modèle IA en local avec Ollama (gratuit et privé)
De l'installation à l'appel API en Python, en passant par une interface de chat type ChatGPT en local : le guide complet pour faire tourner un LLM sur ta machine, avec dépannage et réglages de performance.