Le streaming : réduire la latence perçue
Perçu contre réel
Le streaming renvoie la réponse par fragments (chunks) au fil de la génération, au lieu d'attendre la fin. Il ne rend pas le modèle plus rapide : la latence totale est quasi identique. Il réduit la latence perçue, parce que l'utilisateur lit dès le TTFT.
gantt dateFormat X axisFormat %Ls title "Une réponse de 400 tokens (ordre de grandeur)" section Sans streaming Attente (rien à l'écran) :a1, 0, 4000 Affichage d'un bloc :a2, 4000, 4100 section Avec streaming Attente jusqu'au TTFT :b1, 0, 800 Texte qui s'écrit :b2, 800, 4000
| Situation | Gain du streaming |
|---|---|
| Chat, réponse longue (explication, rédaction) | fort : l'utilisateur lit pendant la génération |
| Réponse courte (1 à 3 phrases, notre FAQ) | faible : TTFT et fin sont proches |
| Sortie JSON consommée par du code | nul pour l'affichage ; utile seulement pour mesurer le TTFT ou couper tôt |
| Appel d'outil, agent | le texte final arrive après les outils : streamer les étapes (« je cherche… ») aide davantage |
Le code
Avec le SDK google-genai, la version asynchrone client.aio renvoie un itérateur asynchrone. Attention à la syntaxe : on attend l'appel, puis on itère.
async for chunk in await client.aio.models.generate_content_stream(
model=model, contents=question, config=config):
if not c.ttft and chunk.text:
c.ttft = time.perf_counter() - t0
parts.append(chunk.text or "")Pour une interface web, on relaie les fragments tels quels, par exemple en Server-Sent Events (SSE, un flux HTTP texte que le navigateur lit au fil de l'eau) avec FastAPI :
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
from google import genai
app, client = FastAPI(), genai.Client()
@app.get("/ask")
async def ask(q: str):
async def events():
async for chunk in await client.aio.models.generate_content_stream(
model="gemini-3.5-flash-lite", contents=q):
if chunk.text:
yield f"data: {chunk.text}\n\n"
return StreamingResponse(events(), media_type="text/event-stream")Vous passez une réponse de FAQ de 50 tokens en streaming. Le TTFT est de 0,76 s et la latence totale de 0,86 s. Qu'avez-vous gagné ?
Le gain perçu est au plus l'écart entre TTFT et fin. Le streaming ne change ni la latence totale ni le coût : les mêmes tokens sont produits et facturés.
Couper tôt : le streaming comme garde-fou
Le streaming permet aussi d'arrêter une génération dès qu'on en sait assez : un filtre détecte un contenu interdit au troisième fragment, l'utilisateur clique sur « Stop », ou la sortie dépasse une longueur. On sort simplement de la boucle async for : la connexion est fermée.
Pourquoi le TTFT d'une requête escaladée vers le grand modèle est-il beaucoup plus élevé que celui d'une requête traitée par le petit modèle ?
Dans une cascade, la décision d'escalader se prend sur la sortie complète du petit modèle. Le TTFT perçu = latence du petit + TTFT du grand. C'est le coût caché du routage, visible dans le p95.
Exercice
Modifiez ask pour qu'elle s'arrête dès que la réponse accumulée dépasse 600 caractères, et renvoie ce qui a été reçu avec une confiance de 0 (réponse tronquée = à ne pas croire).
Voir la solution
async for chunk in await client.aio.models.generate_content_stream(
model=model, contents=question, config=config):
if not c.ttft and chunk.text:
c.ttft = time.perf_counter() - t0
parts.append(chunk.text or "")
if sum(map(len, parts)) > 600:
c.latency = time.perf_counter() - t0
c.answer, c.confidence = "".join(parts), 0.0
return c # sortir de la boucle ferme le fluxLimite : les tokens déjà générés côté serveur peuvent être facturés, et usage_metadata (qui arrive avec le dernier fragment) manque ; le coût est alors à estimer. C'est pourquoi max_output_tokens reste la première ligne de défense.
Résultat obtenu
Mesures réelles du 2026-10-15, premières questions du banc (secondes, resultats_big_small.json) :
modèle question TTFT fin tokens sortie
gemini-3.5-flash-lite q01 0.61 0.71 38
gemini-3.5-flash-lite q02 1.18 1.33 58
gemini-3.8-flash q01 2.20 2.52 63
gemini-3.8-flash q02 1.17 1.57 73
router (escalade q12) q12 2.70 3.19 —Sur des réponses de 40 à 70 tokens, la fin arrive 0,1 à 0,4 s après le premier fragment : le levier principal ici est le TTFT, donc le choix du modèle et la taille du prompt, pas le streaming.
Je sais faire
- Expliquer la différence entre latence perçue et latence totale
- Streamer une réponse avec
client.aio.models.generate_content_stream - Relayer un flux vers un navigateur sans mise en tampon
- Reconnaître les cas où le streaming n'apporte rien