Net-Base Dergi

15.07.2026

ChatGPT API'yi Delphi FMX/VCL ile sağlam bağlama: akış (streaming), yeniden deneme (retry) ve temiz JSON ayrıştırma

ChatGPT API'yi Delphi FMX/VCL ile şu şekilde güvenilir biçimde bağlayın: zaman aşımı ve yeniden denemeyi destekleyen HTTP istemcisi, UI donmalarına yol açmayan SSE akışı ve araç çağrıları ile hata durumları için sağlam JSON ayrıştırma.

15.07.2026

Dergi konusundan proje pratiğine

İçeriğe Uygun Hizmet ve Teknik Sayfalar

Neden „ChatGPT API ile Delphi FMX/VCL“ pratikte sadece bir POST değildir

ChatGPT API ile Delphi FMX/VCL entegrasyonu yapmak isteyenler çabuk basit bir HTTP-POST ile karşılaşır. Ancak gerçek iş yazılımı ortamlarında bu üç noktada tökezler: (1) Zaman aşımı (Timeouts) ve yeniden denemeler (Retries) deterministik olmalı; aksi takdirde kullanıcılar donmuş bir UI ile karşılaşır, (2) Streaming (Server-Sent Events, kısaca SSE) iyi bir UX için sıkça mantıklıdır ama Delphi-Threading içinde hızla hata eğilimli hale gelir, ve (3) JSON sadece „bir nesne“ değildir: hata mesajları, kota problemleri, boş alanlar veya hafifçe değişmiş yanıt biçimleri sağlam biçimde ele alınmalıdır.

Aşağıdaki kaynak parçacığı FMX ve VCL’de aynı şekilde çalışan bir yaklaşımı gösterir: Tercihe bağlı olarak nicht-streaming veya streaming çalışan, UI güncellemelerini düzgün şekilde marshal eden (yani Main-Thread senkronizasyonu üzerinden icra eden) ve hatalarda anlamlı log üreten, kendi içinde test edilebilir bir istemci. Yanı sıra, büyümüş katmanlı yapılara kolayca oturacak şekilde tasarlanmıştır (ör. entegrasyon katmanındaki „API-Client“, UI ince kalır).

Mimari taslak: UI’yi ayırmak, istemciyi test edilebilir tutmak

Uzun geçmişli Delphi projelerinde sıkça „ButtonClick içinde HTTP“ görebilirsiniz. Bu ilk sorun ortaya çıkana kadar çalışır. Önerilen küçük bir istemci şu unsurlara sahip olmalıdır:

  • Yapılandırma: Base-URL, API anahtarı, model, zaman aşımları.
  • Taşıma katmanı: HTTP-Request/Response, yeniden deneme, zaman aşımı, Proxy/SSL seçenekleri (işletmeye göre).
  • Ayrıştırıcı: JSON çözümleme, hata nesneleri, sonuç çıkarımı.
  • UI-Hooks: Token-/metin streaming için callback’ler, ancak VCL/FMX kontrollerine sıkı bağımlılık olmadan.

Böylece entegrasyon bireysel kurumsal yazılıma temiz şekilde uygulanabilir: İstemci servislerde, masaüstü istemcilerde, admin araçlarında veya test-harness’lerde yeniden kullanılabilir.

Kaynak örneği: Delphi-Client ile SSE-Streaming, Timeout/Retry ve sağlam JSON

Kod THTTPClient (System.Net.HttpClient) kullanıyor ve kasıtlı olarak sadece minimal düzeyde System.JSON ile parse ediyor. SSE için satır satır okunuyor ve „data: …“ satırlarına tepki veriliyor. Bu bir „WebSocket“ değil, sürekli metin satırları sağlayan bir HTTP-Response-Stream’dir. Önemli: Okumayı bir worker-thread içinde yapıyoruz ve UI güncellemelerini Main Thread’e marshal ediyoruz.

