4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

DelphiでローカルLLMを利用したアプリを開発 その3 Ollamaとチャット

Last updated at Posted at 2025-12-04

本投稿は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:TEditButton2:TButtonCombobox1:TComboboxButton3:TButtonコンポーネントを追加で配置しています。
それから,ComboBox1.StylecsDropDownListに設定します。

2025-12-04-02.png

それぞれ以下の機能を持っています。
Button1: チャットの送信
Edit1: Ollamaのホスト名(プロトコルとポート番号は自動で追加されます)
Button2: ホストにダウンロードされているモデル一覧を取得する
Combobox1: モデル一覧
Button3: チャットの開始と終了

必要なイベントを追加して以下のようにコードを書きます。

Unit1.pas
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上で動いているモデルからの回答が表示されます。

2025-12-05-01.png

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);は,レポートされた統計情報を表示するための仮の関数です。
TAICompParamOutKP.AICompThreads.pasで定義されていて,以下のような統計情報が返されるので,ステータスバーなどに表示するのもよいかもしれません。
トークン数はコンテキスト長と比較できる数なので,コンテキスト長と合計のトークン数の差が小さくなるようにしたほうが良いようです。

  TAICompParamOut = record
    prompt_tokens    :integer; // 入力トークン数
    completion_tokens:integer; // 出力のために使ったトークン数
    total_tokens     :integer; // 合計のトークン数
    tick_time        :Int64;   // かかった時間
  end;

謝辞

この記事やコードを書くにあたり,以下の記事やサイトを参考にしました。この場を借りて感謝します。

4
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?