Strumieniowanie odpowiedzi LLM w Delphi: token po tokenie z Server-Sent Events

· Komponenty

Szybka odpowiedź: aby strumieniować odpowiedź LLM w Delphi z sgcWebSockets, przypisz zdarzenie komponentu OnHTTPAPISSE, a następnie wywołaj metodę strumieniującą — _CreateChatCompletion z Stream := True dla OpenAI, _CreateMessageStream dla Anthropic Claude i Ollama, _CreateContentStream dla Gemini. Każda delta trafia do procedury obsługi jako Server-Sent Event; dołącz aData do komponentu Memo, a odpowiedź sama się wypisuje, w miarę jak model ją generuje.

Wywołanie LLM bez strumieniowania blokuje aż do momentu, gdy cała odpowiedź jest gotowa. Dla jednoakapitowej odpowiedzi jest to w porządku, ale przy długim uzupełnieniu użytkownik wpatruje się w zamarznięte okno przez kilka sekund bez żadnej informacji zwrotnej. Strumieniowanie to naprawia: model zwraca swoje wyjście jako sekwencję małych porcji przez pojedyncze połączenie HTTP, a Ty renderujesz każdą porcję w chwili, gdy się pojawia. Efektem jest znany efekt „pisania”, który widać w ChatGPT, oraz interfejs, który wydaje się responsywny, nawet gdy pełna odpowiedź zajmuje chwilę.

Jak działa strumieniowanie: OnHTTPAPISSE

Pod maską każdy komponent AI sgcWebSockets strumieniuje za pomocą Server-Sent Events (SSE). Dostawca utrzymuje otwartą odpowiedź i wysyła zdarzenia w miarę generowania tokenów. Komponent parsuje te zdarzenia i zgłasza jedno zdarzenie na porcję przez pojedynczą procedurę obsługi o tej samej sygnaturze w każdym komponencie:

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;

To cały kontrakt. aData niesie przyrostowy ładunek, aEvent mówi, jakiego rodzaju jest to zdarzenie (przydatne u dostawców, którzy emitują kilka typów zdarzeń), a ustawienie Cancel := True zatrzymuje strumień, na przykład gdy użytkownik kliknie przycisk Stop. Ten sam kształt procedury obsługi działa dla OpenAI, Anthropic, Gemini i Ollama, więc gdy raz ją napiszesz, możesz ją wykorzystać ponownie u różnych dostawców.

OpenAI: Stream := True

Z TsgcHTTP_API_OpenAI włączasz strumieniowanie w żądaniu i podpinasz zdarzenie. Ustaw OpenAIOptions.ApiKey, przypisz OnHTTPAPISSE, a uzupełnienie czatu jest dostarczane porcja po porcji zamiast w jednym bloku:

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;

Typowane żądanie udostępnia właściwość Stream, więc gdy sam budujesz obiekt żądania, ustawiasz Stream := True przed wysłaniem. Tokeny wypływają wtedy przez OnHTTPAPISSE w miarę ich generowania, a Ty dołączasz każdy z nich do swojego komponentu Memo.

Anthropic Claude: _CreateMessageStream

Claude udostępnia dedykowaną metodę pomocniczą do strumieniowania, więc nie ma osobnej flagi do przełączenia: wywołanie _CreateMessageStream na TsgcHTTP_API_Anthropic włącza SSE dla tego żądania. Ustaw klucz API i wersję, przypisz procedurę obsługi i wywołaj ją:

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 emituje kilka typów zdarzeń SSE podczas strumieniowania (start bloków treści, delty, a potem stop). Argument aEvent pozwala je rozróżnić, jeśli potrzebujesz; dla prostego interfejsu „pokaż tekst w miarę napływania” wystarczy dołączać aData.

Gemini i Ollama: ten sam kształt

Google Gemini stosuje identyczny wzorzec z własną metodą strumieniującą, _CreateContentStream na TsgcHTTP_API_Gemini:

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

Modele lokalne działają dokładnie tak samo. TsgcHTTP_API_Ollama nie potrzebuje klucza API — wskaż OllamaOptions.BaseUrl na http://localhost:11434/api i wywołaj _CreateMessageStream, a otwarty model na Twoim własnym sprzęcie strumieniuje z powrotem przez tę samą procedurę obsługi OnHTTPAPISSE:

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

Czterej dostawcy, jedno zdarzenie, po jednym wywołaniu metody każdy. Zmiana zaplecza strumieniowania to lokalna edycja, a nie przepisywanie od nowa.

Bezpieczna aktualizacja interfejsu

Kilka praktycznych uwag dotyczących procedury obsługi. Po pierwsze, utrzymuj pracę wewnątrz OnHTTPAPISSE jako niewielką — dołącz deltę i wyjdź. Ciężkie przetwarzanie na każdy token sprawi, że strumień będzie odczuwany jako poszarpany, więc gromadź tekst i wykonuj kosztowne formatowanie raz, gdy strumień się zakończy. Po drugie, miej na uwadze kontekst wątku. Jeśli uruchamiasz żądanie z wątku w tle, zdarzenie SSE wyzwala się w tym wątku, a dotykanie kontrolek VCL lub FMX poza głównym wątkiem nie jest bezpieczne. W takim przypadku przekaż aktualizację z powrotem za pomocą TThread.Synchronize (lub TThread.Queue dla nieblokującego dołączania):

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;

Dołączanie za pomocą SelText zamiast Lines.Add unika ponownego rozkładania całego komponentu Memo przy każdym tokenie, co utrzymuje długi strumień płynnym. Jeśli wywołujesz API z głównego wątku, możesz pominąć opakowanie TThread.Queue i aktualizować kontrolkę bezpośrednio.

Jak zacząć

Wszystkie te komponenty są dostarczane w sgcWebSockets. Pobierz bezpłatną wersję próbną, upuść komponent dla wybranego dostawcy, przypisz OnHTTPAPISSE i wywołaj metodę strumieniującą — będziesz mieć interfejs działający token po tokenie w kilku liniach. Zobacz stronę komponentu OpenAI i stronę komponentu Anthropic po pełną dokumentację metod albo przejrzyj każdy model w centrum komponentów AI i LLM.

Pytania lub uwagi? Skontaktuj się z nami — odpowiedź otrzymasz od ludzi, którzy napisali ten kod.

Powiązane