Delphi
unit Net-Base.OpenAI.ChatClient;

interface

uses
  System.SysUtils, System.Classes, System.Net.URLClient, System.Net.HttpClient,
  System.Net.HttpClientComponent, System.JSON, System.Threading,
  System.SyncObjs;

type
  EChatApiError = class(Exception)
  private
    FHttpStatus: Integer;
    FResponseText: string;
  public
    constructor Create(const Msg: string; AHttpStatus: Integer; const AResponseText: string);
    property HttpStatus: Integer read FHttpStatus;
    property ResponseText: string read FResponseText;
  end;

  TChatStreamEvent = reference to procedure(const AChunkText: string; AIsFinal: Boolean);

  TChatCompletionOptions = record
    Model: string;
    Temperature: Double;
    MaxTokens: Integer;
    constructor Create(const AModel: string; ATemperature: Double = 0.2; AMaxTokens: Integer = 512);
  end;

  TOpenAIChatClient = class
  private
    FBaseUrl: string;
    FApiKey: string;
    FConnectTimeoutMs: Integer;
    FResponseTimeoutMs: Integer;

    function BuildChatRequestBody(const AUserPrompt: string; const AOptions: TChatCompletionOptions;
      AStream: Boolean): TJSONObject;

    function ExtractTextFromNonStreamingResponse(const AJsonText: string): string;
    function TryExtractErrorMessage(const AJsonText: string; out AMessage: string): Boolean;

    procedure ApplyAuthHeaders(ARequest: IHTTPRequest);

    function ExecuteWithRetry(const ADoRequest: TFunc): IHTTPResponse;
  public
    constructor Create(const ABaseUrl, AApiKey: string);

    property ConnectTimeoutMs: Integer read FConnectTimeoutMs write FConnectTimeoutMs;
    property ResponseTimeoutMs: Integer read FResponseTimeoutMs write FResponseTimeoutMs;

    function ChatOnce(const AUserPrompt: string; const AOptions: TChatCompletionOptions): string;

    procedure ChatStream(const AUserPrompt: string; const AOptions: TChatCompletionOptions;
      const AOnEvent: TChatStreamEvent);
  end;

implementation

{ EChatApiError }

constructor EChatApiError.Create(const Msg: string; AHttpStatus: Integer; const AResponseText: string);
begin
  inherited Create(Msg);
  FHttpStatus := AHttpStatus;
  FResponseText := AResponseText;
end;

{ TChatCompletionOptions }

constructor TChatCompletionOptions.Create(const AModel: string; ATemperature: Double; AMaxTokens: Integer);
begin
  Model := AModel;
  Temperature := ATemperature;
  MaxTokens := AMaxTokens;
end;

{ TOpenAIChatClient }

constructor TOpenAIChatClient.Create(const ABaseUrl, AApiKey: string);
begin
  inherited Create;
  FBaseUrl := ABaseUrl.TrimRight(['/']);
  FApiKey := AApiKey;
  FConnectTimeoutMs := 8000;
  FResponseTimeoutMs := 60000;
end;

procedure TOpenAIChatClient.ApplyAuthHeaders(ARequest: IHTTPRequest);
begin
  // Bearer belirteci: burada API anahtarı „Authorization: Bearer …“ olarak.
  // Kurumsal ortamlarda anahtarların loglara yazılmamasına dikkat edin.
  ARequest.AddHeader('Authorization', 'Bearer ' + FApiKey);
  ARequest.AddHeader('Content-Type', 'application/json');
  ARequest.AddHeader('Accept', 'application/json');
end;

function TOpenAIChatClient.BuildChatRequestBody(const AUserPrompt: string;
  const AOptions: TChatCompletionOptions; AStream: Boolean): TJSONObject;
var
  Msgs: TJSONArray;
  Msg: TJSONObject;
