Net-Base Žurnalas

15.07.2026

ChatGPT API su Delphi FMX/VCL: patikima integracija su srautinio perdavimo palaikymu, automatiniais pakartotiniais bandymais ir tvarkingu JSON analizavimu

Kaip patikimai integruoti ChatGPT API su Delphi FMX/VCL: HTTP klientas su laiko limitais ir pakartotiniais bandymais, SSE srautai be naudotojo sąsajos užšalimų ir patikimas JSON apdorojimas įrankių kvietimams bei klaidų atvejams.

15.07.2026

Nuo žurnalo temos iki projekto įgyvendinimo

Tinkami puslapiai apie paslaugas ir techninę informaciją šiam įrašui

Kodėl „ChatGPT API mit Delphi FMX/VCL“ praktiškai nėra vien tik POST

Kas nori prijungti ChatGPT API mit Delphi FMX/VCL, dažnai greitai apsiriboja paprastu HTTP-POST. Tačiau realiose verslo programinės įrangos aplinkose tai sukelia problemų trijose vietose: (1) Timeouts und Retries turi būti deterministiniai, nes kitaip naudotojai patirs „užstrigusią“ UI, (2) Streaming (Server-Sent Events, trumpai SSE) dažnai reikalingas gerai UX, bet Delphi-threadinge greitai tampa klaidų šaltiniu, ir (3) JSON nėra vien „objektas“: klaidų pranešimai, kvotų problemos, tušti laukai ar šiek tiek pakeistos atsakymų formos turi būti tvarkomi patikimai.

Žemiau esantis kodo fragmentas rodo požiūrį, kuris veikia tiek FMX, tiek VCL: savarankiškas, testuojamas klientas, kuris pagal pasirinkimą dirba ne-streaming arba streaming režimu, tvarkingai maršalina UI atnaujinimus (t. y. vykdo per pagrindinės gijos sinchronizaciją) ir gedimų atvejais įrašo aiškius žurnalo įrašus. Be to, jis suprojektuotas taip, kad įsilietų į esamas sluoksnių struktūras (pvz., „API-Client“ integracijos sluoksnyje, UI lieka plonas).

Architektūros eskizas: atskirti UI, išlaikyti klientą testuojamą

Ilgą istoriją turinčiuose Delphi projektuose dažnai randamas „HTTP im ButtonClick“. Tai veikia iki pirmojo incidento. Rekomenduojama turėti nedidelį klientą, turintį šiuos elementus:

  • Konfigūracija: Base-URL, API-Key, Modell, Timeouts.
  • Transporto sluoksnis: HTTP-Request/Response, Retry, Timeout, Proxy/SSL parinktys (priklausomai nuo aplinkos).
  • Parseris: JSON dekodavimas, klaidų objektai, rezultatų išgavimas.
  • UI-Hooks: Callback tokenų/teksto srautui, bet be griežtos priklausomybės nuo VCL/FMX valdiklių.

Taip integracija į individualią įmonės programinę įrangą gali būti vykdoma tvarkingai: klientą galima pakartotinai naudoti servisuose, darbalaukio klientuose, administravimo įrankiuose ar testavimo įrankiuose.

Kodo fragmentas: Delphi-klientas su SSE srautu, Timeout/Retry ir atspariu JSON

Kodas naudoja THTTPClient (System.Net.HttpClient) ir sąmoningai analizuoja tik minimaliai su System.JSON. SSE atveju skaitoma eilutė po eilutės ir reaguojama į „data: …“. Tai nėra „WebSocket“, o HTTP atsakymo srautas, kuris nuolatos tiekia teksto eilutes. Svarbu: skaitome darbinėje gijoje ir maršalinuojame UI atnaujinimus į pagrindinę giją (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 žetonas: čia API raktas kaip „Authorization: Bearer …“.
// Verslo aplinkoje papildomai pasirūpinti, kad raktai nepatektų į žurnalus.
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));

