No žurnāla tēmas līdz projektu praksei
Atbilstošas pakalpojumu un tehniskās lapas rakstam
Kāpēc „ChatGPT API ar Delphi FMX/VCL“ praksē nav tikai POST
Kas vēlas ChatGPT API ar Delphi FMX/VCL pieslēgt, ātri nonāk pie vienkārša HTTP-POST. Tomēr reālā biznesa programmatūras vidē tas sagruva trīs vietās: (1) Timeouts un Retries jābūt deterministiskiem, jo citādi lietotāji piedzīvo «iestrēgušu» UI, (2) Streaming (Server-Sent Events, saīsinājumā SSE) bieži ir lietderīgs labai UX, taču Delphi-threaddingā tas ātri kļūst kļūdjutīgs, un (3) JSON nav tikai «viens objekts»: kļūdu ziņojumi, kvotu problēmas, tukši lauki vai viegli mainītas atbildes formas ir jāapstrādā robusti.
Nākamais Source-sīkums parāda pieeju, kas darbojas gan FMX, gan VCL: pašvaldīts, testējams clients, kas pēc izvēles strādā ne-streaming vai streaming režīmā, korekti marshalo UI-atjauninājumus (t.i., izpilda tos caur Main-Thread-sinhronizāciju) un kļūdu gadījumā veic saprotamu logēšanu. Papildus tas ir veidots tā, lai ērti iekļautos esošās slāņu struktūrās (piem., „API-Client“ integrācijas slānī, UI paliek plāns).
Architektur-Skizze: UI entkoppeln, Client testbar halten
Delphi-projektiem ar garu vēsturi bieži sastopams „HTTP im ButtonClick“. Tas darbojas līdz pirmajam incidentam. Ieteicams neliels clients ar:
- Konfiguration: Base-URL, API-Key, Modell, Timeouts.
- Transportschicht: HTTP-Request/Response, Retry, Timeout, Proxy/SSL-Optionen (atkarībā no darbības vides).
- Parser: JSON-Decoding, Fehlerobjekte, Ergebnisextraktion.
- UI-Hooks: Callback für Token-/Text-Streaming, aber ohne harte Abhängigkeit auf VCL/FMX Controls.
Tādā veidā integrāciju individuālajā uzņēmuma programmatūrā var uzturēt tīru: clients ir atkārtoti izmantojams servisos, desktop-klientos, admin-rīkos vai test-harnessos.
Source-Schnipsel: Delphi-Client mit SSE-Streaming, Timeout/Retry und robustem JSON
Kods izmanto THTTPClient (System.Net.HttpClient) un parsē apzināti tikai minimāli ar System.JSON. SSE gadījumā tiek lasīts pa rindiņām un reaģēts uz „data: …“. Tas nav „WebSocket“, bet gan HTTP-response-stream, kas nepārtraukti piegādā teksta rindas. Svarīgi: lasām darba pavedienā (Worker-Thread) un marshalojam UI-atjauninājumus galvenajā pavedienā (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: šeit API atslēga kā "Authorization: Bearer …".
// Uz uzņēmuma vidi uzmanīgi nodrošināt, lai atslēgas neiekļūtu žurnālos.
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āla messages struktūra (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
// Biežākā 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('Neparedzēta JSON-atbilde (nav objekts).', 0, AJsonText);
Root := J as TJSONObject;
Choices := Root.GetValue<TJSONArray>('choices');
if (Choices = nil) or (Choices.Count = 0) then
raise EChatApiError.Create('Neparedzēta JSON-atbilde: choices trūkst/tukšs.', 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('Neparedzēta JSON-atbilde: message trūkst.', 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
// Tīkla/TLS/laika pārsnieguma kļūmes: vienkāršs atkārtojums ar 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 kļūda ' + 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 mērķtiecīgi startē asinhroni, lai FMX/VCL netiktu bloķēts.
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 kļūmju gadījumā saturs bieži vien tomēr ir JSON.
TThread.Queue(nil,
procedure
begin
AOnEvent('Streaming uzsākšana neizdevās (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āts: rindas kā "data: {...}" vai "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 fragmenta novērtēšana: 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.
Kāpēc šī pieeja ir noderīga
Kods risina trīs tipiskas problēmu kategorijas, kas VCL/FMX vidē ātri kļūst dārgas:
- Streaming bez UI iesaldēšanas: HTTP plūsmu lasa fonā; UI atjauninājumi notiek caur
TThread.Queue(asinhroni uz galveno pavedienu). FMX un VCL tas ir drošs standarta risinājums. - Atkārtotas mēģināšanas tīkla kļūmju gadījumā: Pie
ENetHTTPClientExceptiontiek mēģināts vēlreiz ar backoff mehānisma izmantošanu. Tas ir apzināti vienkārši un vēlāk var tikt paplašināts, ņemot vērā statuskodus (429/5xx). - Robusta JSON parsēšana: Tā vietā, lai lauciņus akli pārvērstu, tie tiek pārbaudīti soli pa solim. Tas samazina “Invalid type cast” kļūdas īpašu atbilžu gadījumā.
Nosacījumi, slazdi un varianti
- SSE nav „parasts JSON”: Streaming gadījumā tiek saņemta notikumu secība, nevis viena JSON atbilde. Tāpēc būtiski ir lasīt pa rindām un atpazīt
[DONE]. - THTTPClient un proksi/SSL: Administratīvajos tīklos bieži sastopama TLS inspekcija un obligāti proksi. Ieplānojiet
THTTPClient.ProxySettingsun, ja nepieciešams, sertifikātu jautājumus. Debugošanai: vienmēr žurnālojiet statuskodu/galvenes (bez API atslēgas). - Timeout stratēģija:
ResponseTimeoutstreamingā ir jūtīgs: ja iestatāt pārāk īsu, klients pārgriezīs garas atbildes. UI rīkos biežāk jēgpilnāk ir ilgāks timeout un „Atcelt” poga, nevis „īsais un cietais” variants. - Pavediena pārtraukšana: Paraugkodā nav Cancel-Token. Produktīvām rīku versijām atmaksājas pārtraukšanas mehānisms (piem., karoga mainīgais +
Http.CancelAlljaunākās Delphi versijās vai ar kontrolētu plūsmas pārtraukšanu). - Modeļa un API attīstība: Atbilžu struktūra var mainīties. Turiet parserus defensīvus un centrālus, neizkaisītus pa formu kodu.
Debugēšana esošajos Delphi klientos: Was Sie wirklich loggen sollten
Integrācijas projektos pirmā iedarbināšana reti neizdodas JSON dēļ; biežāk to izraisa vides nianses. Tehniskajā žurnālā (fails, Eventlog, centrālais logger) praksē jāietver:
- Request-ID (paša piešķirta), laika atzīme, mērķa URL (bez slepenajiem query stringiem).
- HTTP statuskods, Content-Type, atbildes garums, izpildlaiks.
- Saīsināts Response-Body kļūdu gadījumā (piem., max. 4–8 KB), lai varētu atpazīt kvotu/politikas kļūdas.
- Eksplīcīta atkārtoto mēģinājumu atzīmēšana: Attempt, Delay, Exception klase.
API atslēga nekad nedrīkst nonākt žurnālā. Ja žurnālojat Request-Body, tad tikai diagnostikas buildos un ar maskēšanu, jo prompti var saturēt personas datus vai uzņēmuma informāciju.
Novietojums vecās vides situācijām: VCL, FMX und Layer-3 Architektur
Daudzas Delphi lietotnes darbojas klasiskā 3-slāņu loģikā („Layer-3 Architektur“: UI, biznesa loģika, dati/integrācija). ChatGPT savienojumam tas ir noderīgi: parādītais klients pieder integrācijas slānim; biznesa loģika nosaka, ko jautāt; UI tikai attēlo norises vēsturi un statusu. Tas novērš, ka vēlākas izmaiņas (cits pakalpojuma sniedzējs, On-Prem-Proxy, jauni galapunkti) formas „saplēš”.
Arī Delphi modernizācijai tas ir labs sākumpunkts: vispirms stabils klients, pēc tam UI uzlabojumi (straumēšana, Atcelt, vēsture), un tikai pēc tam „inteliģentākas” funkcijas, piemēram strukturētas atbildes vai rīku izsaukumi.
Secinājums: Stabils pamats, bet ne katrai lietojumprogrammai nepieciešama straumēšana
ChatGPT API ar Delphi FMX/VCL tīru integrāciju īpaši atmaksājas tur, kur svarīga lietotāja saskarnes atsaucība, sistēmas stabilitāte un atkļūdošanas iespējas: administrēšanas rīki, procesam tuvie darbvirsmas klienti vai atbalsta rīki digitālajos uzņēmumu risinājumos. Parādītais fragments ir apzināti pragmatisks: SSE straumēšana bez speciālām bibliotēkām, atkārtošana tikai īsteniem tīkla kļūdām, JSON defensīvi parsēts.
Lietošanas ierobežojumi: ja jums nepieciešamas stingras atbilstības prasības, centrāla promptu pārvaldība, vairāku nomnieku atbalsts vai detalizēti audita ieraksti, „klients darbvirsmā“ parasti nepietiek. Tad savienošanai parasti jābūt kontrolētam serverim (piem., īpašs REST-serviss), kas centrāli īsteno politikas, žurnēšanu un piekļuves kontroli. Tomēr daudzām Delphi instalācijām rādītais klients ir noturīgs sākumpunkts, ko pakāpeniski var pārnest uz tīrāku kopējo arhitektūru.
Tehniskajā kontekstā nozīmīgu lomu spēlē arī Openai API in Delphi un Delphi Http Client Timeout Retry, kad integrācijām, datu plūsmām un turpmākajai attīstībai jādarbojas saskaņoti.
Nächster Schritt
Wenn aus dem Thema ein reales Projekt wird, sollten Architektur, Bestand und Betrieb früh zusammen betrachtet werden.
Mēs atbalstām ne tikai atsevišķu jautājumu risināšanā, bet arī tad, kad no avota koda fragmentiem, mantojuma sistēmu jautājumiem vai portāla idejām jāizveido stabils uzņēmuma līmeņa projekts.
- Esošais stāvoklis, mērķa stāvoklis un tehniskie riski tiek kopīgi vērtēti.
- REST, Datenzugriff, Portale und Rollout werden nicht als Spätfolgen verschoben.
- Sie sehen früh, welcher Weg wirtschaftlich und betrieblich tragfähig ist.