begin
  Result := TJSONObject.Create;
  Result.AddPair('model', AOptions.Model);
  Result.AddPair('temperature', TJSONNumber.Create(AOptions.Temperature));
  Result.AddPair('max_tokens', TJSONNumber.Create(AOptions.MaxTokens));
  Result.AddPair('stream', TJSONBool.Create(AStream));

  // Minimum Messages yapısı (Chat Completions): role/content.
  Msgs := TJSONArray.Create;
  Msg := TJSONObject.Create;
  Msg.AddPair('role', 'user');
  Msg.AddPair('content', AUserPrompt);
  Msgs.AddElement(Msg);
  Result.AddPair('messages', Msgs);
end;

function TOpenAIChatClient.TryExtractErrorMessage(const AJsonText: string; out AMessage: string): Boolean;
var
  J: TJSONValue;
  EObj: TJSONObject;
begin
  Result := False;
  AMessage := '';

  J := TJSONObject.ParseJSONValue(AJsonText);
  try
    if (J is TJSONObject) then
    begin
      // Yaygın biçim: { "error": { "message": "...", "type": "..." } }
      EObj := (J as TJSONObject).GetValue<TJSONObject>('error');
      if Assigned(EObj) then
      begin
        AMessage := EObj.GetValue<string>('message', '');
        Result := AMessage <> '';
      end;
    end;
  finally
    J.Free;
  end;
end;

function TOpenAIChatClient.ExtractTextFromNonStreamingResponse(const AJsonText: string): string;
var
  J: TJSONValue;
  Root: TJSONObject;
  Choices: TJSONArray;
  Choice0: TJSONObject;
  Msg: TJSONObject;
begin
  Result := '';

  J := TJSONObject.ParseJSONValue(AJsonText);
  try
    if not (J is TJSONObject) then
      raise EChatApiError.Create('Beklenmeyen JSON yanıtı (nesne değil).', 0, AJsonText);

    Root := J as TJSONObject;
    Choices := Root.GetValue<TJSONArray>('choices');
    if (Choices = nil) or (Choices.Count = 0) then
      raise EChatApiError.Create('Beklenmeyen JSON yanıtı: choices eksik/boş.', 0, AJsonText);

    Choice0 := Choices.Items[0] as TJSONObject;
    // Chat Completions: choices[0].message.content
    Msg := Choice0.GetValue<TJSONObject>('message');
    if Msg = nil then
      raise EChatApiError.Create('Beklenmeyen JSON yanıtı: message eksik.', 0, AJsonText);

    Result := Msg.GetValue<string>('content', '');
  finally
    J.Free;
  end;
end;

function TOpenAIChatClient.ExecuteWithRetry(const ADoRequest: TFunc<IHTTPResponse>): IHTTPResponse;
const
  MaxAttempts = 3;
var
  Attempt: Integer;
  DelayMs: Integer;
begin
  DelayMs := 350;
  for Attempt := 1 to MaxAttempts do
  begin
    try
      Exit(ADoRequest());
    except
      on E: ENetHTTPClientException do
      begin
        // Ağ/TLS/zaman aşımı hataları: backoff ile basit yeniden deneme.
        if Attempt = MaxAttempts then
          raise;
        Sleep(DelayMs);
        DelayMs := DelayMs * 2;
      end;
    end;
  end;
  Result := nil;
end;

function TOpenAIChatClient.ChatOnce(const AUserPrompt: string; const AOptions: TChatCompletionOptions): string;
var
  Http: THTTPClient;
  Req: IHTTPRequest;
  Resp: IHTTPResponse;
  Body: TJSONObject;
  Payload: TStringStream;
  RespText: string;
  ErrMsg: string;
