Delphi에서 LLM 응답 스트리밍: Server-Sent Events로 토큰 단위 출력

· 컴포넌트

빠른 답변: sgcWebSockets로 Delphi에서 LLM 응답을 스트리밍하려면, 컴포넌트의 OnHTTPAPISSE 이벤트를 할당한 다음 스트리밍 메서드를 호출하세요 — OpenAI는 Stream := True를 지정한 _CreateChatCompletion, Anthropic Claude와 Ollama는 _CreateMessageStream, Gemini는 _CreateContentStream입니다. 각 델타는 Server-Sent Event로 핸들러에 도착합니다. aDataMemo에 덧붙이면 모델이 생성하는 대로 답변이 스스로 타이핑되어 나옵니다.

스트리밍하지 않는 LLM 호출은 전체 답변이 준비될 때까지 블록됩니다. 한 문단짜리 응답에는 괜찮지만, 긴 완성에는 사용자가 아무런 피드백 없이 몇 초간 멈춘 창을 바라보게 됩니다. 스트리밍이 이를 해결합니다. 모델은 출력을 하나의 HTTP 연결을 통해 작은 청크의 연속으로 반환하고, 당신은 각 청크가 도착하는 순간 렌더링합니다. 그 결과가 ChatGPT에서 보는 익숙한 "타이핑" 효과이며, 전체 답변에 시간이 걸려도 반응이 빠르게 느껴지는 UI입니다.

스트리밍이 작동하는 방식: OnHTTPAPISSE

내부적으로 모든 sgcWebSockets AI 컴포넌트는 Server-Sent Events(SSE)를 사용해 스트리밍합니다. 제공자는 응답을 열어 두고 토큰이 생성되는 대로 이벤트를 푸시합니다. 컴포넌트는 그 이벤트를 파싱하여, 모든 컴포넌트에서 동일한 시그니처를 갖는 하나의 핸들러를 통해 청크마다 하나의 이벤트를 발생시킵니다.

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;

그것이 전체 계약입니다. aData는 증분 페이로드를 담고, aEvent는 어떤 종류의 이벤트인지 알려주며(여러 이벤트 유형을 내보내는 제공자에 유용), Cancel := True를 설정하면 스트림을 멈춥니다. 예를 들어 사용자가 Stop 버튼을 클릭할 때입니다. 같은 핸들러 형태가 OpenAI, Anthropic, Gemini, Ollama에 모두 작동하므로, 한 번 작성하면 제공자 간에 재사용할 수 있습니다.

OpenAI: Stream := True

TsgcHTTP_API_OpenAI에서는 요청에서 스트리밍을 선택하고 이벤트를 연결합니다. OpenAIOptions.ApiKey를 설정하고, OnHTTPAPISSE를 할당하면, 채팅 완성이 하나의 블록 대신 청크 단위로 전달됩니다.

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;

타입이 지정된 요청은 Stream 속성을 노출하므로, 요청 객체를 직접 구성할 때 전송하기 전에 Stream := True를 설정합니다. 그러면 토큰이 생성되는 대로 OnHTTPAPISSE를 통해 나타나고, 각 토큰을 Memo에 덧붙이면 됩니다.

Anthropic Claude: _CreateMessageStream

Claude는 전용 스트리밍 헬퍼를 노출하므로 따로 켤 플래그가 없습니다. TsgcHTTP_API_Anthropic에서 _CreateMessageStream을 호출하면 그 요청에 SSE가 켜집니다. API 키와 버전을 설정하고, 핸들러를 할당한 다음 호출하세요.

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는 스트리밍하면서 여러 SSE 이벤트 유형을 내보냅니다(콘텐츠 블록 시작, 델타, 그다음 정지). 필요하다면 aEvent 인자로 이들을 구분할 수 있습니다. 단순히 "도착하는 대로 텍스트를 보여주는" UI에는 aData를 덧붙이는 것으로 충분합니다.

Gemini와 Ollama: 같은 형태

Google Gemini는 자체 스트리밍 메서드인 TsgcHTTP_API_Gemini_CreateContentStream으로 동일한 패턴을 따릅니다.

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

로컬 모델도 정확히 같은 방식으로 동작합니다. TsgcHTTP_API_Ollama는 API 키가 필요 없습니다 — OllamaOptions.BaseUrlhttp://localhost:11434/api로 가리키고 _CreateMessageStream을 호출하면, 자신의 하드웨어에서 실행되는 오픈 모델이 같은 OnHTTPAPISSE 핸들러를 통해 스트리밍되어 돌아옵니다.

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

네 개의 제공자, 하나의 이벤트, 각각 하나의 메서드 호출. 스트리밍 백엔드를 전환하는 것은 재작성이 아니라 국소적인 편집입니다.

UI를 안전하게 업데이트하기

핸들러에 대한 몇 가지 실용적인 메모가 있습니다. 첫째, OnHTTPAPISSE 안의 작업은 작게 유지하세요 — 델타를 덧붙이고 반환하세요. 토큰마다 무거운 처리를 하면 스트림이 끊기는 느낌을 주므로, 텍스트를 누적해 두고 비용이 큰 서식 처리는 스트림이 끝난 뒤 한 번에 하세요. 둘째, 스레드 컨텍스트에 유의하세요. 백그라운드 스레드에서 요청을 시작하면 SSE 이벤트가 그 스레드에서 발생하며, 메인 스레드가 아닌 곳에서 VCL이나 FMX 컨트롤을 건드리는 것은 안전하지 않습니다. 그런 경우 TThread.Synchronize(또는 논블로킹 덧붙임에는 TThread.Queue)로 업데이트를 되돌려 마샬링하세요.

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;

Lines.Add 대신 SelText로 덧붙이면 토큰마다 전체 Memo를 다시 흐르게 하는 것을 피할 수 있어, 긴 스트림이 매끄럽게 유지됩니다. 메인 스레드에서 API를 호출한다면 TThread.Queue 래퍼를 빼고 컨트롤을 직접 업데이트할 수 있습니다.

시작하기

이 컴포넌트들은 모두 sgcWebSockets에 포함되어 있습니다. 무료 평가판을 받아서, 원하는 제공자의 컴포넌트를 놓고, OnHTTPAPISSE를 할당하고 스트리밍 메서드를 호출하세요 — 몇 줄로 토큰 단위 UI를 갖게 됩니다. 전체 메서드 레퍼런스는 OpenAI 컴포넌트 페이지Anthropic 컴포넌트 페이지를 보거나, 모든 모델은 AI & LLM 컴포넌트 허브에서 둘러보세요.

질문이나 피드백이 있나요? 문의하기 — 코드를 작성한 사람들로부터 답변을 받게 됩니다.

관련 글