Diffuser les réponses des LLM en Delphi : jeton par jeton avec les Server-Sent Events

· Composants

Réponse rapide : pour diffuser une réponse de LLM en Delphi avec sgcWebSockets, affectez l'événement OnHTTPAPISSE du composant, puis appelez la méthode de diffusion — _CreateChatCompletion avec Stream := True pour OpenAI, _CreateMessageStream pour Anthropic Claude et Ollama, _CreateContentStream pour Gemini. Chaque delta arrive dans le gestionnaire sous forme de Server-Sent Event ; ajoutez aData à un Memo et la réponse se rédige d'elle-même à mesure que le modèle la génère.

Un appel LLM sans diffusion bloque jusqu'à ce que la réponse complète soit prête. Pour une réponse d'un paragraphe, c'est acceptable, mais pour une longue complétion l'utilisateur fixe une fenêtre figée pendant plusieurs secondes sans aucun retour. La diffusion corrige cela : le modèle renvoie sa sortie sous forme d'une séquence de petits fragments sur une seule connexion HTTP, et vous affichez chaque fragment dès qu'il arrive. Le résultat est le familier effet de « frappe » que vous voyez dans ChatGPT, et une interface qui paraît réactive même lorsque la réponse complète prend du temps.

Comment fonctionne la diffusion : OnHTTPAPISSE

En interne, chaque composant IA de sgcWebSockets diffuse en utilisant les Server-Sent Events (SSE). Le fournisseur maintient la réponse ouverte et pousse des événements à mesure que les jetons sont produits. Le composant analyse ces événements et déclenche un événement par fragment via un seul gestionnaire ayant la même signature sur chaque composant :

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;

C'est tout le contrat. aData transporte la charge utile incrémentale, aEvent vous indique de quel type d'événement il s'agit (utile avec les fournisseurs qui émettent plusieurs types d'événements), et définir Cancel := True arrête la diffusion, par exemple lorsque l'utilisateur clique sur un bouton Arrêter. La même forme de gestionnaire fonctionne pour OpenAI, Anthropic, Gemini et Ollama, donc une fois que vous l'avez écrite, vous pouvez la réutiliser pour tous les fournisseurs.

OpenAI : Stream := True

Avec TsgcHTTP_API_OpenAI, vous activez la diffusion sur la requête et vous raccordez l'événement. Définissez OpenAIOptions.ApiKey, affectez OnHTTPAPISSE, et la complétion de discussion est livrée fragment par fragment au lieu d'un seul bloc :

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 requête typée expose une propriété Stream, donc lorsque vous construisez l'objet de requête vous-même, vous définissez Stream := True avant l'envoi. Les jetons remontent ensuite via OnHTTPAPISSE à mesure qu'ils sont générés, et vous ajoutez chacun à votre Memo.

Anthropic Claude : _CreateMessageStream

Claude expose une méthode d'aide à la diffusion dédiée, il n'y a donc pas de drapeau séparé à activer : appeler _CreateMessageStream sur TsgcHTTP_API_Anthropic active le SSE pour cette requête. Définissez la clé d'API et la version, affectez le gestionnaire, et appelez-la :

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 émet plusieurs types d'événements SSE au cours de la diffusion (début des blocs de contenu, deltas, puis arrêt). L'argument aEvent vous permet de les distinguer si nécessaire ; pour une interface simple du type « afficher le texte au fur et à mesure », ajouter aData suffit.

Gemini et Ollama : la même forme

Google Gemini suit le schéma identique avec sa propre méthode de diffusion, _CreateContentStream sur TsgcHTTP_API_Gemini :

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

Les modèles locaux fonctionnent exactement de la même manière. TsgcHTTP_API_Ollama ne nécessite aucune clé d'API — pointez OllamaOptions.BaseUrl vers http://localhost:11434/api et appelez _CreateMessageStream, et le modèle ouvert sur votre propre matériel diffuse en retour via le même gestionnaire OnHTTPAPISSE :

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

Quatre fournisseurs, un seul événement, un seul appel de méthode pour chacun. Changer de backend de diffusion est une modification localisée, pas une réécriture.

Mettre à jour l'interface en toute sécurité

Quelques notes pratiques pour le gestionnaire. D'abord, gardez le travail réalisé dans OnHTTPAPISSE minimal — ajoutez le delta et revenez. Un traitement lourd par jeton rendra la diffusion saccadée, accumulez donc le texte et effectuez le formatage coûteux une fois la diffusion terminée. Ensuite, faites attention au contexte de thread. Si vous lancez la requête depuis un thread d'arrière-plan, l'événement SSE se déclenche sur ce thread, et toucher des contrôles VCL ou FMX hors du thread principal n'est pas sûr. Dans ce cas, redirigez la mise à jour avec TThread.Synchronize (ou TThread.Queue pour un ajout non bloquant) :

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;

Ajouter avec SelText plutôt que Lines.Add évite de réagencer tout le Memo à chaque jeton, ce qui maintient une longue diffusion fluide. Si vous appelez l'API depuis le thread principal, vous pouvez supprimer l'enveloppe TThread.Queue et mettre à jour le contrôle directement.

Pour commencer

Tous ces composants sont fournis avec sgcWebSockets. Récupérez la version d'essai gratuite, déposez le composant du fournisseur souhaité, affectez OnHTTPAPISSE et appelez la méthode de diffusion — vous aurez une interface jeton par jeton en quelques lignes. Consultez la page du composant OpenAI et la page du composant Anthropic pour la référence complète des méthodes, ou parcourez tous les modèles sur le hub des composants IA et LLM.

Des questions ou des retours ? Contactez-nous — vous recevrez une réponse des personnes qui ont écrit le code.

Sur le même thème