begin
  Http := THTTPClient.Create;
  try
    Http.ConnectionTimeout := FConnectTimeoutMs;
    Http.ResponseTimeout := FResponseTimeoutMs;

    Body := BuildChatRequestBody(AUserPrompt, AOptions, False);
    try
      Payload := TStringStream.Create(Body.ToJSON, TEncoding.UTF8);
      try
        Req := Http.GetRequest('POST', FBaseUrl + '/v1/chat/completions');
        ApplyAuthHeaders(Req);

        Resp := ExecuteWithRetry(
          function: IHTTPResponse
          begin
            Result := Http.Execute(Req, Payload);
          end
        );

        RespText := Resp.ContentAsString(TEncoding.UTF8);

        if (Resp.StatusCode < 200) or (Resp.StatusCode >= 300) then
        begin
          if TryExtractErrorMessage(RespText, ErrMsg) then
            raise EChatApiError.Create(ErrMsg, Resp.StatusCode, RespText)
          else
            raise EChatApiError.Create('HTTP hatası ' + Resp.StatusCode.ToString, Resp.StatusCode, RespText);
        end;

        Result := ExtractTextFromNonStreamingResponse(RespText);
      finally
        Payload.Free;
      end;
    finally
      Body.Free;
    end;
  finally
    Http.Free;
  end;
end;

procedure TOpenAIChatClient.ChatStream(const AUserPrompt: string; const AOptions: TChatCompletionOptions;
  const AOnEvent: TChatStreamEvent);
var
  Task: ITask;
begin
  // Streaming kasıtlı olarak asenkron başlatılır, böylece FMX/VCL engellenmez.
  Task := TTask.Run(
    procedure
    var
      Http: THTTPClient;
      Req: IHTTPRequest;
      Resp: IHTTPResponse;
      Body: TJSONObject;
      Payload: TStringStream;
      Stream: TStream;
      Reader: TStreamReader;
      Line, Data: string;
      J: TJSONValue;
      Delta, Choice0, Choices: TJSONValue;
      ContentChunk: string;
    begin
      Http := THTTPClient.Create;
      try
        Http.ConnectionTimeout := FConnectTimeoutMs;
        Http.ResponseTimeout := FResponseTimeoutMs;

        Body := BuildChatRequestBody(AUserPrompt, AOptions, True);
        try
          Payload := TStringStream.Create(Body.ToJSON, TEncoding.UTF8);
          try
            Req := Http.GetRequest('POST', FBaseUrl + '/v1/chat/completions');
            ApplyAuthHeaders(Req);
            Req.AddHeader('Accept', 'text/event-stream');

            Resp := ExecuteWithRetry(
              function: IHTTPResponse
              begin
                Result := Http.Execute(Req, Payload);
              end
            );

            if (Resp.StatusCode < 200) or (Resp.StatusCode >= 300) then
            begin
              // Streaming hatalarında içerik çoğunlukla yine JSON olur.
              TThread.Queue(nil,
                procedure
                begin
                  AOnEvent('Streaming başlatılamadı (HTTP ' + Resp.StatusCode.ToString + ').', True);
                end);
              Exit;
            end;

            Stream := Resp.ContentStream;
            Reader := TStreamReader.Create(Stream, TEncoding.UTF8, True, 4096, False);
            try
              while not Reader.EndOfStream do
              begin
                Line := Reader.ReadLine;
                if Line = '' then
                  Continue;

                // SSE formatı: satırlar örneğin "data: {...}" veya "data: [DONE]"
                if Line.StartsWith('data:') then
                begin
                  Data := Line.Substring(5).Trim;
                  if SameText(Data, '[DONE]') then
                  begin
                    TThread.Queue(nil,
                      procedure
                      begin
                        AOnEvent('', True);
                      end);
                    Break;
                  end;

                  // JSON parçasını değerlendirme: choices[0].delta.content
                  J := TJSONObject.ParseJSONValue(Data);
                  try
                    ContentChunk := '';
                    if J <> nil then
                    begin
                      Choices := (J as TJSONObject).GetValue('choices');
                      if (Choices is TJSONArray) and (TJSONArray(Choices).Count > 0) then
                      begin
                        Choice0 := TJSONArray(Choices).Items[0];
                        Delta := (Choice0 as TJSONObject).GetValue('delta');
                        if (Delta is TJSONObject) then
                          ContentChunk := TJSONObject(Delta).GetValue<string>('content', '');
                      end;
                    end;
                  finally
                    J.Free;
                  end;

                  if ContentChunk <> '' then
                    TThread.Queue(nil,
                      procedure
                      begin
                        AOnEvent(ContentChunk, False);
                      end);
                end;
              end;
            finally
              Reader.Free;
            end;
          finally
            Payload.Free;
          end;
        finally
          Body.Free;
        end;
      finally
        Http.Free;
      end;
    end);
