Risposte LLM in streaming in Delphi: token per token con Server-Sent Events

· Componenti

Risposta rapida: per trasmettere in streaming una risposta LLM in Delphi con sgcWebSockets, assegna l'evento OnHTTPAPISSE del componente, poi chiama il metodo di streaming — _CreateChatCompletion con Stream := True per OpenAI, _CreateMessageStream per Anthropic Claude e Ollama, _CreateContentStream per Gemini. Ogni delta arriva nel gestore come un Server-Sent Event; aggiungi aData a una Memo e la risposta si digita da sola man mano che il modello la genera.

Una chiamata LLM non in streaming si blocca finché l'intera risposta non è pronta. Per una risposta di un paragrafo va bene, ma per un completamento lungo l'utente fissa una finestra bloccata per diversi secondi senza alcun riscontro. Lo streaming risolve questo: il modello restituisce il suo output come una sequenza di piccoli chunk su un'unica connessione HTTP, e tu visualizzi ogni chunk nel momento in cui arriva. Il risultato è il familiare effetto di "digitazione" che vedi in ChatGPT, e una UI che sembra reattiva anche quando la risposta completa richiede un po' di tempo.

Come funziona lo streaming: OnHTTPAPISSE

Sotto il cofano ogni componente AI di sgcWebSockets trasmette in streaming usando i Server-Sent Events (SSE). Il provider mantiene la risposta aperta e invia eventi man mano che i token vengono prodotti. Il componente analizza quegli eventi e solleva un evento per ogni chunk attraverso un unico gestore con la stessa firma su ogni componente:

procedure TForm1.HandleSSE(Sender: TObject;
  const aEvent, aData: string; var Cancel: Boolean);
begin
  // aEvent  -> the SSE event name (provider-specific)
  // aData   -> the payload for this chunk (the token delta)
  // Cancel  -> set True to abort the stream early
  Memo1.Lines.Add(aData);
end;

Questo è l'intero contratto. aData trasporta il payload incrementale, aEvent ti dice di quale tipo di evento si tratta (utile con i provider che emettono diversi tipi di evento), e impostare Cancel := True arresta lo stream, ad esempio quando l'utente fa clic su un pulsante Stop. La stessa forma del gestore funziona per OpenAI, Anthropic, Gemini e Ollama, quindi una volta scritta puoi riutilizzarla tra i vari provider.

OpenAI: Stream := True

Con TsgcHTTP_API_OpenAI abiliti lo streaming sulla richiesta e agganci l'evento. Imposta OpenAIOptions.ApiKey, assegna OnHTTPAPISSE, e il completamento della chat viene consegnato chunk per chunk invece che in un unico blocco:

uses
  sgcHTTP_API_OpenAI;

var
  OpenAI: TsgcHTTP_API_OpenAI;
begin
  OpenAI := TsgcHTTP_API_OpenAI.Create(nil);
  OpenAI.OpenAIOptions.ApiKey := 'sk-...';

  // Each token delta arrives in HandleSSE
  OpenAI.OnHTTPAPISSE := HandleSSE;

  // Build a typed request, set Stream := True, then call
  OpenAI._CreateChatCompletion('gpt-4o-mini', 'Explain WebSockets in detail.');
end;

La richiesta tipizzata espone una proprietà Stream, quindi quando costruisci tu stesso l'oggetto richiesta imposti Stream := True prima dell'invio. I token affiorano poi attraverso OnHTTPAPISSE man mano che vengono generati, e tu aggiungi ciascuno alla tua Memo.

Anthropic Claude: _CreateMessageStream

Claude espone un helper di streaming dedicato, quindi non c'è alcun flag separato da attivare: chiamare _CreateMessageStream su TsgcHTTP_API_Anthropic attiva SSE per quella richiesta. Imposta la chiave API e la versione, assegna il gestore e chiamalo:

uses
  sgcHTTP_API_Anthropic;

var
  Anthropic: TsgcHTTP_API_Anthropic;
begin
  Anthropic := TsgcHTTP_API_Anthropic.Create(nil);
  Anthropic.AnthropicOptions.ApiKey := 'sk-ant-...';
  Anthropic.AnthropicOptions.AnthropicVersion := '2023-06-01';

  Anthropic.OnHTTPAPISSE := HandleSSE;
  Anthropic._CreateMessageStream(
    'claude-3-5-sonnet-latest',
    'Summarise RFC 6455',
    1024);
end;

Claude emette diversi tipi di evento SSE durante lo streaming (inizio dei blocchi di contenuto, delta, poi stop). L'argomento aEvent ti permette di distinguerli se ne hai bisogno; per una semplice UI "mostra il testo man mano che arriva", aggiungere aData è sufficiente.

Gemini e Ollama: la stessa forma

Google Gemini segue lo schema identico con il proprio metodo di streaming, _CreateContentStream su TsgcHTTP_API_Gemini:

Gemini.OnHTTPAPISSE := HandleSSE;
Gemini._CreateContentStream(
  'gemini-2.0-flash',
  'Explain quantum entanglement',
  1024);

I modelli locali funzionano esattamente allo stesso modo. TsgcHTTP_API_Ollama non richiede alcuna chiave API — punta OllamaOptions.BaseUrl su http://localhost:11434/api e chiama _CreateMessageStream, e il modello aperto sul tuo hardware viene trasmesso in streaming attraverso lo stesso gestore OnHTTPAPISSE:

Ollama.OllamaOptions.BaseUrl := 'http://localhost:11434/api';
Ollama.OnHTTPAPISSE := HandleSSE;
Ollama._CreateMessageStream('llama3', 'Summarise RFC 6455');

Quattro provider, un evento, una chiamata di metodo ciascuno. Cambiare il backend di streaming è una modifica localizzata, non una riscrittura.

Aggiornare la UI in sicurezza

Un paio di note pratiche per il gestore. Primo, mantieni piccolo il lavoro all'interno di OnHTTPAPISSE — aggiungi il delta e ritorna. Un'elaborazione pesante per ogni token renderà lo stream a scatti, quindi accumula il testo ed esegui la formattazione costosa una volta che lo stream è terminato. Secondo, fai attenzione al contesto del thread. Se avvii la richiesta da un thread in background, l'evento SSE viene generato su quel thread, e toccare i controlli VCL o FMX al di fuori del thread principale non è sicuro. In tal caso reindirizza l'aggiornamento con TThread.Synchronize (o TThread.Queue per un'aggiunta non bloccante):

procedure TForm1.HandleSSE(Sender: TObject;
  const aEvent, aData: string; var Cancel: Boolean);
begin
  TThread.Queue(nil,
    procedure
    begin
      Memo1.SelStart := Length(Memo1.Text);
      Memo1.SelText := aData; // append at the caret, no full repaint
    end);
end;

Aggiungere con SelText invece di Lines.Add evita di ricomporre l'intera Memo a ogni token, il che mantiene fluido uno stream lungo. Se chiami l'API dal thread principale, puoi eliminare il wrapper TThread.Queue e aggiornare il controllo direttamente.

Come iniziare

Tutti questi componenti sono inclusi in sgcWebSockets. Scarica la prova gratuita, inserisci il componente per il provider che desideri, assegna OnHTTPAPISSE e chiama il metodo di streaming — avrai una UI token per token in poche righe. Consulta la pagina del componente OpenAI e la pagina del componente Anthropic per il riferimento completo dei metodi, oppure esplora ogni modello nell'hub dei componenti AI & LLM.

Domande o feedback? Contattaci — riceverai una risposta dalle persone che hanno scritto il codice.

Correlati