6
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Dart + Ollamaでチャットアプリを作ってみる

6
Posted at

Dart言語でデスクトップ向けのAIチャットアプリを作ってみたいと思います。

実装する機能

今回は以下のような要件で実装します。

  • TUIアプリケーションにする
  • オンデバイスLLMでチャットできるようにする
  • 画像生成できるようにする
  • マークダウン記法の一部をサポートする

実行画面

まず、結果のスクリーンショットを記載しておきます。
画面下部に入力フィールドがあり、その上はスクロール可能なメッセージ表示欄になっています。

ollama_chat.avif

チャットで入力した内容

  • 「ローマ帝国の勃興と滅亡の歴史を、すごく簡単に教えてください」
  • 「もしアウグストゥスがゴールデンリバーだったら、という設定で画像を作ってください」

生成された画像
ollama-2026-07-09T15-44-41.179139.png

主な使用パッケージ

TUIにはnocterm、Ollamaの利用にはollama_dartパッケージを使用します。

nocterm

Flutterに近い感覚でTUIを構築するためのフレームワークです。以前Qiitaに記事を書いているので、興味があれば見てみてください。

Noctermを使ってみる

ollama_dart

Ollamaは、LLMをローカル環境(オンデバイス)で手軽に動かすためのツールです。
dart用にはollama_dart パッケージを使います。ollamaというパッケージもありますが、ollama_dartのほうが新しく機能も多いようです。

ソースコードについて

今回のソースコードはGitHubにホストしてあります。

ファイルは以下のようになっています。

パス 役割
lib/main.dart エントリポイント(引数解析→UI起動)
lib/src/cli_options.dart コマンドライン引数の解析
lib/src/chat_app.dart noctermによるチャットUI
lib/src/chat_service.dart Ollamaとのチャット送受信(ストリーミング・ツール呼び出しの集約)
lib/src/generate_image_tool.dart 画像生成ツール(生成と保存)
lib/src/markdown_util.dart Markdownをターミナル表示用のRichTextに変換
lib/src/system_prompt.dart LLMに与えるシステムプロンプト

使用するモデルやOllamaのホストはコマンドラインオプションで変更できます。

# デフォルト(gemma4:latest / x/z-image-turbo:latest)で起動
dart run lib/main.dart

# モデルを指定して起動
dart run lib/main.dart --model gemma3:latest --image-model x/flux.1:latest

実装のポイント

テキストモデルと画像生成モデルを分けて使う

チャットできてツール対応していて画像生成もできるモデルというのはあまり多くありません。ですので、今回はテキスト用と画像生成用の2つのLLMを使うようにしました。

用途 モデル名(デフォルト) 備考
テキスト用 gemma4:latest tool対応モデルであること
画像生成用 x/z-image-turbo:latest imageモデル

テキスト用のモデルは、画像生成をツール呼び出し(function calling)で依頼するため、tool対応のモデルである必要があります。

チャットから画像生成までの流れは以下のようになっています。

toolの処理

画像生成は、LLMに以下のようなツール定義を渡しておき、LLMがツール呼び出しを返してきたら画像生成モデルを起動する、という流れで実現しています。

static const definition = ToolDefinition(
  type: ToolType.function,
  function: ToolFunction(
    name: 'generate image',
    description: 'tool for generating image',
    parameters: {
      'type': 'object',
      'properties': {
        'prompt': {
          'type': 'string',
          'description': 'prompt for generating image',
        },
      },
      'required': ['prompt'],
    },
  ),
);

ストリーミング中はテキストの差分とツール呼び出しの両方が届く可能性があるので、ChatServiceでツール呼び出しを集約しておき、応答の完了後にまとめて実行しています。

await for (final chunk in stream) {
  final message = chunk.message;
  if (message == null) continue;

  if (message.toolCalls?.isNotEmpty ?? false) {
    toolCalls.addAll(message.toolCalls!);
  } else if (message.content != null) {
    buffer.write(message.content);
    onDelta(message.content!);
  }
}

なお、Ollamaの仕様では、同じレスポンスでチャットとツール呼び出しのデータが返ることがあるようです。
しかし、今回の実装では、通常のチャットとツール呼び出しとを排他的に処理しています(ツール呼び出しがあれば最終メッセージはツールの結果だけになります)。
排他的でなく同時に処理することはそれほど難しくありません。

マークダウンの表示

LLMの応答はマークダウンで返ってくることが多いので、そのまま表示すると **# などの記号だらけになってしまいます。
今回は簡易的なパーサーを実装して、noctermのRichTextTextSpanのツリー)に変換して表示するようにしました。

サポートしているのは以下の記法です。

  • ヘッダー(# 見出し)→ シアンの太字
  • リスト(- item / 1. item)→ 記号を に置き換え
  • ボールド(**text**)→ 太字
  • リンク([label](url))→ ラベルに下線+URLを併記
  • インラインコード(`code`)→ 色付き表示

行単位でヘッダー・リストを判定し、行の中は正規表現でボールド・リンク・インラインコードを拾う、という素朴な実装ですが、チャット表示用途にはこれで十分でした。

まとめ

nocterm + ollama_dartの組み合わせで、ストリーミング表示・画像生成・マークダウン表示まで含めたAIチャットアプリを、少ないコード量で実装できました。

さまざまなチャットクライアントが既にあるので自作する動機はあまりないかもしれませんが、

  • ローカル完結なのでプライバシーの心配がない
  • ツール呼び出しで独自の機能(今回は画像生成)を自由に組み込める
  • Flutterの知識がそのまま活かせる

といった点で、Dartでの自作も選択肢になると思います。

最後に

株式会社ボトルキューブではFlutterを使ったお仕事を募集中です。
お問い合わせフォームからご連絡ください。

また、一緒に働く仲間も募集しています。
詳細は採用情報ページをご覧ください。

6
2
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
6
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?