Net-Base Tímarit

15.07.2026

ChatGPT API með Delphi FMX/VCL: Áreiðanleg tenging með streymi, endurtilraunum og hreinu JSON-parsing

Svo tengir þú ChatGPT API við Delphi FMX/VCL á traustan hátt: HTTP-viðskiptavinur með tímalokum og endurtilraunum, SSE-streymi án UI-frjósa, og áreiðanleg JSON-greining fyrir verkfæraköll og villumeðhöndlun.

15.07.2026

Frá tímaritsþema til verkefnaframkvæmdar

Viðeigandi þjónustu- og tæknisíður fyrir greinina

Af hverju „ChatGPT API mit Delphi FMX/VCL“ in der Praxis nicht nur ein POST ist

Wer die ChatGPT API mit Delphi FMX/VCL anbinden will, landet schnell bei einem simplen HTTP-POST. In echten Business-Software-Umgebungen kippt das aber an drei Stellen: (1) Timeouts und Retries müssen deterministisch sein, weil Anwender sonst „hängende“ UI erleben, (2) Streaming (Server-Sent Events, kurz SSE) ist für gute UX oft sinnvoll, aber im Delphi-Threading schnell fehleranfällig, und (3) JSON ist nicht nur „ein Objekt“: Fehlermeldungen, Quotenprobleme, leere Felder oder leicht veränderte Antwortformen müssen robust gehandhabt werden.

Der folgende Source-Schnipsel zeigt einen Ansatz, der in FMX und VCL gleichermaßen funktioniert: Ein eigener, testbarer Client, der wahlweise nicht-streaming oder streaming arbeitet, UI-Updates sauber marshalt (also über die Main-Thread-Synchronisation ausführt) und bei Fehlern aussagekräftig loggt. Nebenbei ist er so gebaut, dass er sich in gewachsene Layer-Strukturen einfügt (z. B. „API-Client“ in der Integrationsschicht, UI bleibt dünn).

Architektur-Skizze: UI entkoppeln, Client testbar halten

In Delphi-Projekten mit langer Historie findet man häufig „HTTP im ButtonClick“. Das funktioniert bis zum ersten Incident. Empfehlenswert ist ein kleiner Client mit:

  • 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.

So kann die Integration in individuelle Unternehmenssoftware sauber betrieben werden: Der Client lässt sich in Services, Desktop-Clients, Admin-Tools oder Test-Harnesses wiederverwenden.

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-táknið: hér er API-lykillinn sem „Authorization: Bearer …“.
// Í fyrirtækjaumhverfum skal einnig gæta þess að lyklar komist ekki í logg.
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));

// Lágmarksuppbygging ‚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
// Algeng uppbygging: { „error“: { „message“: „…“, „type“: „…“ } }
EObj := (J as TJSONObject).GetValue(‚error‘);
if Assigned(EObj) then
begin
AMessage := EObj.GetValue(‚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ænt JSON-svar (ekki JSON-objekt).‘, 0, AJsonText);

Root := J as TJSONObject;
Choices := Root.GetValue(‚choices‘);
if (Choices = nil) or (Choices.Count = 0) then
raise EChatApiError.Create(‚Óvænt JSON-svar: „choices“ vantar/er tómt.‘, 0, AJsonText);

Choice0 := Choices.Items[0] as TJSONObject;
// Chat Completions: choices[0].message.content
Msg := Choice0.GetValue(‚message‘);
if Msg = nil then
raise EChatApiError.Create(‚Óvænt JSON-svar: „message“ vantar.‘, 0, AJsonText);

Result := Msg.GetValue(‚content‘, “);
finally
J.Free;
end;
end;

