Net-Base Magazin

15.07.2026

ChatGPT API a Delphi FMX/VCL-hez: robosztus csatlakoztatás streaminggel, újrapróbálkozási logikával és precíz JSON-parszolással

Így integrálhatja a ChatGPT API-t a Delphi FMX/VCL-be robusztusan: HTTP-kliens időkorlátokkal és újrapróbálkozási logikával, SSE-streaming UI-fagyás nélkül, valamint terhelhető JSON-parszolás eszközhívásokhoz és hibahelyzetekhez.

15.07.2026

A magazintémától a projektgyakorlatig

A bejegyzéshez tartozó szolgáltatási és technikai oldalak

Miért nem csupán egy POST a „ChatGPT API mit Delphi FMX/VCL“ csatlakoztatása a gyakorlatban

Akinek a ChatGPT API-t Delphi FMX/VCL környezetben kell integrálnia, könnyen eljut egy egyszerű HTTP-POSTig. Valódi vállalati szoftver környezetben ez azonban három ponton megbicsaklik: (1) a timeoutok és retry-k determinisztikusnak kell lenniük, különben a felhasználók „lefagyott” UI-t tapasztalnak, (2) a streaming (Server-Sent Events, röviden SSE) gyakran hasznos a jó UX-hez, de a Delphi-szálkezelésben gyorsan hibára hajlamos, és (3) a JSON nem csupán „egy objektum”: hibaüzeneteket, kvótaproblémákat, üres mezőket vagy enyhén módosult válaszformátumokat robusztusan kell kezelni.

A következő forráskódrészlet egy olyan megközelítést mutat, amely FMX és VCL alatt egyaránt működik: egy saját, tesztelhető kliens, amely tetszőlegesen nem-streaming vagy streaming módban dolgozik, a UI-frissítéseket szabályosan a főszálra marshálja (azaz a főszál szinkronizációján keresztül hajtja végre) és hibák esetén értelmezhetően naplóz. Emellett úgy van felépítve, hogy illeszthető legyen meglévő rétegstruktúrákba (pl. „API-Client” az integrációs rétegben, az UI marad vékony).

Architektúra-vázlat: UI leválasztása, kliens tesztelhetőségét megőrizni

Hosszú múlttal rendelkező Delphi-projektekben gyakran találkozunk a „HTTP a ButtonClick-ben” megoldással. Ez az első incidensig működik. Ajánlott egy kis kliens, amely a következőket tartalmazza:

  • Konfiguráció: Base-URL, API-kulcs, modell, timeoutok.
  • Transzportréteg: HTTP-kérés/válasz, retry, timeoutok, proxy/SSL-opciók (üzemeltetéstől függően).
  • Parser: JSON-dekódolás, hibaobjektumok, eredménykinyerés.
  • UI-Hooks: callback token-/szövegstreaminghez, de VCL/FMX vezérlőkhöz nem szorosan kötődve.

Így az integráció egyedi vállalati szoftverekbe tisztán megvalósítható: a kliens újrahasználható szolgáltatásokban, asztali kliensekben, admin eszközökben vagy tesztharnessekben.

Forráskódrészlet: Delphi-kliens SSE-streaminggel, timeout/újrapróbálkozással és robusztus JSON-kezeléssel

A kód a THTTPClient (System.Net.HttpClient) használatára épít és tudatosan csak minimálisan parsol a System.JSON-nal. SSE esetén soronként olvasunk és a „data: …” sorokra reagálunk. Ez nem „WebSocket”, hanem egy HTTP-válaszfolyam, amely folyamatosan szövegsorokat szolgáltat. Fontos: egy munkaszálon olvasunk, és a UI-frissítéseket a főszálra marsháljuk.

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: itt az API-kulcsot „Authorization: Bearer …“ formában adjuk.
// Vállalati környezetben külön ügyelni kell arra, hogy a kulcsok ne kerüljenek a naplókba.
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ális messages-felépíté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
// Gyakori forma: { „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(‚Váratlan JSON-válasz (nem objektum).‘, 0, AJsonText);

