本投稿はDelphi AdventCalender 2025 #05の記事です。
の記事は,Delphi13でコードを書き,Windows11で動作を確認しています。
はじめに
Delphi13を使ってOllama上のLLMとチャットをするアプリを開発します。
なお,この記事は,
の続きになります。
これらで利用しているコードが前提になっているので,この記事を読む前にぜひこちらをご覧ください。
Ollamaの設定
Ollamaをインストール
OllamaをWindowsにインストール
https://ollama.com/ より,Ollamaをダウンロードしてインストールします。
インストール後に実行するとタスクトレイにOllamaのアイコンが表示されますので,右クリックして表示されるポップアップメニューから Setting... を選択し,Expose Ollama to the network をチェックすると,別のPCからOllamaにアクセスできるようになります。
なお,Ollama accountは設定しなくてもローカルでの利用は可能です。
OllamaをmacOSにインストール
HomeBrewを使ってインストールするのが簡単です。
brew ollama
インストールされたOllama実行して,メニューバーのOllamaを選択し,ドロップダウンメニューから Setting... を選択し,Expose Ollama to the network をチェックすると,別のPCからOllamaにアクセスできるようになります。
Ollamaのコマンド
Ollamaの操作は主にコマンドプロンプトからします。主なコマンドを紹介します
ollama run モデル名 // モデルのダウンロード及び実行
ollama stop モデル名 // 実行されているモデルの停止
ollama list // ダウンロードされたモデルの一覧
ollama ps // 現在実行されているモデルの一覧
ollama rm // モデルの削除
ollama -h // ヘルプの表示
ollama -v // バージョン表示
ollama [command] --help // コマンドの詳しい説明
モデルのダウンロードと動作の確認
https://ollama.com/ の Search models 検索窓からモデルを検索します。
gemma3を検索し,SizeとContextを見て自分のPCで利用できそうなモデルを選択すると,そのモデルをダウンロードするコマンドが表示されます。
例えば,gemma3:12bなら以下のコマンドが表示されコピーできます。
ollama run gemma3:12b
このコマンドをコマンドプロンプトから入力するとダウンロードされ実行できます。以下のプロンプトが表示されたら,日本語で構わないので簡単な質問をして,動作を確認してください。
>>>
動作が確認できたら終了します。
>>> /bye
Ollamaとのチャットのデモ
その1で作成したKP.ListString.pasと,その2で作成したKP.AICompThreads.pasを利用し,簡単なデモプログラムを作ります。
※ KP.AICompThreadsは2025/12/06に更新されています。
その2と同じようにコンポーネントを配置します。
その1から編集する場合は,以下のようにEdit1:TEdit,Button2:TButton,Combobox1:TCombobox,Button3:TButtonコンポーネントを追加で配置しています。
それから,ComboBox1.StyleをcsDropDownListに設定します。
それぞれ以下の機能を持っています。
Button1: チャットの送信
Edit1: Ollamaのホスト名(プロトコルとポート番号は自動で追加されます)
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)
EdgeBrowser1: TEdgeBrowser;
Edit1: TEdit;
Button1: TButton;
Button2: TButton;
Button3: TButton;
ComboBox1: TComboBox;
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; // 憶えている会話の数
OllamaThreadChat: TOllamaThreadChat;
procedure ThreadError(AText:string);
procedure ThreadModelListOut(AAICompModelList: TStringDynArray);
procedure ThreadChatIn(AText:TListString);
procedure ThreadChatOut(AText:String; AOllamaParamOut: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; AOllamaParamOut: TAICompParamOut);
begin
if (ListStringA.Count=ListStringCount) then begin
ListStringQ.Delete(0);
ListStringA.Delete(0);
end;
ListStringA.Add(AText);
// StatusDisp(AOllamaParamOut); // 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
// https://mam-mam.net/delphi/tedgebrowser.html
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;
OllamaThreadChat := 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(OllamaThreadChat) then begin
OllamaThreadChat.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.Ollama);
if LHost='' then exit;
Button2.Enabled := False;
Edit1.Enabled := False;
TOllamaThreadModelList.Create(LHost, ThreadModelListOut, ThreadError);
end;
procedure TForm1.Button3Click(Sender: TObject);
var
LHost: string;
LAICompParamIn: TAICompParamIn;
begin
LHost:= PrivateHostNameMod(Edit1.Text, TAICompType.Ollama);
if LHost='' then exit;
if Combobox1.Text='' then exit;
if isThreadStart then begin
OllamaThreadChat.Terminate;
end else begin
LAICompParamIn.Init;
with LAICompParamIn do begin
AICompType := TAICompType.Ollama;
Host := LHost;
Model := ComboBox1.Text;
SystemPrompt := ''; // システムプロンプト
Prompt := ''; // チャットごとに追加されるプロンプト
Timeout := 30*1000; // タイムアウトは30秒
num_ctx := 4098; // コンテキスト長
end;
OllamaThreadChat := TOllamaThreadChat.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などのOllamaを動かしているPCのIPアドレスを入力し,Button2をクリックするとComboBox1にモデルの一覧が表示されます。
利用したいモデルを選んで,Button3をクリックして,Memo2に質問を書いてButton1をクリックすると,入力した質問がEdgeBrowser1に表示され,Ollama上で動いているモデルからの回答が表示されます。
Ollamaだけの機能
LM Studioと違う点は2つあり,1つ目はTAICompParamInで利用できる以下のパラメータです。
num_ctx: integer; // コンテキスト長
num_predict: integer; // 無限生成
top_k: integer; // ナンセンスを生成する確率を減らすもの(デフォルト: 40)
min_p: Single; // top_pに代わるもの(デフォルト: 0.0)
KeepAlive: string; // ロードされた状態を維持する時間
2つ目は,Ollamaのchatは,開始時にモデルをロードし,終了時にモデルをクローズします。
改良のヒント
タイムアウトの調整
回答のタイムアウトはButton3Clickイベント内で 30*1000 つまり 30秒 が指定されていますが,それでもOllamaからの回答がタイムアウトするなら,Button3Clickイベントで指定している,Timeoutを調整してください。
コンテキスト長の調整
コンテキスト長の調整はButton3Clickイベント内のnum_ctxで行います。PCのメモリがひっ迫するなら,調整が必要です。
統計情報の表示
ThreadChatOutメソッドは生成AIからの出力結果を反映するときに呼ばれるイベントです。
コメントアウトされているStatusDisp(AOllamaParamOut);は,レポートされた統計情報を表示するための仮の関数です。
TAICompParamOutはKP.AICompThreads.pasで定義されていて,以下のような統計情報が返されるので,ステータスバーなどに表示するのもよいかもしれません。
トークン数はコンテキスト長と比較できる数なので,コンテキスト長と合計のトークン数の差が小さくなるようにしたほうが良いようです。
TAICompParamOut = record
prompt_tokens :integer; // 入力トークン数
completion_tokens:integer; // 出力のために使ったトークン数
total_tokens :integer; // 合計のトークン数
tick_time :Int64; // かかった時間
end;
謝辞
この記事やコードを書くにあたり,以下の記事やサイトを参考にしました。この場を借りて感謝します。
- 【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

