Quando un'interfaccia web deve porre una domanda a un modello linguistico — magari con una ricerca RAG su un archivio di documenti — la risposta può richiedere da qualche secondo a decine di secondi: il tempo di recuperare i passaggi rilevanti, costruire il prompt e attendere la generazione. Tenere la richiesta HTTP aperta per tutto questo tempo è fragile: basta un timeout del proxy o un refresh accidentale della pagina per perdere tutto. Un pattern più robusto, usato da molte API di LLM per i job più lunghi, è separare l'invio dalla lettura del risultato: si crea un "job", si riceve subito un identificativo, e poi si interroga periodicamente lo stato finché non è pronto.
Un client fetch con gestione degli errori uniforme
Prima di tutto conviene centralizzare le chiamate HTTP in un'unica funzione, così ogni punto dell'interfaccia gestisce gli errori allo stesso modo (sessione scaduta, risposta non valida, eccezioni di rete):
async function chiamaApi(percorso, opzioni = {}) {
const risposta = await fetch('/api' + percorso, {
...opzioni,
headers: {
...(opzioni.headers || {}),
Authorization: `Bearer ${sessionStorage.getItem('access_token') || ''}`,
},
});
if (risposta.status === 401) {
window.location.href = '/login.html';
throw new Error('Sessione scaduta');
}
if (!risposta.ok) {
const corpo = await risposta.json().catch(() => ({}));
throw new Error(corpo.error?.message || `Errore HTTP ${risposta.status}`);
}
return risposta.status === 204 ? null : risposta.json();
}Avviare il job e attendere il risultato
Con il client pronto, la domanda viene inviata a un endpoint che risponde subito con un identificativo di job, senza attendere la generazione. Da lì si entra in un ciclo di polling, con una piccola pausa tra un controllo e l'altro:
let richiestaInCorso = false;
async function chiediAlModello(domanda) {
if (richiestaInCorso) return; // evita invii doppi da doppio click
richiestaInCorso = true;
try {
const job = await chiamaApi('/llm/jobs', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ domanda }),
});
// Polling: si interroga lo stato del job finché non è concluso
for (;;) {
const stato = await chiamaApi(`/llm/jobs/${job.id}`);
if (stato.status === 'FAILED') {
throw new Error(stato.errore || 'Elaborazione non riuscita');
}
if (stato.status === 'DONE') {
return stato.result; // { risposta, fonti, modello, durata_ms }
}
await new Promise((resolve) => setTimeout(resolve, 1200));
}
} finally {
richiestaInCorso = false;
}
}Il flag richiestaInCorso è una protezione minima ma importante: senza, un utente che preme due volte il bottone "Invia" genera due job paralleli che consumano risorse del modello inutilmente.
Mostrare le fonti citate dalla risposta
Nei sistemi RAG è buona norma restituire, insieme alla risposta, i passaggi dei documenti da cui è stata tratta, così l'utente può verificarla. Un modo semplice per presentarli è un elenco di elementi <details>, così l'estratto resta chiuso finché non lo si vuole leggere:
function mostraFonti(fonti, contenitore) {
contenitore.replaceChildren();
for (const fonte of fonti) {
const dettaglio = document.createElement('details');
const titolo = document.createElement('summary');
titolo.textContent = `[${fonte.citazione}] ${fonte.file} · pagina ${fonte.pagina}`;
const estratto = document.createElement('p');
estratto.textContent = fonte.estratto;
dettaglio.append(titolo, estratto);
contenitore.append(dettaglio);
}
}Qualche accorgimento pratico
- Il polling va sempre fermato in caso di errore o quando il componente viene smontato, altrimenti resta un ciclo appeso.
- Un intervallo fisso va bene per prototipi; in produzione conviene un backoff crescente (1s, 2s, 4s...) per non sovraccaricare il server durante elaborazioni lunghe.
- La chiave dell'API del modello non deve mai comparire nel codice del browser: la chiamata va sempre fatta da un endpoint lato server, che la tiene segreta.
Questo pattern — invia, ricevi un ID, interroga — si applica bene a qualunque operazione lunga esposta da un'API: generazione di documenti, trascrizioni audio, elaborazioni batch. Cambia solo la forma del risultato finale.