LLM-Antworten in Delphi streamen: Token für Token mit Server-Sent Events

· Komponenten

Kurze Antwort: Um eine LLM-Antwort in Delphi mit sgcWebSockets zu streamen, weisen Sie das OnHTTPAPISSE-Ereignis der Komponente zu und rufen dann die Streaming-Methode auf — _CreateChatCompletion mit Stream := True für OpenAI, _CreateMessageStream für Anthropic Claude und Ollama, _CreateContentStream für Gemini. Jedes Delta trifft im Handler als Server-Sent Event ein; hängen Sie aData an ein Memo an, und die Antwort tippt sich selbst aus, während das Modell sie erzeugt.

Ein nicht-streamender LLM-Aufruf blockiert, bis die gesamte Antwort fertig ist. Für eine einzeilige Antwort ist das in Ordnung, aber bei einer langen Vervollständigung starrt der Benutzer mehrere Sekunden lang auf ein eingefrorenes Fenster ohne jede Rückmeldung. Streaming behebt das: Das Modell gibt seine Ausgabe als eine Folge kleiner Chunks über eine einzige HTTP-Verbindung zurück, und Sie rendern jeden Chunk in dem Moment, in dem er eintrifft. Das Ergebnis ist der vertraute „Schreib"-Effekt, den Sie aus ChatGPT kennen, und eine Oberfläche, die sich reaktionsschnell anfühlt, selbst wenn die vollständige Antwort eine Weile dauert.

Wie Streaming funktioniert: OnHTTPAPISSE

Im Hintergrund streamt jede KI-Komponente von sgcWebSockets mit Server-Sent Events (SSE). Der Anbieter hält die Antwort offen und schiebt Ereignisse, sobald Tokens erzeugt werden. Die Komponente parst diese Ereignisse und löst pro Chunk ein Ereignis über einen einzigen Handler aus, mit derselben Signatur auf jeder Komponente:

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;

Das ist der gesamte Vertrag. aData trägt die inkrementelle Nutzlast, aEvent teilt Ihnen mit, welche Art von Ereignis es ist (nützlich bei Anbietern, die mehrere Ereignistypen ausgeben), und das Setzen von Cancel := True stoppt den Stream, zum Beispiel wenn der Benutzer auf eine Stopp-Schaltfläche klickt. Dieselbe Handler-Form funktioniert für OpenAI, Anthropic, Gemini und Ollama, sodass Sie sie nach einmaligem Schreiben über Anbieter hinweg wiederverwenden können.

OpenAI: Stream := True

Mit TsgcHTTP_API_OpenAI entscheiden Sie sich in der Anfrage für Streaming und binden das Ereignis ein. Setzen Sie OpenAIOptions.ApiKey, weisen Sie OnHTTPAPISSE zu, und die Chat-Vervollständigung wird Chunk für Chunk statt in einem Block geliefert:

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;

Die typisierte Anfrage stellt eine Stream-Eigenschaft bereit, sodass Sie beim eigenen Aufbau des Anfrageobjekts Stream := True vor dem Senden setzen. Tokens tauchen dann über OnHTTPAPISSE auf, sobald sie erzeugt werden, und Sie hängen jeden an Ihr Memo an.

Anthropic Claude: _CreateMessageStream

Claude stellt einen eigenen Streaming-Helfer bereit, sodass es kein separates Flag zum Umschalten gibt: Der Aufruf von _CreateMessageStream auf TsgcHTTP_API_Anthropic aktiviert SSE für diese Anfrage. Setzen Sie API-Schlüssel und Version, weisen Sie den Handler zu und rufen Sie es auf:

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 gibt während des Streamens mehrere SSE-Ereignistypen aus (Inhaltsblöcke beginnen, Deltas, dann Stopp). Das Argument aEvent erlaubt es Ihnen, sie auseinanderzuhalten, falls nötig; für eine einfache „Text anzeigen, sobald er eintrifft"-Oberfläche genügt das Anhängen von aData.

Gemini und Ollama: dieselbe Form

Google Gemini folgt dem identischen Muster mit seiner eigenen Streaming-Methode _CreateContentStream auf TsgcHTTP_API_Gemini:

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

Lokale Modelle laufen genau auf dieselbe Weise. TsgcHTTP_API_Ollama benötigt keinen API-Schlüssel — richten Sie OllamaOptions.BaseUrl auf http://localhost:11434/api und rufen Sie _CreateMessageStream auf, und das offene Modell auf Ihrer eigenen Hardware streamt über denselben OnHTTPAPISSE-Handler zurück:

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

Vier Anbieter, ein Ereignis, je ein Methodenaufruf. Das Umstellen des Streaming-Backends ist eine lokale Änderung, kein Neuschreiben.

Die Oberfläche sicher aktualisieren

Ein paar praktische Hinweise für den Handler. Erstens: Halten Sie die Arbeit innerhalb von OnHTTPAPISSE klein — hängen Sie das Delta an und kehren Sie zurück. Aufwändige Verarbeitung pro Token lässt den Stream ruckeln, sammeln Sie also Text und führen Sie teure Formatierungen erst durch, wenn der Stream abgeschlossen ist. Zweitens: Achten Sie auf den Thread-Kontext. Wenn Sie die Anfrage aus einem Hintergrundthread starten, wird das SSE-Ereignis in diesem Thread ausgelöst, und das Berühren von VCL- oder FMX-Steuerelementen außerhalb des Hauptthreads ist nicht sicher. Marschallen Sie die Aktualisierung in diesem Fall mit TThread.Synchronize zurück (oder TThread.Queue für ein nicht-blockierendes Anhängen):

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;

Das Anhängen mit SelText statt Lines.Add vermeidet ein Neufließen des gesamten Memo bei jedem Token, was einen langen Stream flüssig hält. Wenn Sie die API aus dem Hauptthread aufrufen, können Sie die TThread.Queue-Hülle weglassen und das Steuerelement direkt aktualisieren.

Erste Schritte

Alle diese Komponenten sind in sgcWebSockets enthalten. Holen Sie sich die kostenlose Testversion, fügen Sie die Komponente für den gewünschten Anbieter ein, weisen Sie OnHTTPAPISSE zu und rufen Sie die Streaming-Methode auf — Sie haben eine Token-für-Token-Oberfläche in wenigen Zeilen. Siehe die Seite zur OpenAI-Komponente und die Seite zur Anthropic-Komponente für die vollständige Methodenreferenz oder durchsuchen Sie jedes Modell im Hub für KI- & LLM-Komponenten.

Fragen oder Feedback? Kontaktieren Sie uns — Sie erhalten eine Antwort von den Leuten, die den Code geschrieben haben.

Verwandte Themen