function TOpenAIChatClient.ExecuteWithRetry(const ADoRequest: TFunc): 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
// Net-/TLS-/tímaúrtaksvillur: einföld endurtilraun með backoff.
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-villa ‚ + 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
// Streymi er meðvitað ræst sem ósamstillt til að FMX/VCL blokkist ekki.
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
// Við villur í streymi er innihald (content) oft samt JSON.
TThread.Queue(nil,
procedure
begin
AOnEvent(‚Upphaf streymis misheppnaðist (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-snið: línur eins og „data: {…}“ eða „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;

// Unnið úr JSON-bút: 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(‚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.

Hvers vegna þessi nálgun nýtist

Kóðinn leysir þrjár dæmigerðar vandamálaflokkar sem geta fljótt orðið kostnaðarmiklir í VCL/FMX:

  • Streaming án þess að UI frjósi: HTTP-streymið er lesið í bakgrunni; uppfærslur í UI fara í gegnum TThread.Queue (ósamstilltur inn í aðalþráð). Þetta er í FMX og VCL traustur staðalháttur.
  • Endurtilraun við netvillur: Við ENetHTTPClientException er reynt aftur með stigvaxandi bið (backoff). Þetta er vísvitandi einfalt og hægt að bæta við síðar með tilliti til stöðukóða (429/5xx).
  • Traust JSON-skilun: Í stað þess að gera blindar tegundabreytingar á reitum er farið skref fyrir skref og athugað. Þetta dregur úr „Invalid type cast“-villum við sérsvör.

Skilyrði, gildrur og afbrigði

  • SSE er ekki „venjulegt JSON“: Við streymi kemur röð atburða, ekki ein eining af JSON-svörun. Þess vegna er línuskipt lesið og að þekkja [DONE] grundvallaratriði.
  • THTTPClient og Proxies/SSL: Í stjórnandanetum eru TLS-Inspection og kröfur um proxy raunverulegar. Skipuleggið THTTPClient.ProxySettings og ef þörf er mál er að skoða vottorðsmál. Við villuleit: skráið alltaf statuskóða/headers (án API-Key).
  • Tímaútstefna: ResponseTimeout er viðkvæmt við streymi: Ef valið er of stutt klippir klientinn langar svör. Í UI-tólum er oft skynsamlegra að hafa lengri timeout og „Hætta“-hnapp frekar en „stutt og hart“.
  • Þráðsrof: Sýnisbúturinn sýnir ekki Cancel-Token. Fyrir framleiðslugögn er skynsamlegt að hafa afbrotskerfi (t.d. flagg + Http.CancelAll í nýrri Delphi-útgáfum eða með stjórnuðum streymisbrotum).
  • Líkön og API-þróun: Uppbygging svara getur breyst. Haltu parsaranum varnarvænum og miðlægum, ekki dreift inn í formakóða.

Villuleit í eldri Delphi-viðskiptavinum: Hvað ber raunverulega að skrá?

Í samþættingarverkefnum mistakast fyrstu gangsetningar sjaldnast vegna JSON, heldur vegna umhverfismála. Í tæknilegt logg (skrá, Eventlog, miðlægt skráningarkerfi) ætti í framkvæmd að koma:

  • Request-ID (kennd af ykkur sjálfum), tímamerki, áfangastaður-URL (án leynilegra fyrirspurnarstrengja).
  • HTTP-stöðukóði, Content-Type, svariðlengd, svartími.
  • Styttur Response-Body við villur (t.d. hámark 4–8 KB) til að greina quota-/policy-villum.
  • Skýr merking endurtilrauna: Attempt, Delay, Exception-Klasse.

API-Key á aldrei að fara í loggið. Ef þið skráið Request-Body, þá aðeins í greiningarbyggingum og með maskun, því Prompts geta innihaldið persónu- eða viðskiptaupplýsingar.

Staðsetning fyrir eldri aðstæður: VCL, FMX og Layer-3 arkitektúr

Margar Delphi-umsóknir keyra í klassísku 3‑lagakerfi („Layer-3 Architektur“: UI, viðskipta‑rökhús, gögn/samþætting). Fyrir ChatGPT‑tengingu er þetta gagnlegt: Sýndu klientinn á samþættingarlagi; viðskiptarökhúsið ákveður hvað er spurt; UI birtir aðeins sögu og stöðu. Þannig forðast þú að seinni breytingar (annar veitandi, On‑Prem‑Proxy, nýir endapunktar) rjúfi formin.

Einnig fyrir Delphi-nýtingu er þetta góður inngangur: Upphaflega byggja stöðugan klient, svo bæta UI (Streaming, Hætta, Saga), og síðan innleiða „greindari“ eiginleika eins og uppbyggð svör eða tóla‑köll.

Niðurstaða: Traust undirstaða, en ekki öll forrit þurfa streymi

ChatGPT-API með Delphi FMX/VCL er sérstaklega gagnleg þar sem svörun notendaviðmóts, rekstraröryggi og auðveld bilanagreining skiptir máli: stjórnunarverkfæri, ferlinærar skrifborðsviðskiptavinar eða stuðningsverkfæri í stafrænum fyrirtækjalausnum. Sýndur bútur er meðvitað hagnýtur: SSE-streymi án sértækra bókasafna, endurtilraun aðeins fyrir raunveruleg netbilun, JSON varlega greint.

Takmarkanir notkunar: Ef þið þurfið strangar kröfur um samræmi, miðlæga prompt-stjórnun, stuðning við fjölleigun eða ítarlega endurskoðunarspor dugar „ein viðskiptavinur á skrifborði“ yfirleitt ekki. Þá á tengingin yfirleitt heima á stjórnuðum þjón (t.d. eigin REST-service), sem útfærir stefnu, skráningu og aðgangsstýringu miðlægt. Fyrir margar Delphi-uppsetningar er sýndur viðskiptavinur hins vegar traustur upphafspunktur sem hægt er smám saman að færa yfir í hreinni heildararkitektúr.

Í faglegu samhengi spila einnig Openai API í Delphi og Delphi HTTP-viðskiptavinar biðtími og endurtilraun mikilvægt hlutverk, þegar samþættingar, gagnastreymar og áframhaldandi þróun þurfa að vinna hreint saman.

Ræddu verkefni eða endurnýjunarverkefni með Net-Base.

Nächster Schritt

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

Við styðjum ekki aðeins við einstakar spurningar, heldur einnig þegar úr kóðabútum, eldri kerfum eða gáttahugmyndum þarf að verða traust fyrirtækjaverkefni.

  • Núverandi staða, markmynd og tæknileg áhætta eru metin saman.
  • REST, Datenzugriff, Portale und Rollout werden nicht als Spätfolgen verschoben.
  • Sie sehen früh, welcher Weg wirtschaftlich und betrieblich tragfähig ist.

Deila færslu

Deila þessari færslu beint

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

Tölvupóstur

Instagram opnast í nýjum flipa. Tengill og stuttur texti eru afritaðir í klippiborðið á undan.