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.
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):
ENetHTTPClientExceptiondurumunda 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.ProxySettingsve 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:
ResponseTimeoutakış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.CancelAllyeni 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.
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.