// Minimalus messages sudarymas (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
// Dažna 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(‚Nenumatytas JSON-atsakymas (ne objektas).‘, 0, AJsonText);

Root := J as TJSONObject;
Choices := Root.GetValue<TJSONArray>(‚choices‘);
if (Choices = nil) or (Choices.Count = 0) then
raise EChatApiError.Create(‚Nenumatytas JSON-atsakymas: choices trūksta/tuščias.‘, 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(‚Nenumatytas JSON-atsakymas: message trūksta.‘, 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
// Tinklo/TLS/timeout klaidos: paprastas pakartotinimas su palaipsniui didėjančia delsos trukme.
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 klaida ‚ + 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
// Srautinį perdavimą sąmoningai paleisti asinchroniškai, kad FMX/VCL nebūtų blokuojami.
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
// Srautinio klaidų atveju turinys dažnai vis tiek yra JSON.
TThread.Queue(nil,
procedure
begin
AOnEvent(‚Srautinio pradžios nepavyko (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 formatas: eilutės tokios kaip „data: {…}“ arba „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 fragmento analizė: 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.

Kam šis požiūris naudingas

Kodas sprendžia tris tipines problemų klases, kurios VCL/FMX kontekste greitai tampa brangios:

  • Streaming ohne UI-Freezes: HTTP srautas skaitomas fone; UI atnaujinimai vykdomi per TThread.Queue (asinchroniškai pagrindiniame gijoje). Tai FMX ir VCL patikimas standarto būdas.
  • Retry für Netzfehler: Prie ENetHTTPClientException atliekamas pakartotinis bandymas su backoff mechanizmu. Tai sąmoningai paprasta ir vėliau gali būti išplėsta apie būsenos kodus (429/5xx).
  • Robustes JSON-Parsing: Užuot aklai konvertavus laukus, jie tikrinami žingsnis po žingsnio. Tai sumažina „Invalid type cast“ klaidų skaičių specialių atsakymų atvejais.

Ribinės sąlygos, spąstai ir variantai

  • SSE ist kein „normales JSON“: Srautiniu būdu gaunama įvykių seka, o ne vienas JSON atsakymas. Todėl svarbus eiliškas skaitymas (po eilute) ir [DONE] atpažinimas.
  • THTTPClient und Proxies/SSL: Administraciniuose tinkluose dažnai pasitaiko TLS inspeksija ir privalomi proxy. Numatyti THTTPClient.ProxySettings ir, jei reikia, sertifikatų klausimus. Derinimui: visada žurnalizuokite statuso kodą ir antraštes (be API‑raktų).
  • Timeout-Strategie: ResponseTimeout srautiniam režimui yra jautrus: pasirinkus per trumpą laiką, klientas nutrauks ilgus atsakymus. UI įrankiuose dažnai prasmingesnis ilgesnis timeout ir mygtukas „Atšaukti“, nei „trumpai ir griežtai“.
  • Thread-Abbruch: Pavyzdinis kodas nerodo Cancel‑Token. Produkciniams įrankiams verta įdiegti nutraukimo mechanizmą (pvz. žymą + Http.CancelAll naujesnėse Delphi versijose arba per kontroliuojamą srauto nutraukimą).
  • Modell- und API-Weiterentwicklung: Atsakymų struktūra gali skirtis. Laikykite parsinimo logiką defensyvią ir centralizuotą, ne paskirstytą formų kode.

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

Integracijos projektuose pirmasis paleidimas retai žlunga dėl JSON – dažniau dėl aplinkos detalių. Į techninį žurnalą (failą, Eventlog, centralizuotą loggerį) praktikoje reikėtų įrašyti:

  • Request‑ID (savo priskirta), laiko žymą, tikslinį URL (be slaptų užklausos parametrų).
  • HTTP statuso kodą, Content‑Type, atsakymo ilgį, vykdymo trukmę.
  • Trumpą Response‑Body klaidų atveju (pvz. maks. 4–8 KB), kad būtų galima atpažinti kvotos / politikos klaidas.
  • Aiškus retry bandymų žymėjimas: bandymo numeris, uždelsimo trukmė, išimties klasė.

API‑raktas niekada neturi patekti į žurnalus. Jei žurnalinsite Request‑Body, darykite tai tik diagnostiniuose build’uose ir su maskavimu, nes prompt’ai gali turėti asmeninius arba verslo duomenis.

Paaiškinimas paveldėtoms (Legacy) situacijoms: VCL, FMX und Layer-3 Architektur

Daugelis Delphi programų veikia klasikine 3 sluoksnių logika („Layer-3 Architektur“: UI, verslo logika, duomenys/integracija). ChatGPT prijungimui tai yra naudinga: parodytas klientas priklauso integracijos sluoksniui; verslo logika nusprendžia, kas bus užduota; UI tik pateikia pokalbio eigą ir būseną. Taip išvengsite, kad vėlesnis keitimas (kitas tiekėjas, On‑Prem‑Proxy, nauji galutiniai taškai) „sudaužytų“ formas.

Taip pat Delphi modernizacijai tai geras pradžios taškas: pirmiausia stabilus klientas, vėliau UI patobulinimai (Streaming, Atšaukti, pokalbio istorija), ir tik po to „išmanesnės“ funkcijos, pvz. struktūruoti atsakymai ar įrankių kvietimai.

Išvada: tvirta bazė, bet ne kiekviena programa reikalauja Streaming

ChatGPT API su Delphi FMX/VCL tvarkingai integruoti ypač verta ten, kur svarbūs sąsajos reaguojamumas, veikimo patikimumas ir derinimo galimybės: administravimo įrankiai, procesams artimi darbalaukio klientai arba palaikymo priemonės skaitmeniniuose įmonių sprendimuose. Čia parodytas kodo fragmentas sąmoningai pragmatiškas: SSE srautinimas be specialių bibliotekų, pakartojimai tik tikroms tinklo klaidoms, JSON parsinamas defensyviai.

Taikymo ribos: jei jums reikia griežtų atitikties reikalavimų, centrinės Prompt-Governance, daugiaklientystės arba detalių audito įrašų, vienas „klientas darbalaukyje“ dažniausiai nepakanka. Tokiu atveju integracija paprastai turi būti perkelta į kontroliuojamą serverį (pvz., atskirą REST-servisą), kuris centralizuotai įgyvendina politiką, žurnalavimą ir prieigos kontrolę. Tačiau daugeliui Delphi diegimų čia parodytas klientas yra patikima pradinė stotelė, kurią galima žingsnis po žingsnio perkelti į tvarkingesnę bendrą architektūrą.

Profesiniame kontekste taip pat svarbų vaidmenį atlieka Openai API naudojimas su Delphi ir Delphi Http Client Timeout Retry, kai integracijos, duomenų srautai ir tolesnė plėtra turi veikti sklandžiai.

Aptarkite projektą arba modernizavimo užduotį su Net-Base.

Nächster Schritt

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

Mes padedame ne tik pavienėse užklausose, bet ir tuomet, kai iš šaltinio kodo fragmentų, paveldėtų temų ar portalo idėjų turi tapti patikimas įmonės projektas.

  • Esama padėtis, tikslinis vaizdas ir techninės rizikos vertinami kartu.
  • REST, Datenzugriff, Portale und Rollout werden nicht als Spätfolgen verschoben.
  • Sie sehen früh, welcher Weg wirtschaftlich und betrieblich tragfähig ist.

Pasidalinti įrašu

Tiesiogiai pasidalinti šiuo įrašu

LinkedIn, X, XING, Facebook, WhatsApp ir el. paštas yra iš karto prieinami. Instagramui paruošiame nuorodą ir trumpą tekstą iš karto.

El. paštas

Instagram atidaromas naujame skirtuke. Nuoroda ir trumpas tekstas iš anksto nukopijuojami į iškarpinę.