end;

end.

Yaklaşımın amacı

Bu kod, VCL/FMX’de hızla maliyetli hale gelebilen üç tipik problem sınıfını çözer:

  • UI donmalarına yol açmayan akış: HTTP akışı arka planda okunur; UI güncellemeleri TThread.Queue üzerinden (ana iş parçacığına asenkron) gerçekleştirilir. Bu, FMX ve VCL’de sağlam ve standart yaklaşımdır.
  • Ağ hatalarına karşı yeniden deneme (Retry): ENetHTTPClientException durumunda geri çekilme (backoff) ile yeniden deneme yapılır. Bu kasıtlı olarak basittir ve daha sonra durum kodları (429/5xx) ile genişletilebilir.
  • Dayanıklı JSON ayrıştırma: Alanları körü körüne cast etmek yerine adım adım doğrulama yapılır. Bu, özel yanıtlar durumunda „Invalid type cast“ hatalarını azaltır.

Kısıtlar, tuzaklar ve varyantlar

  • SSE normal bir „JSON“ değildir: Akışta tek bir JSON yanıtı değil, bir dizi event gelir. Bu nedenle satır satır okuma ve [DONE]‚in tespit edilmesi merkezi önemdedir.
  • THTTPClient ve Proxies/SSL: Yönetici ağlarında TLS incelemesi ve proxy zorunlulukları gerçektir. THTTPClient.ProxySettings ve gerekirse sertifika konularını planlayın. Hata ayıklama: durum kodunu ve başlıkları (API-Key olmadan) her zaman kaydedin.
  • Zaman aşımı stratejisi: ResponseTimeout akışta hassastır: Çok kısa seçerseniz, istemci uzun yanıtları keser. UI araçlarında genellikle daha uzun bir zaman aşımı ve bir „İptal“ düğmesi „kısa ve sert“ bir yaklaşımdan daha uygundur.
  • İş parçacığı iptali: Örnek kod bir iptal tokeni göstermiyor. Üretim araçları için bir iptal mekanizması (ör. bayrak + Http.CancelAll yeni Delphi sürümlerinde veya kontrollü bir akış kesmesi yoluyla) faydalı olur.
  • Model ve API gelişimi: Yanıtların yapısı farklılık gösterebilir. Ayrıştırıcıyı (parser) defansif ve merkezi tutun; form koduna dağıtmayın.

Yerleşik Delphi-istemcilerinde hata ayıklama: Gerçekten neleri loglamalısınız

Entegrasyon projelerinde ilk devreye alma nadiren JSON’dan başarısız olur; genellikle ortam ayrıntıları sebebidir. Teknik bir loga (dosya, Eventlog, merkezi logger) pratikte şunlar dahil edilmelidir:

  • Request-ID (kendi atadığınız), zaman damgası, hedef URL (gizli sorgu parametreleri olmadan).
  • HTTP durum kodu, Content-Type, yanıt uzunluğu, süre.
  • Hatalarda kısaltılmış bir Response-Body (ör. maksimum 4–8 KB), kota/policy hatalarını tespit edebilmek için.
  • Yeniden deneme girişimlerinin açıkça işaretlenmesi: Attempt, Delay, Exception sınıfı.

