Risposte degli LLM in streaming: perché contano per la UX e i costi
I 900 ms che facevano sembrare «guasta» la nostra interfaccia di chat
Qualche mese fa stavo facendo il debug di un assistente per il supporto clienti che, sulla carta, aveva una latenza end-to-end perfettamente accettabile: la maggior parte delle risposte veniva completata in 4–7 secondi. Il modello selezionava gli strumenti, recuperava il contesto dell'account e generava una risposta utile.
Ma gli utenti lo odiavano.
Il motivo era semplice: l'interfaccia rimaneva vuota per i primi 3.8 secondi.
Nessun testo accanto all'indicatore di caricamento, nessuna risposta parziale, nessun segnale che il modello avesse capito la domanda. Solo una bolla vuota dell'assistente. Quando abbiamo modificato lo stesso endpoint per trasmettere i token in streaming tramite Server-Sent Events, il tempo medio totale di completamento è cambiato appena. Ma il tempo fino al primo output visibile è sceso a circa 700–900 ms per molte richieste, e il prodotto è sembrato immediatamente più veloce.
È questo il vero motivo per cui lo streaming conta.
Lo streaming delle risposte LLM non è magia. Non fa ragionare il modello più velocemente. Non riduce il numero totale di token. Non riduce automaticamente la fattura. Ma modifica il ciclo di interazione in un modo che gli utenti percepiscono e offre alla tua applicazione maggiore controllo su annullamento, rendering, moderazione, contabilizzazione dell'utilizzo e contropressione.
Questo articolo spiega come funziona davvero lo streaming SSE, cosa misurare, dove la fatturazione può presentare sottigliezze e quali approcci lato client uso nei sistemi di produzione.
Qual è il principale vantaggio delle risposte in streaming?
Il principale vantaggio delle risposte in streaming è la latenza percepita: l'utente vede i primi token in poche centinaia di millisecondi invece di fissare un indicatore di caricamento per tutta la durata della generazione. Il tempo totale di generazione cambia appena: a cambiare è il tempo fino al primo token, ed è questo il valore che gli utenti percepiscono davvero. Una risposta che impiega 9 secondi per completarsi ma inizia ad apparire dopo 400 ms sembra veloce; la stessa risposta mostrata tutta insieme dopo 9 secondi dà l'impressione che qualcosa non funzioni.
Ci sono anche vantaggi secondari: lo streaming consente di visualizzare progressivamente l'output parziale, annullare la generazione in anticipo quando l'utente ha visto abbastanza contenuti (risparmiando token di output) ed evitare i timeout di lettura dei proxy sulle risposte lunghe. Ma se devi ricordare una sola cosa: lo streaming non rende il modello più veloce, fa sparire l'attesa dall'esperienza utente.
Cosa significa davvero streaming
Quando invii una richiesta LLM senza streaming, il server di solito attende che il modello abbia finito di generare la risposta, poi restituisce un unico payload JSON:
{
"id": "resp_123",
"model": "some-model",
"output_text": "Sure — here is the answer...",
"usage": {
"input_tokens": 812,
"output_tokens": 436
}
}
Con lo streaming, il server invia eventi incrementali man mano che l'output diventa disponibile. Il meccanismo di trasporto più comune è Server-Sent Events, solitamente abbreviato in SSE.
SSE è semplicemente una risposta HTTP con una connessione persistente e un tipo di contenuto specifico:
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
Invece di un unico oggetto JSON, il server invia una sequenza di piccoli eventi:
event: message.delta
data: {"delta":"Sure"}
event: message.delta
data: {"delta":" — here"}
event: message.delta
data: {"delta":" is"}
event: message.delta
data: {"delta":" the answer..."}
event: message.done
data: {"finish_reason":"stop","usage":{"input_tokens":812,"output_tokens":436}}
Le diverse API usano nomi e schemi degli eventi differenti, ma la struttura è solitamente la stessa:
- Aprire una connessione HTTP.
- Inviare i metadati della richiesta e il prompt.
- Ricevere blocchi incrementali.
- Aggiungere i blocchi al messaggio corrente dell'assistente.
- Ricevere un evento finale con il motivo dell'arresto e, spesso, i metadati di utilizzo.
- Chiudere il flusso.
In pratica, nella maggior parte dei casi lo «streaming» LLM non invia un token per evento. I fornitori possono raggruppare i token in blocchi per ragioni di efficienza. Potresti ricevere un singolo carattere, un frammento di parola, più parole o un delta JSON strutturato. Non creare mai un client che presupponga che ogni blocco sia una parola o una frase completa.
Latenza: tempo totale e tempo fino al primo token
La distinzione più importante è tra latenza totale e tempo fino al primo token.
| Metrica | Cosa misura | Perché conta |
|---|---|---|
| Tempo fino al primo byte, TTFB | Primo byte ricevuto dal server | Include instradamento, autenticazione, accodamento e pianificazione dell'esecuzione del modello |
| Tempo fino al primo token, TTFT | Primo token o blocco generato visibile all'utente | Principale fattore della reattività percepita |
| Token al secondo | Velocità di generazione dell'output dopo il primo token | Determina quanto rapidamente vengono completate le risposte lunghe |
| Tempo totale di completamento | Ricezione della risposta completa | Conta per automazione, chiamate agli strumenti e attività in batch |
| Tempo fino a una risposta utilizzabile | L'utente dispone di informazioni sufficienti per agire | Spesso precede il completamento della risposta |
Per l'esperienza utente della chat, TTFT è solitamente più importante del tempo totale. Una risposta che inizia in 800 ms e termina in 8 secondi spesso sembra migliore di una che appare tutta insieme dopo 4 secondi.
Nei flussi di lavoro machine-to-machine, lo streaming spesso conta meno. Se il passaggio successivo non può essere eseguito finché l'intero oggetto JSON non è completo, lo streaming potrebbe soltanto aggiungere complessità all'implementazione.
Misurare TTFT in Python
Ecco un approccio semplice per misurare la latenza del primo blocco e il tempo totale del flusso. La struttura dell'API è volutamente generica: adatta l'URL e il payload al tuo fornitore.
import json
import time
import requests
url = "https://api.example.com/v1/chat/completions"
payload = {
"model": "sonnet-4.6",
"stream": True,
"messages": [
{"role": "user", "content": "Explain SSE streaming in 5 bullet points."}
]
}
headers = {
"Authorization": "Bearer $API_KEY",
"Content-Type": "application/json",
"Accept": "text/event-stream",
}
start = time.perf_counter()
first_chunk_at = None
chars = 0
with requests.post(url, headers=headers, json=payload, stream=True) as r:
r.raise_for_status()
for raw_line in r.iter_lines(decode_unicode=True):
if not raw_line:
continue
if not raw_line.startswith("data: "):
continue
data = raw_line[len("data: "):]
if data == "[DONE]":
break
event = json.loads(data)
# Provider-specific: this may be event["delta"], event["choices"][0]["delta"], etc.
delta = event.get("delta", "")
if delta and first_chunk_at is None:
first_chunk_at = time.perf_counter()
chars += len(delta)
print(delta, end="", flush=True)
end = time.perf_counter()
print("\n\n--- metrics ---")
print(f"ttft_ms={((first_chunk_at or end) - start) * 1000:.0f}")
print(f"total_ms={(end - start) * 1000:.0f}")
print(f"chars={chars}")
Un'insidia comune: se testi lo streaming con un proxy locale, una funzione serverless o un reverse proxy che memorizza le risposte in un buffer, potresti pensare che il modello non stia trasmettendo in streaming. Il modello potrebbe farlo correttamente, mentre la tua infrastruttura trattiene i blocchi fino al completamento della risposta.
L'ho riscontrato con:
- Buffering di Nginx abilitato.
- Funzioni cloud che non supportano le risposte in streaming.
- Middleware di compressione che attendono buffer più grandi.
- Framework web che costruiscono l'intera risposta prima di restituirla.
- Proxy aziendali che aggregano i blocchi piccoli.
Per Nginx, questa impostazione è spesso importante:
location /api/llm/stream {
proxy_pass http://app_backend;
proxy_http_version 1.1;
proxy_buffering off;
proxy_cache off;
chunked_transfer_encoding on;
add_header X-Accel-Buffering no;
}
SSE e WebSocket a confronto
SSE non è l'unico modo per trasmettere in streaming. Anche i WebSocket sono comuni, soprattutto per le applicazioni bidirezionali. Per la maggior parte delle interfacce di chat LLM, però, SSE è la scelta predefinita più semplice.
| Funzionalità | SSE | WebSocket |
|---|---|---|
| Direzione | Dal server al client | Bidirezionale |
| Protocollo | Normale risposta HTTP | Upgrade a WebSocket |
| Supporto nei browser | Nativo EventSource; funziona anche con fetch flussi | Nativo WebSocket |
| Comportamento di riconnessione | Meccanismo integrato, ma serve comunque logica applicativa | Da implementare |
| Compatibilità con i bilanciatori di carico | Solitamente più semplice | A volte richiede configurazione aggiuntiva |
| Ideale per | Streaming dei token, eventi di avanzamento, chat semplice | Collaborazione in tempo reale, audio, sessioni dal vivo con più turni |
| Supporto del corpo della richiesta | EventSource supporta solo GET; usa fetch per lo streaming POST | Messaggi bidirezionali completi |
La EventSource L'API del browser è comoda, ma supporta solo richieste GET. La maggior parte delle chiamate agli LLM richiede corpi POST e intestazioni di autenticazione, quindi di solito gestisco lo streaming con fetch() e ReadableStream.
Pattern per il client browser: Fetch + ReadableStream
Un'implementazione minima per il browser si presenta così:
async function streamChat({ messages, signal, onDelta, onDone }) {
const res = await fetch("/api/chat/stream", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
model: "gpt-5.5",
stream: true,
messages
}),
signal
});
if (!res.ok) {
throw new Error(`HTTP ${res.status}`);
}
const reader = res.body.getReader();
const decoder = new TextDecoder("utf-8");
let buffer = "";
while (true) {
const { value, done } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop() || "";
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed.startsWith("data:")) continue;
const data = trimmed.slice(5).trim();
if (data === "[DONE]") {
onDone?.();
return;
}
const event = JSON.parse(data);
if (event.delta) {
onDelta(event.delta);
}
}
}
onDone?.();
}
E in un'interfaccia utente:
const controller = new AbortController();
let assistantText = "";
streamChat({
messages,
signal: controller.signal,
onDelta(delta) {
assistantText += delta;
renderAssistantMessage(assistantText);
},
onDone() {
markMessageComplete();
}
});
// Cancel button
document.querySelector("#stop").onclick = () => {
controller.abort();
};
Non eseguire il rendering di ogni frammento in modo ingenuo
Se aggiorni lo stato di React a ogni piccolo frammento, puoi creare involontariamente un problema di prestazioni lato client. Alcuni flussi producono decine di frammenti al secondo. Ripetere ogni volta il rendering di un grande albero di componenti della chat è uno spreco di risorse.
In pratica, di solito raggruppo gli aggiornamenti dell'interfaccia nei frame di animazione:
let pending = "";
let scheduled = false;
function onDelta(delta) {
pending += delta;
if (!scheduled) {
scheduled = true;
requestAnimationFrame(() => {
assistantText += pending;
pending = "";
scheduled = false;
setAssistantText(assistantText);
});
}
}
Questo mantiene fluida l'interfaccia, preservando la percezione di un output in tempo reale.
Gestire correttamente l'output parziale
Lo streaming significa che l'applicazione riceve testo incompleto. Sembra ovvio, ma molti bug derivano dal trattare l'output parziale come output definitivo.
Il Markdown è temporaneamente non valido
Durante lo streaming, il Markdown può essere malformato:
Here is the query:
```sql
SELECT *
FROM users
WHERE
Il delimitatore del blocco di codice potrebbe non essere ancora chiuso. Una tabella potrebbe avere solo la riga di intestazione. Un link potrebbe essere stato emesso solo a metà. Il renderer deve tollerare queste situazioni.
Per le interfacce di chat, preferisco:
- Eseguire il rendering del Markdown in modo incrementale, ma con le dovute precauzioni.
- Disabilitare HTML pericoloso.
- Evitare l'evidenziazione della sintassi, che richiede molte risorse, finché il messaggio non è completo.
- Mostrare un discreto stato di «generazione in corso».
- Riapplicare la formattazione finale al termine del flusso.
Lo streaming di JSON è più difficile
Se chiedi al modello di produrre JSON e lo trasmetti in streaming, lo stato intermedio di solito non è JSON valido. Questo è un problema per le applicazioni che vogliono aggiornare interfacce strutturate in tempo reale.
Per esempio:
{
"title": "Incident Summary",
"severity": "high",
"steps": [
"Restart
Non puoi ancora analizzarlo.
Esistono tre approcci comuni:
- Attendere il completamento, quindi analizzare il contenuto una sola volta.
- Usare JSON delimitato da righe, in cui ogni riga completa può essere analizzata indipendentemente.
- Trasmettere eventi semantici in streaming, anziché JSON grezzo.
Per lo streaming strutturato, preferisco un output sotto forma di eventi:
event: section
data: {"title":"Summary"}
event: bullet
data: {"text":"Database failover started at 09:42 UTC."}
event: bullet
data: {"text":"Read latency remained elevated for 6 minutes."}
Questo pattern richiede più orchestrazione, ma è molto più facile da visualizzare in modo affidabile.
Cosa succede davvero quando gli utenti premono «Interrompi»
L'annullamento è uno dei vantaggi meno apprezzati dello streaming.
Con una richiesta senza streaming, l'utente può abbandonare la pagina mentre il modello continua a generare sul server. A seconda del provider e del backend, quella generazione potrebbe continuare a consumare token di output.
Con lo streaming, il client può interrompere la richiesta HTTP:
const controller = new AbortController();
fetch("/api/chat/stream", {
method: "POST",
body: JSON.stringify(payload),
signal: controller.signal
});
// Later
controller.abort();
Ma l'annullamento non è sempre istantaneo lungo l'intero percorso.
Ci sono diversi livelli:
- Il browser interrompe la chiamata fetch.
- Il backend rileva la chiusura della connessione.
- Il backend annulla la richiesta al provider a monte.
- Il provider interrompe la generazione.
- L'utilizzo viene calcolato per i token già elaborati o generati.
Un'insidia comune: se il backend funge da proxy per il flusso ma non propaga l'annullamento a monte, il browser dell'utente si disconnette, ma il server continua a leggere dal modello fino al completamento.
In pseudocodice in stile Node:
app.post("/api/chat/stream", async (req, res) => {
const upstreamController = new AbortController();
req.on("close", () => {
upstreamController.abort();
});
const upstream = await fetch(PROVIDER_URL, {
method: "POST",
headers: providerHeaders,
body: JSON.stringify({
...req.body,
stream: true
}),
signal: upstreamController.signal
});
res.setHeader("Content-Type", "text/event-stream");
for await (const chunk of upstream.body) {
res.write(chunk);
}
res.end();
});
Questa è la differenza tra «l'interfaccia si è fermata» e «il modello si è fermato».
Streaming e costi: cosa cambia e cosa no
Lo streaming cambia il momento in cui osservi l'utilizzo. Di solito non cambia l'unità di base della fatturazione.
La maggior parte delle API degli LLM addebita i costi in base a elementi come:
- Token di input.
- Token in uscita.
- Token di input memorizzati nella cache, se si applica la memorizzazione dei prompt nella cache.
- Chiamate agli strumenti o modalità speciali, a seconda dell'API.
- Talvolta, token di ragionamento o risorse di calcolo interne soggette a un budget per determinate famiglie di modelli.
Lo streaming di per sé generalmente non comporta uno sconto. Se il modello genera 700 token di output, devi prevedere di contabilizzare 700 token di output, sia che arrivino in modo incrementale sia che arrivino in un unico payload finale.
Lo streaming influisce sui costi sul piano operativo:
| Scenario | Impatto sui costi |
|---|---|
| L'utente annulla dopo 80 token | Puoi evitare di generare il resto dell'output, se l'annullamento viene propagato |
| L'interfaccia rileva in anticipo che la risposta è sufficiente | L'applicazione può interrompere intenzionalmente la generazione |
| Il backend non riesce a interrompere la richiesta a monte | Potresti pagare l'intera generazione nonostante la disconnessione del client |
| I dati di utilizzo arrivano solo nell'evento finale | I flussi interrotti potrebbero richiedere log lato provider o stime |
| Le risposte lunghe vengono trasmesse in streaming per impostazione predefinita | Gli utenti potrebbero consumare di più perché l'esperienza risulta più fluida |
| Chiamate agli strumenti prima dello streaming del testo | TTFT può aumentare, ma il costo dei token di output può rimanere invariato |
Un'insidia nella contabilizzazione dell'utilizzo
Molte API di streaming inviano i metadati di utilizzo alla fine del flusso:
{
"type": "message.done",
"usage": {
"input_tokens": 1240,
"output_tokens": 389
},
"finish_reason": "stop"
}
Se il flusso viene interrotto prima dell'evento finale, l'applicazione potrebbe non ricevere i dati di utilizzo. Questo non significa necessariamente che l'utilizzo sia pari a zero.
Per la contabilizzazione interna, di solito memorizzo tre campi:
{
"request_id": "chat_01j...",
"status": "client_aborted",
"usage": null,
"estimated_output_chars": 1842,
"final_usage_received": false
}
Poi riconcilio i dati in un secondo momento, se il provider mette a disposizione log delle richieste o eventi di utilizzo. In caso contrario, puoi almeno fare una stima approssimativa per le analisi del prodotto, evitando una falsa precisione nella fatturazione.
Non fatturare agli utenti finali sulla base di una stima dei caratteri, a meno che le tue condizioni non lo prevedano esplicitamente. La tokenizzazione varia in base al modello e alla lingua.
Pattern di architettura del backend
Un percorso di streaming in produzione si presenta in genere così:
Browser
|
| POST /api/chat/stream
v
App Backend
|
| provider streaming request
v
LLM API
Espongo raramente le chiavi dei provider direttamente ai browser. Il backend offre:
- Autenticazione e autorizzazione.
- Limitazione della frequenza delle richieste per utente o spazio di lavoro.
- Composizione del prompt e recupero delle informazioni.
- Esecuzione degli strumenti.
- Controlli sull'oscuramento dei dati e sulla registrazione nei log.
- Passaggio a un provider alternativo in caso di errore.
- Propagazione dell'annullamento.
- Registrazione dell'utilizzo.
Una risposta minima in streaming da un backend Python FastAPI:
import json
from fastapi import FastAPI, Request
from fastapi.responses import StreamingResponse
import httpx
app = FastAPI()
PROVIDER_URL = "https://api.example.com/v1/chat/completions"
@app.post("/api/chat/stream")
async def chat_stream(request: Request):
body = await request.json()
async def event_generator():
async with httpx.AsyncClient(timeout=None) as client:
async with client.stream(
"POST",
PROVIDER_URL,
headers={
"Authorization": "Bearer YOUR_PROVIDER_KEY",
"Content-Type": "application/json",
"Accept": "text/event-stream",
},
json={
**body,
"stream": True,
},
) as upstream:
async for line in upstream.aiter_lines():
if await request.is_disconnected():
break
if not line:
continue
# Optionally transform provider-specific events here.
yield line + "\n\n"
return StreamingResponse(
event_generator(),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"X-Accel-Buffering": "no",
},
)
In pratica, di solito normalizzo gli eventi dei provider in un unico schema interno:
{
"type": "delta",
"text": "partial text here"
}
e poi:
{
"type": "done",
"finish_reason": "stop",
"usage": {
"input_tokens": 812,
"output_tokens": 436
}
}
Questo è importante se supporti più modelli, come Claude, GPT, Gemini, Llama, Qwen o Kimi, attraverso un'unica interfaccia. Ogni provider ha una semantica dello streaming leggermente diversa. Normalizzare fin dall'inizio mantiene il frontend semplice e prevedibile, proprio come desideri.
TTFT non dipende solo dal modello
Quando TTFT è insoddisfacente, spesso si dà subito la colpa al modello. A volte è corretto: modelli più grandi, contesti più lunghi e modalità di ragionamento più intensive possono aumentare la latenza del primo token. Ma prima che arrivi il primo token entrano in gioco molti meccanismi.
Tra i fattori che contribuiscono ci sono:
- Configurazione della connessione TLS.
- Avvii a freddo nel backend.
- Costruzione del prompt e recupero delle informazioni.
- Latenza del database vettoriale.
- Chiamate agli strumenti prima della generazione.
- Accodamento presso il provider.
- Acquisizione di un contesto di input lungo.
- Controlli di sicurezza o livelli di applicazione delle policy.
- Buffering del proxy.
- Ritardo di rendering del client.
Una traccia utile dovrebbe distinguere questi aspetti:
{
"trace_id": "req_abc",
"timings_ms": {
"auth": 12,
"retrieval": 84,
"prompt_build": 9,
"provider_connect": 141,
"ttfb": 612,
"first_delta": 804,
"stream_complete": 5820
}
}
Se il recupero richiede 600 ms e il primo delta del modello arriva 250 ms dopo, cambiare modello potrebbe non risolvere i problemi della tua UX. Se il tempo è assorbito principalmente dalla creazione della connessione al provider, riutilizzare le connessioni può aiutare. Se il rendering del frontend attende un paragrafo completo, lo streaming funziona, ma la logica della tua UI lo nasconde.
Quando non usare lo streaming
Lo streaming non conviene sempre.
Evito lo streaming quando:
- La risposta è molto breve, ad esempio per la classificazione o l'instradamento.
- Il chiamante è un altro servizio che necessita di JSON completo.
- L'output deve essere sottoposto a moderazione completa prima di essere visualizzato.
- La richiesta viene eseguita in una coda di processi o in una pipeline batch.
- Il framework o il livello di hosting rende lo streaming inaffidabile.
- Il prodotto richiede risposte atomiche, non testo progressivo.
C'è anche uno svantaggio per la UX: lo streaming può mostrare il modello che cambia direzione a metà risposta. Le persone lo tollerano in una certa misura nelle chat, ma nei flussi di lavoro ad alto rischio può apparire poco curato. Per le interfacce in ambito legale, medico, finanziario o con forti requisiti di conformità, puoi scegliere di avviare lo streaming solo dopo i controlli di sicurezza oppure di inviare il flusso a un buffer interno prima di mostrare blocchi sicuri.
Pattern pratici lato client
Ecco i pattern che consiglio per la maggior parte dei prodotti di chat:
1. Mostra lo stato prima che arrivi il testo
Non lasciare vuoto il fumetto dell'assistente. Usa stati espliciti:
- “Lettura del contesto…”
- “Ricerca nei documenti…”
- “Preparazione della risposta…”
- “Elaborazione ancora in corso…”
Questo è particolarmente utile quando le chiamate agli strumenti avvengono prima della generazione del testo.
2. Mantieni un vero pulsante di interruzione
Un pulsante di interruzione non è un semplice elemento decorativo della UX. Se implementato correttamente, può far risparmiare token di output.
Assicurati che:
- Il browser interrompa la richiesta.
- Il backend rilevi le disconnessioni.
- Il backend interrompa la richiesta al provider.
- La richiesta venga registrata come annullata.
3. Esegui il rendering progressivamente e formatta alla fine
Durante lo streaming:
- Aggiungi il testo in coda.
- Esegui un rendering leggero del Markdown.
- Evita l'evidenziazione della sintassi che richiede molte risorse.
- Mantieni prevedibile il comportamento dello scorrimento.
Al termine:
- Analizza di nuovo il Markdown.
- Evidenzia il codice.
- Abilita i pulsanti di copia.
- Aggiungi citazioni o riferimenti se il tuo sistema li supporta.
4. Memorizza con attenzione i messaggi parziali
Se un flusso si interrompe a metà, decidi quale significato attribuire alla trascrizione.
Opzioni:
- Mantieni il messaggio parziale dell'assistente con un indicatore “interrotto”.
- Rimuovilo completamente.
- Consenti all'utente di riprovare con lo stesso prompt.
- Chiedi al modello di continuare dal contesto parziale.
Di solito mantengo il messaggio parziale con un'indicazione ben visibile. Eliminarlo senza avvisare confonde gli utenti, e fingere che sia completo è ancora peggio.
5. Normalizza gli eventi
Il frontend non dovrebbe dover conoscere cinque formati di streaming specifici dei provider. Normalizzali nel backend:
{"type":"delta","text":"Hello"}
{"type":"tool_start","name":"search_docs"}
{"type":"tool_result","name":"search_docs","status":"ok"}
{"type":"done","finish_reason":"stop","usage":{"input_tokens":100,"output_tokens":50}}
{"type":"error","message":"upstream timeout"}
Questo rende anche più facile passare da una famiglia di modelli all'altra o instradare le richieste dinamicamente. Se usi un livello API multimodello, questa normalizzazione potrebbe già far parte del tuo gateway; altrimenti vale la pena implementarla.
Indicazioni pratiche
- Lo streaming migliora la latenza percepita riducendo il tempo necessario alla comparsa del primo output visibile, non rendendo l'intera generazione gratuita o istantanea.
- Misura separatamente TTFT, il tempo totale di completamento e i token al secondo.
- SSE è solitamente il trasporto più semplice per lo streaming di testo degli LLM; usa
fetch()flussi quando ti servono corpi POST. - Fai attenzione al buffering di proxy e framework. Molti “bug dello streaming” sono problemi di buffering dell'infrastruttura.
- Considera l'output parziale non valido: il Markdown potrebbe essere incompleto, JSON potrebbe non essere analizzabile e i delimitatori dei blocchi di codice potrebbero essere ancora aperti.
- L'annullamento riduce i costi solo se l'interruzione si propaga dal browser al backend e quindi al provider.
- I metadati di utilizzo arrivano spesso alla fine del flusso; i flussi interrotti richiedono una contabilizzazione accurata.
- Non eseguire nuovamente il rendering dell'intera UI per ogni piccolo blocco. Raggruppa gli aggiornamenti usando i fotogrammi di animazione.
- Normalizza gli eventi di streaming specifici dei provider dietro il backend.
- Evita lo streaming per classificazioni brevi, processi batch, output con struttura rigorosa o flussi di lavoro che richiedono una moderazione completa prima della visualizzazione.
Un'unica chiave API per Claude Opus 5.5, Sonnet 5, Haiku 4.5 e Fable 5.1, oltre a GPT-6 modelli. Pagamento a consumo, senza abbonamento.
Ottieni la tua chiave API →