Root := J as TJSONObject;
Choices := Root.GetValue<TJSONArray>(‚choices‘);
if (Choices = nil) or (Choices.Count = 0) then
raise EChatApiError.Create(‚Váratlan JSON-válasz: choices hiányzik/üres.‘, 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(‚Váratlan JSON-válasz: message hiányzik.‘, 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
// Hálózat-/TLS-/timeout-hiba: egyszerű újrapróbálkozás növekvő késleltetéssel.
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-hiba ‚ + 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
// A streamet szándékosan aszinkron indítjuk, hogy az FMX/VCL ne blokkolódjon.
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-hibák esetén a Content gyakran mégis JSON.
TThread.Queue(nil,
procedure
begin
AOnEvent(‚Streaming indítása sikertelen (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-formátum: sorok, pl. „data: {…}“ vagy „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-Chunk auswerten: 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.

Mire jó ez a megközelítés

A kód három tipikus problémakört old meg, amelyek VCL/FMX alatt gyorsan költségessé válhatnak:

  • Streaming UI-fagyások nélkül: A HTTP-stream háttérben olvasódik; a felhasználói felület frissítései a TThread.Queue-n keresztül történnek (aszinkron a fő szálon). Ez FMX és VCL esetén a megbízható, szabványos megközelítés.
  • Újrapróbálkozás hálózati hibák esetén: ENetHTTPClientException esetén backoff mechanizmussal próbálkozik újra. Ez szándékosan egyszerű, később viszont bővíthető státuszkódok (429/5xx) figyelembevételével.
  • Robusztus JSON-elemzés: Ahelyett, hogy vakon mezőket kasztolnánk, lépésenként ellenőrizzük azokat. Ez csökkenti az „Invalid type cast” hibákat különleges válaszok esetén.

Korlátozások, buktatók és változatok

  • SSE nem „normális JSON”: Streaming során események sorozata érkezik, nem egyetlen JSON-válasz. Ezért a soronkénti olvasás és a [DONE] felismerése kulcsfontosságú.
  • THTTPClient és proxy-k/SSL: Admin hálózatokban TLS-inspection és proxy-követelmények valóságosak. Tervezze be a THTTPClient.ProxySettings-et és szükség esetén a tanúsítványkezelést. Hibakeresésnél mindig naplózza a státuszkódot/fejlécet (API-Key nélkül).
  • Timeout-stratégia: ResponseTimeout streaming esetén érzékeny: ha túl rövidet választ, a kliens levágja a hosszú válaszokat. UI-eszközökben gyakran érdemes hosszabb timeout és egy „Mégse” gomb a „rövid és kemény” megközelítés helyett.
  • Thread-megszakítás: A példakód nem mutat Cancel-Token-t. Termelési eszközök esetén érdemes beépíteni egy megszakítási mechanizmust (pl. flag + Http.CancelAll in neueren Delphi-Versionen vagy kontrollált stream-megszakítással).
  • Modell- és API-fejlesztés: A válaszok szerkezete eltérő lehet. Tartsa a parsert defenzívan és központilag, ne az űrlapkódba ágyazva.

Debugging in gewachsenen Delphi-Clients: Was Sie wirklich loggen sollten

Integrációs projektekben a kezdeti élesítés ritkán a JSON miatt bukik el; általában környezeti részletek okozzák a problémát. Egy műszaki naplóba (fájl, Eventlog, központi logger) a gyakorlatban a következők tartoznak:

  • Request-ID (saját generálás), Timestamp, cél-URL (titkos query stringek nélkül).
  • HTTP-státuszkód, Content-Type, válasz hossza, futási idő.
  • Hibák esetén rövidített Response-Body (pl. max. 4–8 KB), hogy kvóta-/policy-hibákat felismerhessenek.
  • Újrapróbálkozások egyértelmű megjelölése: Attempt, Delay, Exception-Klasse.

Az API-Key soha nem kerülhet a naplóba. Ha a Request-Body-t naplózza, csak diagnosztikai buildben és maszkolva tegye, mert a promptok személyes vagy üzleti adatokat is tartalmazhatnak.

Besorolás örökölt helyzetekhez: VCL, FMX und Layer-3 Architektur

Sok Delphi-alkalmazás klasszikus 3-rétegű logikában fut („Layer-3 Architektur“: UI, üzleti logika, adatok/integráció). A ChatGPT-csatlakozásnál ez hasznos: a bemutatott kliens az integrációs rétegbe tartozik; az üzleti logika dönt arról, mit kérdezzen; a UI csak a történetet és az állapotot jeleníti meg. Így elkerülhető, hogy egy későbbi váltás (más provider, On-Prem-Proxy, új végpontok) az űrlapokat szétszaggassa.

Az Delphi-modernizációhoz is jó kiindulópont: előbb egy stabil kliens, aztán UI-verbesserungen (Streaming, Abbrechen, Verlauf), majd később „intelligensebb” funkciók, például strukturált válaszok vagy eszközhívások.

Fazit: Szilárd alap, de nem minden alkalmazásnak kell Streaming

A ChatGPT API tiszta integrálása Delphi FMX/VCL alatt különösen ott éri meg, ahol a felhasználói felület reagálóképessége, az üzemeltetési megbízhatóság és a hibakereshetőség fontos: admin eszközök, folyamatközeli asztali kliensek vagy support‑eszközök digitális vállalati megoldásokban. A bemutatott kódrészlet szándékosan pragmatikus: SSE‑streaming speciális könyvtárak nélkül, retry csak valódi hálózati hibákra, JSON óvatos, defenzív feldolgozása.

Alkalmazási korlátok: ha szigorú megfelelőségi előírásokra, központi prompt‑governance‑re, többbérlős működésre vagy részletes audit‑nyomvonalakra van szükség, egyetlen „asztali kliens” általában nem elegendő. Ilyenkor az integrációnak tipikusan egy kontrollált szerveren a helye (pl. saját REST‑szolgáltatás), amely központilag valósítja meg a policy‑kat, a naplózást és a hozzáférés‑vezérlést. Sok Delphi telepítésnél azonban az itt bemutatott kliens megbízható kiindulópont, amely fokozatosan átvezethető egy tisztább, átfogó architektúrába.

Az üzemi‑technikai környezetben fontos szerepet játszik az Openai API használata Delphi esetén, valamint az Delphi Http Client Timeout Retry kezelése, amikor az integrációknak, az adatfolyamoknak és a további fejlesztésnek összehangoltan kell működnie.

Projekt vagy modernizációs feladat megbeszélése Net-Base részvételével.

Nächster Schritt

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

Nemcsak egyedi kérdésekben támogatunk, hanem akkor is, amikor forráskódrészletekből, örökölt rendszerekkel kapcsolatos témákból vagy portálötletekből robusztus vállalati projektet kell kialakítani.

  • A jelenlegi állapotot, a célállapotot és a műszaki kockázatokat együttesen értékeljük.
  • REST, Datenzugriff, Portale und Rollout werden nicht als Spätfolgen verschoben.
  • Sie sehen früh, welcher Weg wirtschaftlich und betrieblich tragfähig ist.

Bejegyzés megosztása

Ezt a bejegyzést közvetlenül megosztani

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

E-mail

Az Instagram egy új lapon nyílik meg. A link és a rövid szöveg előzetesen a vágólapra másolódik.