API-Key asla log’a yazılmamalıdır. Eğer Request-Body’yi logluyorsanız, bunu sadece diagnostik build’lerde ve maskelenmiş olarak yapın; çünkü prompt’lar kişisel veya ticari içerikler içerebilir.

Sınıflandırma: Legacy durumları için VCL, FMX ve Layer-3 mimarisi

Çok sayıda Delphi uygulaması klasik bir 3-katmanlı mantıkla çalışır („Layer-3 mimarisi„: UI, iş mantığı, veri/entegrasyon). ChatGPT entegrasyonu için bu faydalıdır: gösterilen client entegrasyon katmanında yer almalıdır; iş mantığı neyin sorulacağını belirler; UI yalnızca geçmişi ve durumu gösterir. Bu şekilde ileride yapılacak bir değişiklik (başka bir provider, on-prem proxy, yeni uç noktalar) Forms’ları „paramparça etmesin“.

Ayrıca Delphi modernizasyonu için de iyi bir başlangıç noktasıdır: Önce stabil bir client, sonra UI iyileştirmeleri (akış, iptal, geçmiş), daha sonra yapılandırılmış cevaplar veya araç çağrıları gibi „daha akıllı“ özellikler.

Sonuç: Sağlam bir temel, ancak her uygulama akışa ihtiyaç duymaz

ChatGPT API’yi Delphi FMX/VCL ile güvenilir şekilde entegre etmek, özellikle kullanıcı arayüzünün tepki hızı, işletme güvenliği ve hata ayıklanabilirliğin önemli olduğu alanlarda—yönetici araçları, süreç yakın masaüstü istemcileri veya dijital kurumsal çözümlerdeki destek araçları—avantaj sağlar. Gösterilen örnek kasıtlı olarak pragmatiktir: özel kütüphaneler olmadan SSE akışı, yeniden deneme yalnızca gerçek ağ hataları için, JSON savunmacı şekilde ayrıştırıldı.

Kullanım sınırları: Sert uyumluluk gereksinimleriniz, merkezi prompt yönetişimi, çoklu kiracı desteği veya ayrıntılı denetim izleri gerekiyorsa, „masaüstünde bir istemci“ genellikle yeterli olmaz. Bu durumda bağlantı tipik olarak politikaları, kayıtlamayı ve erişim denetimini merkezi olarak uygulayan kontrollü bir sunucuya (ör. kendi REST servisi) alınmalıdır. Ancak birçok Delphi kurulumu için burada gösterilen istemci, kademeli olarak daha temiz bir genel mimariye geçirilebilecek sağlam bir başlangıç noktasıdır.

Uzmanlık alanında, entegrasyonlar, veri akışları ve devam eden geliştirme temiz şekilde etkileşime girmeli ise, Openai API’de Delphi ve Delphi için HTTP istemci zaman aşımı ve yeniden deneme mekanizmaları da önemli rol oynar.

Proje veya modernizasyon girişimini Net-Base ile görüşün.

Nächster Schritt

Wenn aus dem Thema ein reales Projekt wird, sollten Architektur, Bestand und Betrieb früh zusammen betrachtet werden.

Bireysel sorularda destek vermekle kalmıyoruz; kaynak kodu parçacıklarından, legacy konularından veya portal fikirlerinden sağlam bir kurumsal projeye dönüşene kadar da destek veriyoruz.

  • Mevcut durum, hedef durum ve teknik riskler birlikte değerlendirilir.
  • REST, Datenzugriff, Portale und Rollout werden nicht als Spätfolgen verschoben.
  • Sie sehen früh, welcher Weg wirtschaftlich und betrieblich tragfähig ist.

Gönderiyi paylaş

Bu gönderiyi doğrudan paylaş

LinkedIn, X, XING, Facebook, WhatsApp und E-Mail sind sofort verfügbar. Für Instagram bereiten wir Link und Kurztext direkt vor.

E-posta

Instagram yeni bir sekmede açılır. Bağlantı ve kısa metin önceden panoya kopyalanır.