本投稿はDelphi AdventCalender 2025 #04の記事です。
この記事は,Delphi13でコードを書き,Windows11で動作を確認しています。
はじめに
Delphi13を使ってLM Studio上のLLMとチャットするアプリを作ります。
この記事は,
の続きになります。
この記事て使ったコードが前提になりますので,この記事を読む前にぜひこちらをご覧ください。
LM Studioの設定
DelphiでローカルLLMを利用したアプリを開発 その1 で作ったKP.ListString.pasを使って,ローカルに配置されたLM StudioとOllamaを利用するクラスを作ります。
LM Studioをインストール
LM StudioはAPIからモデルをアンロードできません。またcontent lengthもWebアプリごとに指定することができませんが,GUIで管理できるので最初の運用は簡単です。
WindowsにLM Studioをインストール
上記の公式サイトからインストーラをダウンロードしてインストールします。
MacOSにLM Studioをインストール
macOSにインストールする場合は,Homebrewを使うと簡単です。
brew install --cask lm-studio
サーバを起動する
LM Studioを起動したら,右下にある⚙アイコンをクリックして設定を開き,アプリの更新を確認し,更新します。
また,言語に日本語を選択できます。
LM Studioの左上にある開発者を選択し,StatusをRunningにするとAPIが利用できます。
サーバとして運用するなら,DeveloperタブのローカルLLMサービス(ヘッドレス)をチェックします。
その隣のServer Settingを開いて,ローカルネットワークで提供をチェックするとLAN内の別のPCからLM Studioを利用できるようになります。右上にサーバのURLが表示されます。
モデルをロードする
LM Studioの左上にある探索を選択し,検索窓にLLMモデル名を入れて利用するモデルをダウンロードします。
検索されたモデルを選ぶと,そのPCのVRAMで動作可能かが表示されます。
まずはGoogleのGemma 3n か Gemma 3を選択するとよいのではないかと思います。後で交換可能です。
今回はGemma 3 12Bを利用します。
動作確認する
LM Studioの左上のチャットを選択し,画面の上部中央にある「モデルを選択してください」をクリックして,Gemma 3 12Bを選択し,モデルを読み込みます。
モデルのコンテキスト長はここで変更可能です。より詳しい設定や,設定を記憶させることも可能です。
簡単な質問をしてモデルの動作を確認してください。
LM StudioとOllamaを使うクラスを作る
LM StudioのAPIは,OpenAIのAPIと互換性があり,OllamaのOpenAI APIと同じように利用できます。そこで,以前作ったOllamaを利用するRESTと同じように利用できるクラスを作りました。LLMモデルの選択とチャットができるようになっています。
※ 2025/12/06更新 回答が空なら回答できなかった扱いにするように変更しました。
KP.AICompThreads.pas
unit KP.AICompThreads;
(*
OllamaとLM Studioのローカル生成AIを利用するスレッドクラス
LICENSE
Copyright (c) 2025 Yuzuru Kato
Released under the MIT license
http://opensource.org/licenses/mit-license.php
2025/12/06 回答が空なら回答できなかった扱いにする
*)
interface
uses
System.Types, System.Classes,
REST.Client, // REST
KP.ListString;
const
gAICompErrorText: string = 'The following questions could not be answered:'; // エラー時に履歴に残す先頭文字列
gPromptTarget = '####'; // プロンプトの中の####をインプットの内容に置き換える
gAICompTimeout = 60000; // GenerateとChatのタイムアウト
gAttachHeader = ':attach:'; // 画像を添付する場合のデリミタ 以降にファイルの場所を記述
gTimeoutFirstRate = 5 ; // 最初のタイムアウトは通常の5倍
type
TAICompType = (Ollama, Ollama_OpenAI, LM_Studio);
// Ollama: Ollama API
// Ollama_OpenAI: OllamaのOpenAI互換API API Keyのヘッダが不要
// LM_Studio: LM StudioのOpenAI互換API API Keyのヘッダが不要
// ※OpenAI互換APIの処理は共通 デフォルトのポート番号が違うため分けている
//==============================================================================
// OpenAI / Ollama 共通
type
TAICompThreadError = procedure(AText:string) of object;
// for ModelList
TAICompThreadModelListOut = procedure(AAICompModelList: TStringDynArray) of object;
// AICompThreadModelList
type
TAICompThreadModelList = class(TThread)
private
{ Private 宣言 }
FAICompHost: string;
protected
FAICompThreadModelListOut: TAICompThreadModelListOut;
FAICompThreadError: TAICompThreadError;
procedure Execute; override; abstract;
public
constructor Create(AHost:string;
AAICompThreadModelListOut: TAICompThreadModelListOut;
AAICompThreadError:TAICompThreadError); virtual;
end;
type
TAICompParamIn = record
AICompType: TAICompType;
Host: string;
Model: string;
SystemPrompt: string;
Prompt: string;
temperature: single;
top_p: Single; // top-kと連携します。値が大きいほど(0.95など),テキストの多様性が増し,値を小さくする(0.5など)テキストは,より焦点を絞った保守的なテキストが生成されます。(デフォルト: 0.9)
Sleep: integer; // OllamaThreadChatQueueのループの休み
Timeout: integer; // GenerateとChatのタイムアウト
// 以下はOllamaのみ適用される
num_ctx: integer; // コンテキスト長
num_predict: integer; // 無限生成
top_k: integer; // ナンセンスを生成する確率を減らします。値が大きいほど(例:100),回答が多様化し,値が低いほど(例:10)保守的になります。(デフォルト: 40)
min_p: Single; // top_pに代わるもので,品質と多様性のバランスを確保することを目指しています。パラメーター p は,最も可能性の高いトークンの確率に対する,トークンが考慮される最小の確率を表します。たとえば,p=0.05 で,最も可能性の高いトークンの確率が 0.9 の場合,値が 0.045 未満のロジットはフィルターで除外されます。(デフォルト: 0.0)
KeepAlive: string; // Load Keep Alive 5s default
procedure Init;
end;
TAICompParamOut = record
prompt_tokens :integer;
completion_tokens:integer;
total_tokens :integer;
tick_time :Int64;
end;
TAICompThreadIn = procedure(AText:TListString) of object;
TAICompThreadOut = procedure(AText:String; AAICompParamOut:TAICompParamOut) of object;
TAICompThreadFinish = procedure of object;
type
TAICompThreadChat = class(TThread)
private
{ Private 宣言 }
protected
FAICompParamIn: TAICompParamIn;
FText: string;
FAICompThreadIn : TAICompThreadIn;
FAICompThreadOut : TAICompThreadOut;
FAICompThreadFinish : TAICompThreadFinish;
FAICompThreadError : TAICompThreadError;
procedure Execute; override; abstract;
public
constructor Create(AAICompParamIn:TAICompParamIn;
AAICompThreadIn : TAICompThreadIn;
AAICompThreadOut : TAICompThreadOut;
AAICompThreadFinish : TAICompThreadFinish;
AAICompThreadError : TAICompThreadError); virtual;
end;
// プライベートホスト名 タイプごとのデフォルトポート番号を割り当てる 正しくない場合は空を返す
function PrivateHostNameMod(AHostName: string; AAICompType: TAICompType):string;
//==============================================================================
// OpenAI
// OpenAIThreadModelList
type
TOpenAIThreadModelList = class(TAICompThreadModelList)
private
{ Private 宣言 }
protected
procedure Execute; override;
public
end;
// OpenAIThreadChat
type
TOpenAIThreadChat = class(TAICompThreadChat)
private
{ Private 宣言 }
protected
procedure Execute; override;
public
end;
//==============================================================================
// Ollama
// OllamaThreadModelList
type
TOllamaThreadModelList = class(TAICompThreadModelList)
private
{ Private 宣言 }
protected
procedure Execute; override;
end;
// OllamaThreadChat
type
TOllamaThreadChat = class(TAICompThreadChat)
private
{ Private 宣言 }
protected
procedure Execute; override;
public
end;
implementation
uses
System.SysUtils, System.Math,
System.Diagnostics, // TStopWatch
System.JSON, System.NetEncoding,
System.JSON.Types, System.JSON.Serializers, // TJsonSerializer
System.RegularExpressions,
REST.Types; // REST
//==============================================================================
// 共通
const
OpenAILastErrors:array[-3..-1] of string
=('Couldn''t parse output.', // -3 パースエラー
'Couldn''t connect to OpenAI. ', // -2 接続エラー
'OpenAI Error'); // -1 その他のエラー
OllamaLastErrors:array[-3..-1] of string
=('Couldn''t parse output.', // -3 パースエラー
'Couldn''t connect to Ollama. ', // -2 接続エラー
'Ollama Error'); // -1 その他のエラー
function InPutEx(APrompt, AInput:string): string; // InputにPromptを追加
begin
if APrompt='' then begin
Result:=AInput;
end else
if pos(gPromptTarget,APrompt)<>0 then begin
Result:=StringReplace(APrompt,gPromptTarget,AInput,[]); // プロンプトの中の####をインプットの内容に置き換える
end else begin
Result:=APrompt+' '+AInput;
end;
end;
function FileNameToBase64Str(AFileName:string): string;
var
m: TMemoryStream;
s: TStringStream;
begin
if FileExists(AFileName) then begin
m:=TMemoryStream.Create;
m.LoadFromFile(AFileName);
s:=TStringStream.Create;
TNetEncoding.Base64.Encode(m, s);
m.Free;
Result:=StringReplace(s.DataString,#13#10,'',[rfReplaceAll]);
s.Free;
end else begin
Result:='';
end;
end;
procedure StringSplitAttach(AInput:string; var s, f, e:string); // 入力を文字列とファイル名と拡張子情報に分解
var
j:integer;
begin
j:=pos(gAttachHeader,AInput);
if j>0 then begin
s:=copy(AInput, 1, j-1);
f:=copy(AInput, j+Length(gAttachHeader), Length(AInput));
if FileExists(f) then begin
e:=LowerCase(ExtractFileExt(f));
if (e='.png') then begin
e:='png';
end else
if (e='.jpg')or(e='.jpeg') then begin
e:='jpeg';
end else begin
f:='';
end;
end else begin
f:='';
end;
end else begin
s:=AInput;
f:='';
end;
end;
//------------------------------------------------------------------------------
{ TAICompParamIn }
procedure TAICompParamIn.Init;
begin
AICompType := TAICompType.Ollama;
SystemPrompt := '';
Prompt := '';
temperature := 0.1; // 0.8; 0 - 2
top_p:=0.1; // 0.9; // top-kと連携します。値が大きいほど(0.95など),テキストの多様性が増し,値を小さくする(0.5など)テキストは,より焦点を絞った保守的なテキストが生成されます。(デフォルト: 0.9)
// スレッド用のパラメータ
Sleep := 10; // OllamaThreadChatQueueのループの休み
Timeout := gAICompTimeout; // GenerateとChatのタイムアウト
// 以下はOllamaのみ適用される
num_ctx := 2048; // コンテキスト長
num_predict := -1; // -1; 無限生成
top_k := 10; // 40; ナンセンスを生成する確率を減らします。値が大きいほど(例:100),回答が多様化し,値が低いほど(例:10)保守的になります。(デフォルト: 40)
min_p := 0.0; // top_pに代わるもので,品質と多様性のバランスを確保することを目指しています。パラメーター p は,最も可能性の高いトークンの確率に対する,トークンが考慮される最小の確率を表します。たとえば,p=0.05 で,最も可能性の高いトークンの確率が 0.9 の場合,値が 0.045 未満のロジットはフィルターで除外されます。(デフォルト: 0.0)
KeepAlive := '30m'; // '5m'; '5s'; ModelのKeep Alive -1:無制限 0:即時停止
end;
//------------------------------------------------------------------------------
{ TAICompThreadModelList }
constructor TAICompThreadModelList.Create(AHost: string;
AAICompThreadModelListOut: TAICompThreadModelListOut;
AAICompThreadError: TAICompThreadError);
begin
FAICompHost := AHost;
FAICompThreadModelListOut := AAICompThreadModelListOut;
FAICompThreadError := AAICompThreadError;
FreeOnTerminate := True;
inherited Create(False);
end;
//------------------------------------------------------------------------------
{ TAICompThreadChat }
constructor TAICompThreadChat.Create(AAICompParamIn: TAICompParamIn;
AAICompThreadIn: TAICompThreadIn; AAICompThreadOut: TAICompThreadOut;
AAICompThreadFinish: TAICompThreadFinish;
AAICompThreadError: TAICompThreadError);
begin
FAICompParamIn := AAICompParamIn;
FAICompThreadIn := AAICompThreadIn;
FAICompThreadOut := AAICompThreadOut;
FAICompThreadFinish := AAICompThreadFinish;
FAICompThreadError := AAICompThreadError;
FreeOnTerminate := True;
inherited Create(False);
end;
//------------------------------------------------------------------------------
// HostName
procedure HostNameParse(AHostName:string; out p1, p2, p3:string);
var
match:TMatch;
begin
match := TRegEx.Match(AHostName, '^(|https?:\/\/)([^:]*)(|:\d{1,5})$');
if match.Success then begin
p1 := match.Groups[1].Value;
p2 := match.Groups[2].Value;
p3 := match.Groups[3].Value;
end else begin
p1 := '';
p2 := '127.0.0.1';
p3 := '';
end;
end;
function isPrivateHostName(AHostName:string):boolean;
begin
if (AHostName='localhost')or(AHostName='127.0.0.1') then begin
Result:=True;
end else
if TRegEx.Match(AHostName, '^10\.([0-9]\n[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\.([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\.([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])$').Success then begin
Result := True;
end else
if TRegEx.Match(AHostName, '^172\.(1[6-9]|2[0-9]|3[0-1])\.([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\.([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])$').Success then begin
Result := True;
end else
if TRegEx.Match(AHostName, '192\.168\.([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\.([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])$').Success then begin
Result := True;
end else begin
Result := False;
end;
end;
function PrivateHostNameMod(AHostName: string; AAICompType: TAICompType):string;
var
p1,p2,p3:string;
begin
HostNameParse(AHostName, p1, p2, p3);
if p1='' then begin
p1 := 'http://';
end;
if p3='' then begin
case AAICompType of
Ollama, Ollama_OpenAI: p3 := ':11434';
LM_Studio: p3 := ':1234';
end;
end;
if p2='localhost' then begin
Result := p1+'127.0.0.1'+p3;
end else
if p2='127.0.0.1' then begin
Result := p1+p2+p3;
end else
if TRegEx.Match(p2, '^10\.([0-9]\n[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\.([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\.([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])$').Success then begin
Result := p1+p2+p3;
end else
if TRegEx.Match(p2, '^172\.(1[6-9]|2[0-9]|3[0-1])\.([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\.([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])$').Success then begin
Result := p1+p2+p3;
end else
if TRegEx.Match(p2, '192\.168\.([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\.([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])$').Success then begin
Result := p1+p2+p3;
end else begin
Result := '';
end;
end;
//==============================================================================
{ OpenAI }
function OpenAIURL(AHost, AMethod:string):string;
begin
Result:=AHost+'/v1/'+AMethod;
end;
//------------------------------------------------------------------------------
{ TOpenAIThreadModelList }
procedure OpenAIRESTRequestGET(ARESTRequest: TRESTRequest; AHost, AMethod: string);
begin
ARESTRequest.Client := TRESTClient.Create(ARESTRequest);
ARESTRequest.Client.SynchronizedEvents := False;
ARESTRequest.Client.BaseURL := OpenAIURL(AHost, AMethod);
ARESTRequest.Method := TRESTRequestMethod.rmGET;
ARESTRequest.Response := TRESTResponse.Create(ARESTRequest);
ARESTRequest.Params.Clear; // アイテムは不要
ARESTRequest.SynchronizedEvents := False;
end;
function OpenAIJSONListToText(AContent:string;var List:TStringDynArray):
integer;
type
TOpenAIModel=record
id:string;
end;
TOpenAIModelList=record
data:array of TOpenAIModel;
end;
var
st:TStringList;
m:TOpenAIModel;
begin
Result:=-3;
SetLength(List,0);
var JsonSerializer := TJsonSerializer.Create;
try
var JSONed := JsonSerializer.Deserialize<TOpenAIModelList>(AContent);
st:=TStringList.Create;
for m in JSONed.data do begin
st.Add(m.id);
end;
List:=st.ToStringArray;
st.Free;
Result:=0;
finally
JsonSerializer.Free;
end;
end;
function OpenAIRESTExecute(ARESTRequest:TRESTRequest; var AResponse: string): integer; // 0:成功 -2:接続エラー
var
i:integer;
begin
Result := -1;
try
ARESTRequest.Execute;
except
Result := -2;
end;
if (Result=-1)and(ARESTRequest.Response.StatusCode=200) then begin
AResponse := ARESTRequest.Response.Content;
Result := 0;
end else begin
AResponse := '';
Result := -2; // -2:接続エラー
end;
end;
procedure TOpenAIThreadModelList.Execute;
var
LRESTRequest: TRESTRequest;
LResponse: string;
LOllamaModelList:TStringDynArray;
LastError: integer;
begin
LastError:=-1;
LRESTRequest:=TRESTRequest.Create(nil);
OpenAIRESTRequestGET(LRESTRequest, FAICompHost, 'models');
try
LastError := OpenAIRESTExecute(LRESTRequest, LResponse);
if LastError=0 then begin
LastError := OpenAIJSONListToText(LResponse, LOllamaModelList);
if LastError=0 then begin
if Assigned(FAICompThreadModelListOut) then begin
Synchronize(
procedure
begin
FAICompThreadModelListOut(LOllamaModelList);
end
);
end;
end;
end;
if LastError<0 then begin
Synchronize(
procedure
begin
FAICompThreadError(OpenAILastErrors[LastError]);
end
);
end;
finally
LRESTRequest.Response.Free;
LRESTRequest.Client.Free;
LRESTRequest.Free;
end;
end;
//------------------------------------------------------------------------------
{ TOpenAIThreadChat }
procedure OpenAIRESTRequestPOST(ARESTRequest: TRESTRequest);
begin
ARESTRequest.Client := TRESTClient.Create(ARESTRequest);
ARESTRequest.Client.SynchronizedEvents := False;
ARESTRequest.Method := TRESTRequestMethod.rmPOST;
ARESTRequest.Response := TRESTResponse.Create(ARESTRequest);
ARESTRequest.Params.Clear;
// 空のアイテムを作っておく
ARESTRequest.Params.AddItem('OpenAIJSON', '', TRESTRequestParameterKind.pkREQUESTBODY,
[], TRESTContentType.ctAPPLICATION_JSON);
ARESTRequest.SynchronizedEvents := False;
end;
function OpenAIChatToJSON(AAICompParamIn:TAICompParamIn; AInput:TListString):string;
var
LJsonObj: TJSONObject;
LJsonArray: TJSONArray;
LJsonObjs: array of TJSONObject;
LJsonArrays: array of TJSONArray;
LJsonObjs2: array of array[0..2]of TJSONObject;
i,j,c:integer;
s,f,e:string;
begin
LJsonObj := TJSONObject.Create;
LJsonObj.AddPair('model', AAICompParamIn.Model);
c:=AInput.Count;
SetLength(LJsonArrays,c);
SetLength(LJsonObjs2,c);
if AAICompParamIn.SystemPrompt='' then begin
SetLength(LJsonObjs,c);
j := 0;
end else begin
SetLength(LJsonObjs,c+1);
LJsonObjs[0]:=TJSONObject.Create;
LJsonObjs[0].AddPair('role', 'developer');
LJsonObjs[0].AddPair('content', AAICompParamIn.SystemPrompt);
j := 1;
end;
for i:=0 to c-1 do begin
LJsonObjs[j]:=TJSONObject.Create;
if (i mod 2)=0 then begin
LJsonObjs[j].AddPair('role', 'user');
end else begin
LJsonObjs[j].AddPair('role', 'assistant');
end;
StringSplitAttach(AInput[i], s, f, e); // 入力を文字列とファイル名と拡張子情報に分解
s:=InputEx(AAICompParamIn.Prompt, s);
if f='' then begin
LJsonObjs[j].AddPair('content', s);
end else begin
LJsonObjs2[i,0]:=TJSONObject.Create;
LJsonObjs2[i,0].AddPair('type', 'text');
LJsonObjs2[i,0].AddPair('text', s);
LJsonObjs2[i,1]:=TJSONObject.Create;
LJsonObjs2[i,1].AddPair('type', 'image_url');
LJsonObjs2[i,2]:=TJSONObject.Create;
LJsonObjs2[i,2].AddPair('url','data:image/'+e+';base64,'+FileNameToBase64Str(f));
LJsonObjs2[i,1].AddPair('image_url', LJsonObjs2[i,2]);
LJsonArrays[i]:=TJSONArray.Create;
LJsonArrays[i].Add(LJsonObjs2[i,0]);
LJsonArrays[i].Add(LJsonObjs2[i,1]);
LJsonObjs[j].AddPair('content', LJsonArrays[i]);
end;
inc(j);
end;
LJsonArray := TJSONArray.Create;
for i:= 0 to j-1 do begin
LJsonArray.Add(LJsonObjs[i]);
end;
LJsonObj.AddPair('messages', LJsonArray);
LJsonObj.AddPair('temperature', AAICompParamIn.temperature);
LJsonObj.AddPair('top_p', AAICompParamIn.top_p);
LJsonObj.AddPair('stream', False);
Result:= LJsonObj.ToString;
LJsonObj.Free;
end;
function OpenAIJSONToChat(AContent:string;var AOutput:string;
var AAICompParamOut:TAICompParamOut): integer;
type
TOpenAIMessage=record
content:string;
end;
TOpenAIChoice=record
[JsonName('message')] _message:TOpenAIMessage;
end;
TOpenAIUsage=record
prompt_tokens :integer;
completion_tokens:integer;
total_tokens :integer;
end;
TOpenAIChat=record
choices:array of TOpenAIChoice;
usage:TOpenAIUsage;
end;
begin
Result:=-3;
var JsonSerializer := TJsonSerializer.Create;
try
var JSONed := JsonSerializer.Deserialize<TOpenAIChat>(AContent);
AOutput := JSONed.choices[0]._message.content;
AAICompParamOut.prompt_tokens := JSONed.usage.prompt_tokens;
AAICompParamOut.completion_tokens := JSONed.usage.completion_tokens;
AAICompParamOut.total_tokens := JSONed.usage.total_tokens;
Result:=0;
finally
JsonSerializer.Free;
end;
end;
function OpenAIChat(ARESTRequest:TRESTRequest; AAICompParamIn:TAICompParamIn;
AListString: TListString; var AOutPut: string;
var AAICompParamOut:TAICompParamOut): integer;
var
LastError:integer;
LResponse:string;
begin
LastError := -1;
ARESTRequest.Client.ReadTimeout := AAICompParamIn.Timeout;
ARESTRequest.Client.BaseURL := OpenAIURL(AAICompParamIn.Host,'chat/completions');
ARESTRequest.Params[0].Value := OpenAIChatToJSON(AAICompParamIn, AListString);
LastError := OpenAIRESTExecute(ARESTRequest, LResponse);
if LastError=0 then begin
LastError := OpenAIJSONToChat(LResponse,AOutput,AAICompParamOut);
if LastError<>0 then begin
AOutput := '';
AAICompParamOut.prompt_tokens := 0;
AAICompParamOut.completion_tokens := 0;
AAICompParamOut.total_tokens := 0;
end;
end;
Result := LastError;
end;
procedure TOpenAIThreadChat.Execute;
var
LRESTRequest: TRESTRequest;
LText:string;
FListString:TListString;
LAICompParamOut:TAICompParamOut;
LastError:integer;
StopWatch: TStopWatch;
isFirst: boolean;
dTimeOut:integer;
begin
FListString:=TListString.Create;
LRESTRequest:=TRESTRequest.Create(nil);
OpenAIRESTRequestPOST(LRESTRequest);
isFirst := True;
dTimeOut := FAICompParamIn.Timeout;
while True do begin
if Terminated then begin
LRESTRequest.Response.Free;
LRESTRequest.Client.Free;
LRESTRequest.Free;
FListString.Free;
// LastErrorを表示する処理が必要か?
if Assigned(FAICompThreadFinish) then begin
Synchronize(
procedure
begin
FAICompThreadFinish;
end
);
end;
exit;
end;
if Assigned(FAICompThreadIn) then begin
Synchronize(
procedure
begin
FAICompThreadIn(FListString);
end
);
end;
if FListString.Count>0 then begin
if isFirst then begin
FAICompParamIn.Timeout:=dTimeOut*gTimeOutFirstRate;
isFirst := False;
end else begin
FAICompParamIn.Timeout:=dTimeOut;
end;
StopWatch := TStopWatch.StartNew;
LastError := OpenAIChat(LRESTRequest, FAICompParamIn, FListString, LText, LAICompParamOut);
LAICompParamOut.tick_time := StopWatch.ElapsedMilliseconds;
if LastError=0 then begin
if LText='' then begin // 回答が空なら回答できなかった扱いにする
LText := gAICompErrorText+' '+FListString[FListString.Count-1];
end;
if Assigned(FAICompThreadOut) then begin
Synchronize(
procedure
begin
FAICompThreadOut(LText, LAICompParamOut);
end
);
end;
end else begin
LText := gAICompErrorText+' '+FListString[FListString.Count-1];
if Assigned(FAICompThreadOut) then begin
Synchronize(
procedure
begin
FAICompThreadOut(LText, LAICompParamOut);
end
);
end;
if Assigned(FAICompThreadError) then begin
Synchronize(
procedure
begin
FAICompThreadError(OllamaLastErrors[LastError]);
end
);
end;
end;
end;
if FAICompParamIn.Sleep>0 then sleep(FAICompParamIn.Sleep);
end;
end;
//==============================================================================
{ Ollama }
function OllamaURL(AHost,AMethod:string):string;
// RESTRequest.Client.BaseURL := OllamaURL(AHost,AMethod);
begin
Result:=AHost+'/api/'+AMethod;
end;
//------------------------------------------------------------------------------
{ TOllamaThreadModelList }
procedure OllamaRESTRequestGET(ARESTRequest: TRESTRequest; AHost, AMethod: string);
begin
ARESTRequest.Client := TRESTClient.Create(ARESTRequest);
ARESTRequest.Client.SynchronizedEvents := False;
ARESTRequest.Client.BaseURL := OllamaURL(AHost,AMethod);
ARESTRequest.Method := TRESTRequestMethod.rmGET;
ARESTRequest.Response := TRESTResponse.Create(ARESTRequest);
ARESTRequest.Params.Clear; // アイテムは不要
ARESTRequest.SynchronizedEvents := False;
end;
function OllamaRESTExecute(ARESTRequest:TRESTRequest; var AResponse: string): integer; // 0:成功 -2:接続エラー
var
i:integer;
begin
Result := -1;
try
ARESTRequest.Execute;
except
Result := -2;
end;
if (Result=-1)and(ARESTRequest.Response.StatusCode=200) then begin
AResponse := ARESTRequest.Response.Content;
Result := 0;
end else begin
AResponse := '';
Result := -2; // -2:接続エラー
end;
end;
function OllamaJSONListToText(AContent:string;var List:TStringDynArray):
integer;
type
TOllamaModel=record
name:string;
end;
TOllama=record
models:array of TOllamaModel;
end;
var
st:TStringList;
m:TOllamaModel;
begin
Result:=-3;
SetLength(List,0);
var JsonSerializer := TJsonSerializer.Create;
try
var JSONed := JsonSerializer.Deserialize<TOllama>(AContent);
st:=TStringList.Create;
for m in JSONed.models do begin
st.Add(m.name);
end;
List:=st.ToStringArray;
st.Free;
Result:=0;
finally
JsonSerializer.Free;
end;
end;
procedure TOllamaThreadModelList.Execute;
var
LRESTRequest: TRESTRequest;
LResponse: string;
LOllamaModelList:TStringDynArray;
LastError: integer;
begin
LastError:=-1;
LRESTRequest:=TRESTRequest.Create(nil);
OllamaRESTRequestGET(LRESTRequest, FAICompHost, 'tags');
try
LastError := OllamaRESTExecute(LRESTRequest, LResponse);
if LastError=0 then begin
LastError := OllamaJSONListToText(LResponse, LOllamaModelList);
if LastError=0 then begin
if Assigned(FAICompThreadModelListOut) then begin
Synchronize(
procedure
begin
FAICompThreadModelListOut(LOllamaModelList);
end
);
end;
end;
end;
if LastError<0 then begin
Synchronize(
procedure
begin
FAICompThreadError(OllamaLastErrors[LastError]);
end
);
end;
finally
LRESTRequest.Response.Free;
LRESTRequest.Client.Free;
LRESTRequest.Free;
end;
end;
//------------------------------------------------------------------------------
procedure OllamaRESTRequestPOST(ARESTRequest: TRESTRequest);
begin
ARESTRequest.Client := TRESTClient.Create(ARESTRequest);
ARESTRequest.Client.SynchronizedEvents := False;
ARESTRequest.Method := TRESTRequestMethod.rmPOST;
ARESTRequest.Response := TRESTResponse.Create(ARESTRequest);
ARESTRequest.Params.Clear;
// 空のアイテムを作っておく
ARESTRequest.Params.AddItem('OllamaJSON', '', TRESTRequestParameterKind.pkREQUESTBODY,
[], TRESTContentType.ctAPPLICATION_JSON);
ARESTRequest.SynchronizedEvents := False;
end;
//------------------------------------------------------------------------------
// OllamaModelLoad
function OllamaLoadTextToJSON(AModel:string; AKeepALive:string): string;
type
TOllama=record
model:string;
keep_alive:string;
end;
var
Ollama:TOllama;
begin
Result:='';
Ollama.model := AModel;
Ollama.keep_alive := AKeepALive; // AKeepALive:-1 無制限
var JsonSerializer := TJsonSerializer.Create;
try
JsonSerializer.Formatting := TJsonFormatting.Indented;
Result := JsonSerializer.Serialize<TOllama>(Ollama);
finally
JsonSerializer.Free;
end;
end;
function OllamaLoadJSONToBool(AContent:string;var isDone:boolean): integer;
type
TOllama=record
done:boolean;
end;
begin
isDone:=False;
Result:=-3;
var JsonSerializer := TJsonSerializer.Create;
try
var JSONed := JsonSerializer.Deserialize<TOllama>(AContent);
isDone:=JSONed.done;
Result:=0;
finally
JsonSerializer.Free;
end;
end;
function OllamaModelLoad(ARESTRequest:TRESTRequest; AAICompParamIn:TAICompParamIn; var isDone:boolean):integer;
var
LastError:integer;
LResponse:string;
begin
LastError := -2;
ARESTRequest.Client.ReadTimeout := AAICompParamIn.Timeout*gTimeOutFirstRate;
ARESTRequest.Client.BaseURL := OllamaURL(AAICompParamIn.Host,'generate');
ARESTRequest.Params[0].Value := OllamaLoadTextToJSON(AAICompParamIn.Model, AAICompParamIn.KeepAlive);
LastError := OllamaRESTExecute(ARESTRequest, LResponse);
if LastError=0 then begin
LastError := OllamaLoadJSONToBool(LResponse,isDone);
end;
Result:=LastError;
end;
//------------------------------------------------------------------------------
// OllamaModelUnLoad
function OllamaUnLoadTextToJSON(AModel:string): string;
type
TOllama=record
model:string;
keep_alive:integer;
end;
var
Ollama:TOllama;
begin
Result:='';
Ollama.model := AModel;
Ollama.keep_alive := 0; // 秒単位
var JsonSerializer := TJsonSerializer.Create;
try
JsonSerializer.Formatting := TJsonFormatting.Indented;
Result := JsonSerializer.Serialize<TOllama>(Ollama);
finally
JsonSerializer.Free;
end;
end;
function OllamaUnLoadJSONToBool(AContent:string;var isDone:boolean): integer;
type
TOllama=record
model:string;
created_at:TDateTime;
response:string;
done:boolean;
done_reason:string;
end;
begin
Result:=-3;
var JsonSerializer := TJsonSerializer.Create;
try
var JSONed := JsonSerializer.Deserialize<TOllama>(AContent);
isDone:=(JSONed.done)and(JSONed.done_reason='unload');
Result:=0;
finally
JsonSerializer.Free;
end;
end;
function OllamaModelUnload(ARESTRequest:TRESTRequest; AAICompParamIn:TAICompParamIn; var isDone:boolean):integer;
var
LastError:integer;
LResponse:string;
begin
LastError:=-1;
ARESTRequest.Client.ReadTimeout := AAICompParamIn.Timeout*gTimeOutFirstRate;
ARESTRequest.Client.BaseURL := OllamaURL(AAICompParamIn.Host,'generate');
ARESTRequest.Params[0].Value := OllamaUnLoadTextToJSON(AAICompParamIn.Model);
LastError := OllamaRESTExecute(ARESTRequest, LResponse);
if LastError=0 then begin
LastError := OllamaUnLoadJSONToBool(LResponse,isDone);
end;
Result:=LastError;
end;
//------------------------------------------------------------------------------
{ TOllamaThreadChat }
function OllamaChatToJSON(AAICompParamIn:TAICompParamIn; AInput:TListString): string;
type
TOllamaMessage=record
role:string;
content:string;
images:array of string;
end;
TOllamaOptions=record
num_ctx:integer;
temperature: single;
num_predict: integer; // テキストを生成するときに予測するトークンの最大数。(デフォルト: -1,無限生成)
top_k: integer; // ナンセンスを生成する確率を減らします。値が大きいほど(例:100),回答が多様化し,値が低いほど(例:10)保守的になります。(デフォルト: 40)
top_p: Single; // top-kと連携します。値が大きいほど(0.95など),テキストの多様性が増し,値を小さくする(0.5など)テキストは,より焦点を絞った保守的なテキストが生成されます。(デフォルト: 0.9)
min_p: Single; // top_pに代わるもので,品質と多様性のバランスを確保することを目指しています。パラメーター p は,最も可能性の高いトークンの確率に対する,トークンが考慮される最小の確率を表します。たとえば,p=0.05 で,最も可能性の高いトークンの確率が 0.9 の場合,値が 0.045 未満のロジットはフィルターで除外されます。(デフォルト: 0.0)
end;
TOllama=record
model:string;
messages:array of TOllamaMessage;
stream:boolean;
options:TOllamaOptions;
end;
var
Ollama:TOllama;
i,j:integer;
s,f,e:string;
begin
Result:='';
Ollama.model:= AAICompParamIn.Model;
if AAICompParamIn.SystemPrompt='' then begin
SetLength(Ollama.Messages,AInput.Count);
j:=-1;
end else begin
SetLength(Ollama.Messages,AInput.Count+1);
Ollama.Messages[0].role :='system';
Ollama.Messages[0].content:=AAICompParamIn.SystemPrompt;
j:=0;
end;
for i:=1 to AInput.Count do begin
if i mod 2=1 then begin
Ollama.Messages[i+j].role :='user';
end else begin
Ollama.Messages[i+j].role :='assistant';
end;
StringSplitAttach(AInput[i-1], s, f, e); // 入力を文字列とファイル名と拡張子情報に分解
Ollama.Messages[i+j].content:=InputEx(AAICompParamIn.Prompt, s);
if (f<>'')and(FileExists(f)) then begin
SetLength(Ollama.Messages[i+j].images,1);
Ollama.Messages[i+j].images[0]:=FileNameToBase64Str(f);
end else begin
SetLength(Ollama.Messages[i+j].images,0);
end;
end;
Ollama.options.num_ctx:= AAICompParamIn.num_ctx;
Ollama.options.temperature := AAICompParamIn.temperature;
Ollama.options.num_predict := AAICompParamIn.num_predict;
Ollama.options.top_k := AAICompParamIn.top_k;
Ollama.options.top_p := AAICompParamIn.top_p;
Ollama.options.min_p := AAICompParamIn.min_p;
Ollama.stream := False;
var JsonSerializer := TJsonSerializer.Create;
try
JsonSerializer.Formatting := TJsonFormatting.Indented;
Result := JsonSerializer.Serialize<TOllama>(Ollama);
finally
JsonSerializer.Free;
end;
end;
function OllamaJSONToChat(AContent:string;var AOutput:string;var AAICompParamOut:TAICompParamOut): integer;
type
TOllamaMessage=record
role:string;
content:string;
end;
TOllama=record
model:string;
created_at:TDateTime;
message:TOllamaMessage;
done:boolean;
prompt_eval_count:integer;
eval_count:integer;
prompt_eval_duration:Int64;
eval_duration:Int64;
end;
begin
Result := -3;
var JsonSerializer := TJsonSerializer.Create;
try
var JSONed := JsonSerializer.Deserialize<TOllama>(AContent);
AOutput := JSONed.message.content;
AAICompParamOut.prompt_tokens := JSONed.prompt_eval_count;
AAICompParamOut.completion_tokens := JSONed.eval_count;
AAICompParamOut.total_tokens := JSONed.prompt_eval_count+JSONed.eval_count;
AAICompParamOut.tick_time := JSONed.prompt_eval_duration+JSONed.eval_duration;
Result := 0;
finally
JsonSerializer.Free;
end;
end;
function OllamaChat(ARESTRequest:TRESTRequest; AAICompParamIn:TAICompParamIn;
AListString: TListString; var AOutPut: string;var AAICompParamOut:TAICompParamOut): integer;
var
LastError:integer;
LResponse:string;
begin
LastError:=-1;
ARESTRequest.Client.ReadTimeout := AAICompParamIn.Timeout;
ARESTRequest.Client.BaseURL := OllamaURL(AAICompParamIn.Host,'chat');
ARESTRequest.Params[0].Value := OllamaChatToJSON(AAICompParamIn, AListString);
LastError := OllamaRESTExecute(ARESTRequest, LResponse);
if LastError=0 then begin
LastError := OllamaJSONToChat(LResponse,AOutput,AAICompParamOut);
if LastError<>0 then begin
AOutput := '';
AAICompParamOut.prompt_tokens := 0;
AAICompParamOut.completion_tokens := 0;
AAICompParamOut.total_tokens := 0;
AAICompParamOut.tick_time := 0;
end;
end;
Result:=LastError;
end;
procedure TOllamaThreadChat.Execute;
var
LRESTRequest: TRESTRequest;
LText:string;
FListString:TListString;
LAICompParamOut:TAICompParamOut;
isDone:boolean;
LastError:integer;
StopWatch: TStopWatch;
begin
FListString:=TListString.Create;
LRESTRequest:=TRESTRequest.Create(nil);
OllamaRESTRequestPOST(LRESTRequest);
LastError:=OllamaModelLoad(LRESTRequest, FAICompParamIn, isDone);
if isDone then begin
while True do begin
if Terminated then begin
LastError:=OllamaModelUnload(LRESTRequest, FAICompParamIn, isDone);
LRESTRequest.Response.Free;
LRESTRequest.Client.Free;
LRESTRequest.Free;
FListString.Free;
// LastErrorを表示する処理が必要か?
if Assigned(FAICompThreadFinish) then begin
Synchronize(
procedure
begin
FAICompThreadFinish;
end
);
end;
exit;
end;
if Assigned(FAICompThreadIn) then begin
Synchronize(
procedure
begin
FAICompThreadIn(FListString);
end
);
end;
if FListString.Count>0 then begin
StopWatch := TStopWatch.StartNew;
LastError := OllamaChat(LRESTRequest, FAICompParamIn, FListString, LText, LAICompParamOut);
LAICompParamOut.tick_time:=StopWatch.ElapsedMilliseconds;
if LastError=0 then begin
if LText='' then begin // 回答が空なら回答できなかった扱いにする
LText := gAICompErrorText+' '+FListString[FListString.Count-1];
end;
if Assigned(FAICompThreadOut) then begin
Synchronize(
procedure
begin
FAICompThreadOut(LText, LAICompParamOut);
end
);
end;
end else begin
LText := gAICompErrorText+' '+FListString[FListString.Count-1];
if Assigned(FAICompThreadOut) then begin
Synchronize(
procedure
begin
FAICompThreadOut(LText, LAICompParamOut);
end
);
end;
if Assigned(FAICompThreadError) then begin
Synchronize(
procedure
begin
FAICompThreadError(OllamaLastErrors[LastError]);
end
);
end;
end;
end;
if FAICompParamIn.Sleep>0 then sleep(FAICompParamIn.Sleep);
end;
end else begin
if Assigned(FAICompThreadError) then begin
Synchronize(
procedure
begin
FAICompThreadError(OllamaLastErrors[LastError]);
end
);
end;
end;
end;
end.
LM_Studioとのチャットのデモ
その1で作成したKP.ListString.pasと,上記のKP.AICompThreads.pasを利用し,簡単なデモプログラムを作ります。
以下のようにその1のForm1に,Edit1:TEdit,Button2:TButton,Combobox1:TCombobox,Button3:TButtonコンポーネントを追加で配置しています。
それから,ComboBox1.StyleをcsDropDownListに設定します。
それぞれ以下の機能を持っています。
Button1: チャットの送信
Edit1: LM Studioのホスト名(プロトコルとポート番号は自動で追加されます)
Button2: ホストにダウンロードされているモデル一覧を取得する
Combobox1: モデル一覧
Button3: チャットの開始と終了
必要なイベントを追加して,以下のようにコードを書きます。
unit Unit1;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes,
System.Types, // TStringDynArray
Vcl.Forms, Vcl.Graphics, Vcl.StdCtrls, Vcl.Controls,
Winapi.WebView2, Winapi.ActiveX, Vcl.Edge,
KP.ListString, KP.AICompThreads;
type
TForm1 = class(TForm)
Button1: TButton;
EdgeBrowser1: TEdgeBrowser;
Edit1: TEdit;
Button2: TButton;
ComboBox1: TComboBox;
Button3: TButton;
Memo2: TMemo;
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
procedure Button1Click(Sender: TObject);
procedure Button2Click(Sender: TObject);
procedure Button3Click(Sender: TObject);
procedure EdgeBrowser1NavigationCompleted(Sender: TCustomEdgeBrowser;
IsSuccess: Boolean; WebErrorStatus: COREWEBVIEW2_WEB_ERROR_STATUS);
private
{ Private 宣言 }
isThreadStart: boolean;
ListStringQ, ListStringA: TListString;
ListStringCount: integer; // 憶えている会話の数
OpenAIThreadChat: TOpenAIThreadChat;
procedure ThreadError(AText:string);
procedure ThreadModelListOut(AAICompModelList: TStringDynArray);
procedure ThreadChatIn(AText:TListString);
procedure ThreadChatOut(AText:String; AAICompParamOut:TAICompParamOut);
procedure ThreadChatFinish;
procedure ListStringToEdgeBrowser1;
public
{ Public 宣言 }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
uses
Vcl.Dialogs; // MessageDlg
procedure TForm1.ThreadError(AText: string);
begin
if AText<>'' then begin
MessageDlg(AText, TMsgDlgType.mtError, [TMsgDlgBtn.mbOK], 0);
end;
end;
procedure TForm1.ThreadModelListOut(AAICompModelList: TStringDynArray);
var
LModel, LItem: string;
i:integer;
begin
LItem:=ComboBox1.Items[ComboBox1.ItemIndex];
ComboBox1.Items.Clear;
for LModel in AAICompModelList do begin
ComboBox1.Items.Add(LModel);
end;
i := ComboBox1.Items.IndexOf(LItem);
if i=-1 then i := 0;
ComboBox1.ItemIndex := i;
Button3.Enabled := Combobox1.Items.Count>0
end;
procedure TForm1.ThreadChatIn(AText: TListString);
begin
ListStringQAMarge(ListStringQ, ListStringA, AText);
if AText.Count>0 then begin
Button1.Enabled := False;
end;
end;
procedure TForm1.ThreadChatOut(AText: String; AAICompParamOut: TAICompParamOut);
begin
if (ListStringA.Count=ListStringCount) then begin
ListStringQ.Delete(0);
ListStringA.Delete(0);
end;
ListStringA.Add(AText);
// StatusDisp(AAICompParamOut); // StatusBarにTokenの情報を出力
ListStringToEdgeBrowser1;
Button1.Enabled := True;
end;
procedure TForm1.ThreadChatFinish;
begin
// Terminateをした後の処理
Button1.Enabled := False;
Button2.Enabled := True;
Button3.Enabled := True;
Button3.Caption := 'Start';
Edit1.Enabled := True;
ComboBox1.Enabled := True;
isThreadStart := False;
end;
procedure TForm1.ListStringToEdgeBrowser1;
var
s: string;
begin
s:=ChatQAToHTML(ListStringQ, ListStringA);
if not EdgeBrowser1.WebViewCreated then
begin
EdgeBrowser1.CreateWebView;
while not EdgeBrowser1.WebViewCreated do begin
Application.ProcessMessages;
Sleep(100);
end;
end;
EdgeBrowser1.NavigateToString(s);
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
Memo2.Lines.TrailingLineBreak := False;
ListStringQ := TListString.Create;
ListStringA := TListString.Create;
isThreadStart := False;
OpenAIThreadChat := nil;
ListStringCount := 100; // 憶えている会話の数
Edit1.Text := 'localhost';
Button1.Enabled := False;
Button2.Enabled := True;
Button3.Enabled := False;
Button3.Caption := 'Start';
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
if Assigned(OpenAIThreadChat) then begin
OpenAIThreadChat.Terminate;
while isThreadStart do begin
Application.ProcessMessages;
end;
end;
ListStringQ.Free;
ListStringA.Free;
end;
procedure TForm1.EdgeBrowser1NavigationCompleted(Sender: TCustomEdgeBrowser;
IsSuccess: Boolean; WebErrorStatus: COREWEBVIEW2_WEB_ERROR_STATUS);
begin
// 一番下までスクロールする
EdgeBrowser1.ExecuteScript('window.scroll(0, Number.MAX_SAFE_INTEGER);');
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
if Memo2.Lines.Text='' then exit;
if not isThreadStart then begin
Button3Click(Self);
end;
ListStringQ.Add(Memo2.Lines.Text);
// Memo2.Lines.Text := '';
// edtAttach1.Text := '';
ListStringToEdgeBrowser1;
end;
procedure TForm1.Button2Click(Sender: TObject);
var
LHost: string;
begin
LHost:= PrivateHostNameMod(Edit1.Text, TAICompType.LM_Studio);
if LHost='' then exit;
Button2.Enabled := False;
Edit1.Enabled := False;
TOpenAIThreadModelList.Create(LHost, ThreadModelListOut, ThreadError);
end;
procedure TForm1.Button3Click(Sender: TObject);
var
LHost: string;
LAICompParamIn: TAICompParamIn;
begin
LHost:= PrivateHostNameMod(Edit1.Text, TAICompType.LM_Studio);
if LHost='' then exit;
if Combobox1.Text='' then exit;
if isThreadStart then begin
OpenAIThreadChat.Terminate;
end else begin
LAICompParamIn.Init;
with LAICompParamIn do begin
AICompType := TAICompType.LM_Studio;
Host := LHost;
Model := ComboBox1.Text;
SystemPrompt := '';
Prompt := '';
Timeout := 30*1000; // タイムアウトは30秒
num_ctx := 4098; // コンテキスト長 LM_Studioでは使わない
end;
OpenAIThreadChat := TOpenAIThreadChat.Create(LAICompParamIn, ThreadChatIn,
ThreadChatOut, ThreadChatFinish, ThreadError);
isThreadStart := True;
Button1.Enabled := True;
Button2.Enabled := False;
Button3.Caption := 'Stop';
Edit1.Enabled := False;
ComboBox1.Enabled := False;
end;
end;
end.
Edit1にLocalhostや192.168.1.20などのLM Studioを動かしているPCのIPアドレスを入力し,Button2をクリックするとComboBox1にモデルの一覧が表示されます。
利用したいモデルを選んで,Button3をクリックして,Memo2に質問を書いてButton1をクリックすると,入力した質問がEdgeBrowser1に表示され,LM Studio上で動いているモデルからの回答が表示されます。
改良のヒント
タイムアウトの調整
回答のタイムアウトはButton3Clickイベント内で 30*1000 つまり 30秒が指定されていますが,それでもLM Studioからの回答がタイムアウトするなら,Button3Clickイベントで指定している,Timeoutを調整してください。
コンテキスト長の調整
コンテキスト長の調整はLM Studioのモデルの設定で行います。PCのメモリがひっ迫するなら,調整が必要です。
統計情報の表示
ThreadChatOutメソッドは生成AIからの出力結果を反映するときに呼ばれるイベントです。
コメントアウトされているStatusDisp(AAICompParamOut);は,レポートされた統計情報を表示するための仮の関数です。
KP.AICompThreads.pasで定義されていて,以下のような統計情報が返されるので,ステータスバーなどに表示するのもよいかもしれません。
トークン数はコンテキスト長と比較できる数なので,コンテキスト長と合計のトークン数の差が小さくなるようにしたほうが良いようです。
TAICompParamOut = record
prompt_tokens :integer; // 入力トークン数
completion_tokens:integer; // 出力のために使ったトークン数
total_tokens :integer; // 合計のトークン数
tick_time :Int64; // かかった時間
end;
謝辞
この記事やコードを書くにあたり,以下の記事やサイトを参考にしました。この場を借りて感謝します。
- MacでLM Studioを使うためのメモ
https://autumn-color.com/blog/2025/07/2025-07-11/ - 【Delphi】REST クライアントライブラリを使う @ht_deko(Hideaki Tominaga)
https://qiita.com/ht_deko/items/eb3f987c50012109e781 - [Delphi] JSON の処理は Serializer を使うのがナウい @pik(Jun HOSOKAWA)
https://qiita.com/pik/items/4e601c5db404d5a88dec


