Ajakirjateemast projektipraktikasse
Sobivad teenuse- ja tehnilised lehed postituse jaoks
Miks „ChatGPT API koos Delphi FMX/VCL“ praktikas ei ole lihtsalt üks POST
Kes soovib ChatGPT API-d koos Delphi FMX/VCL integreerida, jõuab kiiresti lihtsa HTTP-POST‑ini. Tõsistes ärirakenduse keskkondades ebaõnnestub see aga kolmes kohas: (1) timeoutid ja taaskatsetused peavad olema deterministlikud, muidu kogevad kasutajad „hangivaid“ kasutajaliideseid, (2) streaming (Server-Sent Events, lühidalt SSE) on hea UX-i jaoks tihti vajalik, kuid Delphi-threadingus kiiresti vigadele avatud, ja (3) JSON ei ole lihtsalt „üks objekt“: veateated, kvoodiprobleemid, tühjad väljad või kergelt muudetud vastusevormid tuleb robustselt käsitleda.
Järgmine lähtekoodilõik näitab lähenemist, mis töötab nii FMX kui VCL puhul: iseseisev, testitav klient, mis töötab valikuliselt mitte-streamingu või streamingu režiimis, marshaldib UI-uuendused korrektselt (st täidab need põhilõime sünkroniseerimise kaudu) ja logib vigade korral selgelt. Lisaks on see üles ehitatud nii, et see integreerub olemasolevatesse kihtstruktuuridesse (nt „API-Client“ integratsioonikihis, UI jääb kõhn).
Arhitektuuri skeem: UI eraldamine, kliendi testitavuse säilitamine
Pika ajaloo Delphi-projektides võib tihti leida „HTTP im ButtonClick“. See töötab kuni esimese intsidendini. Soovitatav on väike klient, millel on:
- Konfiguratsioon: Base-URL, API-Key, mudel, timeoutid.
- Transpordikiht: HTTP-päring/vastus, taaskatsetused, timeoutid, proxy/SSL-valikud (sõltuvalt käituskeskkonnast).
- Parser: JSON-dekodeerimine, veaobjektid, tulemuse ekstraheerimine.
- UI-Hooks: callback tokeni-/teksti-streamingu jaoks, kuid ilma tugeva sõltuvuseta VCL/FMX komponentidest.
Nii saab integratsiooni individuaalsesse ettevõttetarkvarasse puhtalt hallata: klient on taaskasutatav teenustes, töölauaklientides, admin‑tööriistades või test‑harnessides.
Lähtekoodilõik: Delphi-klient SSE-streamingu, timeout/taaskatsetuse ja robustse JSON-iga
Kood kasutab THTTPClient (System.Net.HttpClient) ja parsib teadlikult minimaalselt System.JSON-iga. SSE puhul loetakse rearealt ja reageeritakse „data: …“-rea ilmumisele. See ei ole „WebSocket“, vaid HTTP-response-stream, mis tarnib pidevalt tekstiridasid. Oluline: loeme töötluslõimes ja marshaldime UI-uuendused põhilõimesse.
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: siin API-võti päises ‚Authorization: Bearer …‘.
// Ettevõttekeskkondades tuleb tagada, et võtmeid ei jõuaks logidesse.
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));
// Minimaalne messages-ehitus (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
// Levinud vorm: { „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(‚Ootamatu JSON-vastus (ei ole objekt).‘, 0, AJsonText);
Root := J as TJSONObject;
Choices := Root.GetValue<TJSONArray>(‚choices‘);
if (Choices = nil) or (Choices.Count = 0) then
raise EChatApiError.Create(‚Ootamatu JSON-vastus: choices puudub/tühi.‘, 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(‚Ootamatu JSON-vastus: message puudub.‘, 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
// Võrgu-/TLS-/timeout-vead: lihtne taaskatse tagasilöögiga.
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-viga ‚ + 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 alustatakse teadlikult asünkroonselt, et FMX/VCL ei blokeeruks.
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
// Voogedastuse vigade korral on sisu sageli siiski JSON.
TThread.Queue(nil,
procedure
begin
AOnEvent(‚Streamingi käivitamine ebaõnnestus (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-formaat: read nagu „data: {…}“ või „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-juppide töötlemine: 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.
Milleks see lähenemine hea on
Kood lahendab kolm tüüpilist probleemiklassi, mis VCL/FMX-is kiiresti kulukaks võivad muutuda:
- Voogedastus ilma UI-külmumata: HTTP-voogu loetakse taustal; UI-uuendused käivitatakse läbi
TThread.Queue(asünkroonselt pealõimele). See on FMX-is ja VCL-is robustne standardlahendus. - Uuesti proovimine võrguvigade korral: Kui tekib
ENetHTTPClientException, tehakse taustviivitusega (backoff) uus katse. See on teadlikult lihtne ja seda saab hiljem laiendada staatusekoodide (429/5xx) järgi. - Robustne JSON-parsimine: Pimeväljade castimise asemel kontrollitakse välju samm-sammult. See vähendab „Invalid type cast“ vigu erandlike vastuste puhul.
Piirtingimused, komistuskohad ja variandid
- SSE ei ole „tavaline JSON“: Voogesituse puhul tekib hulk sündmusi, mitte üksainus JSON-vastus. Seetõttu on reavaheline lugemine ja
[DONE]tuvastamine keskne. - THTTPClient ja proksid/SSL: Admin-võrkudes on TLS-inspektsioon ja proxy-nõuded tihti reaalsus. Arvestage
THTTPClient.ProxySettingsja vajadusel sertifikaaditeemadega. Debugimisel: logige alati staatusekood ja päised (ilma API-Key’ta). - Timeout-strateegia:
ResponseTimeouton voogesituse puhul delikaatne: kui määrate liiga lühikese, lõikab klient pikad vastused ära. UI-tööriistades on pikem timeout ja „Tühista“-nupp sageli mõistlikum kui „lühike ja karm“. - Lõimekatkestus: Näidiskood ei kasuta Cancel-Tokenit. Tootmiskeskkonna tööriistades tasub rakendada katkestusmehhanism (nt lipu +
Http.CancelAllkasutamine uuemates Delphi-versioonides või kontrollitud voo katkestus). - Mudeli- ja API-arenduse jätkamine: Vastuste struktuur võib muutuda. Hoidke parserid kaitsvad ja kesksetena, mitte laiali vormikoodis.
Debugimine olemasolevates Delphi-klientides: mida peaksite tegelikult logima
Integratsiooni projektides ebaõnnestub esmane käivitamine harva JSON-i tõttu, sagedamini mängivad rolli keskkonnaspetsiifikad. Tehnilisse logisse (fail, Eventlog, tsentraliseeritud logger) peaks praktikas kuuluma:
- Request-ID (endi määratud), ajatempel, siht-URL (ilma salajaste päringuparameetriteta).
- HTTP-staatusekood,
Content-Type, vastuse pikkus, kestus. - Lühendatud vastusekeha vigade korral (nt maks. 4–8 KB), et tuvastada kvota- või poliitikavead.
- Selge märgistus korduskatsetest: katsearv, viivitus, erandi klass.
API-Key ei kuulu kunagi logi. Kui logite Request-Body’d, tehke seda ainult diagnostika-buildides ja maskeeritult, sest promptid võivad sisaldada isikuandmeid või ärisaladusi.
Kontekst pärandolukordades: VCL, FMX ja Layer-3 arhitektuur
Paljud Delphi-rakendused töötavad klassikalises 3-kihilises loogikas („Layer-3 Architektur“: UI, äriloogika, andmed/integratsioon). ChatGPT-liidestuse jaoks on see kasulik: näidatud klient peaks kuuluma integratsioonikihisse; äriloogika otsustab, mida küsitakse; UI kuvab vaid ajaloo ja oleku. Nii väldite, et hilisem vahetus (teine pakkuja, on-prem-proxy, uued lõpp-punktid) lõhkuks vormid.
Ka Delphi-moderniseerimise jaoks on see hea lähtepunkt: esmalt stabiilne klient, siis UI-parendused (Streaming, Tühista, ajalugu), alles seejärel „nutikamad“ funktsioonid nagu struktureeritud vastused või tööriista-kõned.
Kokkuvõte: kindel alus, kuid mitte iga rakendus ei vaja voogedastust
Die ChatGPT API mit Delphi FMX/VCL sauber anzubinden lohnt sich besonders dort, wo UI-Reaktionsfähigkeit, Betriebssicherheit und Debuggability wichtig sind: Admin-Tools, prozessnahe Desktop-Clients oder Support-Werkzeuge in digitalen Unternehmenslösungen. Der gezeigte Schnipsel ist bewusst pragmatisch: SSE-Streaming ohne Spezialbibliotheken, Retry nur für echte Netzfehler, JSON defensiv geparst.
Einsatzgrenzen: Wenn Sie harte Compliance-Vorgaben, zentrale Prompt-Governance, Mandantenfähigkeit oder detaillierte Audit-Trails brauchen, reicht „ein Client im Desktop“ meist nicht aus. Dann gehört die Anbindung typischerweise in einen kontrollierten Server (z. B. eigener REST-Service), der Policies, Logging und Zugriffssteuerung zentral umsetzt. Für viele Delphi-Installationen ist der hier gezeigte Client aber ein belastbarer Startpunkt, der sich schrittweise in eine sauberere Gesamtarchitektur überführen lässt.
Im fachlichen Umfeld spielen auch Openai API In Delphi und Delphi Http Client Timeout Retry eine wichtige Rolle, wenn Integrationen, Datenflüsse und Weiterentwicklung sauber zusammenspielen müssen.
Projekt oder Modernisierungsvorhaben mit Net-Base besprechen.
Nächster Schritt
Wenn aus dem Thema ein reales Projekt wird, sollten Architektur, Bestand und Betrieb früh zusammen betrachtet werden.
Me ei toeta ainult üksikute küsimuste lahendamist, vaid ka siis, kui lähtekoodilõikudest, pärandsüsteemidest või portaalikontseptsioonidest peab saama usaldusväärne ettevõtteprojekt.
- Olemasolev olukord, sihtpilt ja tehnilised riskid hinnatakse üheskoos.
- REST, Datenzugriff, Portale und Rollout werden nicht als Spätfolgen verschoben.
- Sie sehen früh, welcher Weg wirtschaftlich und betrieblich tragfähig ist.