Respuestas de LLM en streaming en Delphi: token a token con Server-Sent Events

· Componentes

Respuesta rápida: para transmitir una respuesta de un LLM en Delphi con sgcWebSockets, asigna el evento OnHTTPAPISSE del componente y luego llama al método de streaming — _CreateChatCompletion con Stream := True para OpenAI, _CreateMessageStream para Anthropic Claude y Ollama, _CreateContentStream para Gemini. Cada delta llega al manejador como un Server-Sent Event; añade aData a un Memo y la respuesta se va escribiendo sola a medida que el modelo la genera.

Una llamada a un LLM sin streaming se bloquea hasta que toda la respuesta está lista. Para una respuesta de un párrafo eso está bien, pero para una respuesta larga el usuario se queda mirando una ventana congelada durante varios segundos sin ninguna señal. El streaming soluciona eso: el modelo devuelve su salida como una secuencia de pequeños fragmentos sobre una única conexión HTTP, y tú renderizas cada fragmento en el momento en que llega. El resultado es el familiar efecto de "escritura" que ves en ChatGPT, y una interfaz que se siente reactiva incluso cuando la respuesta completa tarda un rato.

Cómo funciona el streaming: OnHTTPAPISSE

Por debajo, cada componente de IA de sgcWebSockets transmite usando Server-Sent Events (SSE). El proveedor mantiene la respuesta abierta y envía eventos a medida que se producen los tokens. El componente analiza esos eventos y lanza un evento por fragmento a través de un único manejador con la misma firma en cada 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;

Ese es todo el contrato. aData transporta la carga útil incremental, aEvent te indica de qué tipo de evento se trata (útil con proveedores que emiten varios tipos de evento), y establecer Cancel := True detiene el stream, por ejemplo cuando el usuario pulsa un botón de Detener. La misma forma de manejador funciona para OpenAI, Anthropic, Gemini y Ollama, así que una vez que lo has escrito una vez puedes reutilizarlo entre proveedores.

OpenAI: Stream := True

Con TsgcHTTP_API_OpenAI activas el streaming en la petición y enganchas el evento. Establece OpenAIOptions.ApiKey, asigna OnHTTPAPISSE, y la respuesta del chat se entrega fragmento a fragmento en lugar de en un solo bloque:

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 petición tipada expone una propiedad Stream, así que cuando construyes el objeto de petición tú mismo estableces Stream := True antes de enviarlo. Los tokens entonces afloran a través de OnHTTPAPISSE a medida que se generan, y añades cada uno a tu Memo.

Anthropic Claude: _CreateMessageStream

Claude expone un asistente de streaming dedicado, así que no hay un flag aparte que activar: llamar a _CreateMessageStream en TsgcHTTP_API_Anthropic activa SSE para esa petición. Establece la clave de API y la versión, asigna el manejador y llámalo:

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 emite varios tipos de evento SSE a medida que transmite (inicio de bloques de contenido, deltas, y luego parada). El argumento aEvent te permite distinguirlos si lo necesitas; para una interfaz sencilla del tipo "muestra el texto a medida que llega", basta con añadir aData.

Gemini y Ollama: la misma forma

Google Gemini sigue el patrón idéntico con su propio método de streaming, _CreateContentStream en TsgcHTTP_API_Gemini:

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

Los modelos locales funcionan exactamente igual. TsgcHTTP_API_Ollama no necesita clave de API — apunta OllamaOptions.BaseUrl a http://localhost:11434/api y llama a _CreateMessageStream, y el modelo abierto en tu propio hardware transmite de vuelta a través del mismo manejador OnHTTPAPISSE:

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

Cuatro proveedores, un evento, una llamada a método cada uno. Cambiar el backend de streaming es una edición localizada, no una reescritura.

Actualizar la interfaz de forma segura

Un par de notas prácticas para el manejador. Primero, mantén pequeño el trabajo dentro de OnHTTPAPISSE — añade el delta y retorna. Un procesamiento pesado por token hará que el stream se sienta entrecortado, así que acumula el texto y haz el formateo costoso una vez que el stream termine. Segundo, ten en cuenta el contexto del hilo. Si inicias la petición desde un hilo en segundo plano, el evento SSE se dispara en ese hilo, y tocar controles VCL o FMX fuera del hilo principal no es seguro. En ese caso, devuelve la actualización al hilo principal con TThread.Synchronize (o TThread.Queue para un añadido no bloqueante):

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;

Añadir con SelText en lugar de Lines.Add evita reformatear todo el Memo en cada token, lo que mantiene fluido un stream largo. Si llamas a la API desde el hilo principal, puedes prescindir del envoltorio TThread.Queue y actualizar el control directamente.

Primeros pasos

Todos estos componentes vienen incluidos en sgcWebSockets. Consigue la prueba gratuita, coloca el componente del proveedor que quieras, asigna OnHTTPAPISSE y llama al método de streaming — tendrás una interfaz token a token en unas pocas líneas. Consulta la página del componente OpenAI y la página del componente Anthropic para la referencia completa de métodos, o explora todos los modelos en el hub de componentes de IA y LLM.

¿Preguntas o comentarios? Ponte en contacto — recibirás una respuesta de las personas que escribieron el código.

Relacionado