Od témy magazínu k projektovej praxi
Súvisiace stránky služieb a technológií k príspevku
Prečo „ChatGPT API mit Delphi FMX/VCL“ v praxi nie je iba POST
Kto chce pripojiť ChatGPT API mit Delphi FMX/VCL, často skončí pri jednoduchom HTTP-POST. V reálnych business softvérových prostrediach sa to však láme na troch miestach: (1) Timeouts a Retries musia byť deterministické, inak používatelia zažijú „zavesené“ UI, (2) Streaming (Server-Sent Events, skrátene SSE) je pre dobrú UX často rozumný, ale v Delphi-Threadingu rýchlo náchylný na chyby, a (3) JSON nie je len „jeden objekt“: chybové hlásenia, problémy s kvótami, prázdne polia alebo mierne zmenené tvary odpovedí musia byť robustne spracované.
Nasledujúci Source-Schnipsel ukazuje prístup, ktorý funguje rovnako vo FMX aj VCL: vlastný, testovateľný klient, ktorý môže pracovať voliteľne v režime not-streaming alebo streaming, správne marshaluje UI-aktualizácie (t. j. vykonáva ich cez synchronizáciu hlavného vlákna) a pri chybách dáva výpovednú stopu v logu. Zároveň je navrhnutý tak, aby sa dal vložiť do existujúcich vrstvových štruktúr (napr. „API-Client“ v integračnej vrstve, UI zostáva tenké).
Architektur-Skizze: UI entkoppeln, Client testbar halten
V Delphi-projektoch s dlhou históriou sa často nájde „HTTP im ButtonClick“. To funguje až do prvého incidentu. Odporúčateľné je malý klient s:
- Konfiguration: Base-URL, API-Key, Modell, Timeouts.
- Transportschicht: HTTP-Request/Response, Retry, Timeout, Proxy/SSL-Optionen (je nach Betrieb).
- Parser: JSON-Decoding, Fehlerobjekte, Ergebnisextraktion.
- UI-Hooks: Callback für Token-/Text-Streaming, aber ohne harte Abhängigkeit auf VCL/FMX Controls.
Týmto spôsobom je integrácia do individuálneho podnikového softvéru čistá: klient je možné znovu použiť v servisoch, desktop-klientoch, administračných nástrojoch alebo test-harnesses.
Source-Schnipsel: Delphi-Client mit SSE-Streaming, Timeout/Retry und robustem JSON
Der Code nutzt THTTPClient (System.Net.HttpClient) und pars(t) bewusst nur minimalistisch mit System.JSON. Für SSE wird zeilenweise gelesen und auf „data: …“ reagiert. Das ist kein „WebSocket“, sondern ein HTTP-Response-Stream, der kontinuierlich Textzeilen liefert. Wichtig: Wir lesen in einem Worker-Thread und marshalen UI-Updates in den Main Thread.
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 token: tu API-kľúč ako „Authorization: Bearer …“.
// V podnikových prostrediach dbajte, aby sa kľúče nedostali do logov.
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));
// Minimálna štruktúra messages (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
// Bežný tvar: { „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(‚Neočakávaná JSON-odpoveď (nie je objekt).‘, 0, AJsonText);
Root := J as TJSONObject;
Choices := Root.GetValue<TJSONArray>(‚choices‘);
if (Choices = nil) or (Choices.Count = 0) then
raise EChatApiError.Create(‚Neočakávaná JSON-odpoveď: choices chýba alebo je prázdne.‘, 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(‚Neočakávaná JSON-odpoveď: message chýba.‘, 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
// Chyby siete/TLS/timeout: jednoduché opakovanie s backoffom.
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 chyba ‚ + 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
// Streamovanie zámerne spúšťame asynchrónne, aby FMX/VCL nebolo blokované.
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
// Pri chybách pri streamovaní je obsah často stále v JSON formáte.
TThread.Queue(nil,
procedure
begin
AOnEvent(‚Spustenie streamovania zlyhalo (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;
// Formát SSE: riadky ako „data: {…}“ alebo „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;
// Vyhodnotenie JSON-fragmentu: 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.
Na čo je tento prístup dobrý
Kód rieši tri typické triedy problémov, ktoré sa vo VCL/FMX rýchlo môžu stať nákladnými:
- Streaming bez zamŕzania UI: HTTP-stream sa číta na pozadí; aktualizácie UI prebiehajú cez
TThread.Queue(asynchrónne do hlavného vlákna). To je vo FMX a VCL robustný štandardný postup. - Retry pri chybách siete: Pri
ENetHTTPClientExceptionsa opakuje s backoff mechanizmom. Je to zámerne jednoduché a neskôr sa dá rozšíriť o kontrolu stavových kódov (429/5xx). - Robustné parsovanie JSON: Namiesto slepého pretypovávania polí sa postupne kontroluje. To znižuje chyby „Invalid type cast“ pri špeciálnych odpovediach.
Podmienky, úskalia a varianty
- SSE nie je „bežné JSON“: Pri streamovaní prichádza sled udalostí, nie jedna jediná JSON-odpoveď. Preto je čítanie po riadkoch a rozpoznanie
[DONE]kľúčové. - THTTPClient a Proxies/SSL: V administrátorských sieťach sú reálne TLS-inspekcia a povinné proxy. Naplánujte
THTTPClient.ProxySettingsa prípadne otázky certifikátov. Ladenie: Stavový kód/hlavičky vždy logujte (bez API-Key). - Strategia timeoutu:
ResponseTimeoutje pri streamovaní citlivý: Ak zvolíte príliš krátky, klient preruší dlhé odpovede. V UI-nástrojoch je často rozumnejší dlhší timeout a tlačidlo „Abbrechen“ než krátky a tvrdý časový limit. - Prerušenie vlákna: Úryvok neukazuje Cancel-Token. Pre produkčné nástroje sa oplatí implementovať mechanizmus prerušenia (napr. flag +
Http.CancelAllv novších Delphi-verziách alebo cez kontrolované prerušenie streamu). - Vývoj modelu a API: Štruktúra odpovedí sa môže meniť. Držte parser defensívne a centrálne, nie rozptýlený v kóde formulárov.
Ladenie v existujúcich Delphi-klientoch: čo by ste skutočne mali logovať
V integračných projektoch prvé nasadenie zriedka zlyhá kvôli JSON, skôr kvôli detailom prostredia. Do technického logu (súbor, Eventlog, centrálny logger) patria v praxi:
- Request-ID (vlastne pridelená), časová pečiatka, cieľová URL (bez Secret-Querystrings).
- HTTP-stavový kód, Content-Type, dĺžka odpovede, doba trvania.
- Skrátené telo odpovede pri chybách (napr. max. 4–8 KB), aby bolo možné rozpoznať Quota-/Policy-chyby.
- Explicitné označenie pokusov o retry: Attempt, Delay, Exception-Klasse.
API-Key nikdy nepatrí do logu. Ak logujete Request-Body, robte to len v diagnostických buildoch a s maskovaním, pretože prompty môžu obsahovať osobné alebo obchodné údaje.
Zaradenie pre Legacy-situácie: VCL, FMX a Layer-3 architektúra
Mnoho Delphi-aplikácií beží v klasickej 3-vrstvovej logike („Layer-3 Architektur“: UI, obchodná logika, dáta/integrácia). Pre napojenie ChatGPT je to užitočné: Ukázaný klient patrí do integračnej vrstvy; obchodná logika rozhoduje, čo sa má pýtať; UI zobrazuje iba priebeh a stav. Tým zabránite, aby neskoršia zmena (iný provider, On-Prem-Proxy, nové endpointy) formuláre „roztrhala“.
Aj pre modernizáciu Delphi je to dobrý východiskový bod: Najprv stabilný klient, potom vylepšenia UI (Streaming, Abbrechen, Verlauf), a až potom „inteligentnejšie“ funkcie ako štruktúrované odpovede alebo volania nástrojov.
Záver: Pevný základ, ale nie každá aplikácia potrebuje Streaming
Integrácia ChatGPT API s Delphi do FMX/VCL sa obzvlášť oplatí tam, kde sú dôležité rýchlosť reakcie UI, prevádzková spoľahlivosť a možnosť ladenia: administračné nástroje, procesne blízke desktopové klienty alebo podporné nástroje v digitálnych podnikových riešeniach. Ukážkový útržok je zámerne pragmatický: SSE-Streaming bez špeciálnych knižníc, Retry iba pri skutočných sieťových chybách, JSON parsovaný defenzívne.
Obmedzenia nasadenia: Ak potrebujete prísne požiadavky na compliance, centrálnu správu promptov, podporu viacerých nájomníkov alebo detailné auditné stopy, zvyčajne nestačí „klient na desktope“. V takom prípade patrí integrácia typicky do kontrolovaného servera (napr. vlastný REST-Service), ktorý centrálne implementuje politiky, logging a riadenie prístupu. Pre mnohé inštalácie Delphi je však tu ukázaný klient spoľahlivým východiskovým bodom, ktorý sa postupne dá previesť do čistejšej celkovej architektúry.
V odbornom kontexte zohrávajú dôležitú úlohu aj Openai API v Delphi a Delphi Http Client Timeout Retry, keď musia integrácie, dátové toky a ďalší vývoj hladko spolupracovať.
Nächster Schritt
Wenn aus dem Thema ein reales Projekt wird, sollten Architektur, Bestand und Betrieb früh zusammen betrachtet werden.
Podporujeme nielen pri jednotlivých otázkach, ale aj vtedy, keď sa z fragmentov zdrojového kódu, tém súvisiacich s legacy systémami alebo nápadov na portál má stať robustný podnikový projekt.
- Stav, cieľový obraz a technické riziká sa hodnotia spoločne.
- REST, Datenzugriff, Portale und Rollout werden nicht als Spätfolgen verschoben.
- Sie sehen früh, welcher Weg wirtschaftlich und betrieblich tragfähig ist.