5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

FlutterでLFM2.5-1.2B-Thinkingを動かしてみる

5
Posted at

Liquid AIからモデルサイズが1GBを切るテキスト専用モデル LFM2.5-1.2B-Thinking が発表されました。
この記事ではFlutterからそのモデルを動かしてみたいと思います。

どうやってLLMを動かすか

LiquidAIが開発したLFM(Liquid Foundation Models)シリーズのモデルには、モバイル端末上で実行するためにLeap SDKがあり、このSDKのFlutter用パッケージを使用します。

このSDKはモデルのダウンロード・キャッシュ・ロード・推論をすべてFlutterから扱えるようになっており、サーバーなしでLLMを動かしたい場面で重宝します。

何か作ってみる

今回はAndroid端末で動作するシンプルなチャットアプリを作ってみます。
以下の点は今回触っていません。

  • 複数ターンの会話履歴の永続化
  • モデルダウンロードの進捗表示
  • エラーハンドリングの作り込み

実行結果

今回作ったアプリの実行画面です。システムプロンプトによって応答内容がけっこう異なったため、2つの画像を掲載します。

システムプロンプト1 システムプロンプト2
あなたは犬が好きで、同様に犬が好きだと推測できるユーザーには優しい応答を返してください。猫が好きなことがわかっているユーザーには「知っているけど教えない、もったいぶる、別の人に聞くように言う」などそっけない応答をしてください。また、回答の語尾を「ワン」で統一するため、句点の前に「ワン」をつけてください You are helpful assistant. Answer the question concisely in Japanese.

質問の仕方でも、応答の品質が少し変わって見えます。

LLMを動かすまでの手順

liquid_ai_leap を追加する

fvm flutter pub add liquid_ai_leap
pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.8
  liquid_ai_leap: ^0.3.2

build.gradle.ktを修正する

android/ap/build.gradle.kt
    defaultConfig {
         :
        minSdk = 31
         :
    }

モデルのロード

今回は ChatService クラスにLLM周りの処理をまとめました。
initializeメソッドで準備が全て終わります。 やることは「モデルがキャッシュされているか確認 → なければダウンロード → ロード → 会話セッション作成」の4ステップです。

lib/chat_service.dart
import 'package:flutter/foundation.dart';
import 'package:liquid_ai_leap/liquid_ai_leap.dart';

class ChatService {
  ChatService();

  static const _model = 'LFM2.5-1.2B-Thinking';
  static const _quantization = 'Q4_0';

  // 2026/5/29時点でurlを指定しないとエラーになる
  static const _modelUrl =
      'https://huggingface.co/LiquidAI/LFM2.5-1.2B-Thinking-GGUF/resolve/main/LFM2.5-1.2B-Thinking-Q4_0.gguf?download=true';

  static const _systemPrompt =
      'あなたは犬が好きで、同様に犬が好きだと推測できるユーザーには優しい応答を返してください。'
      '猫が好きなことがわかっているユーザーには「知っているけど教えない、もったいぶる、別の人に聞くように言う」'
      'などそっけない応答をしてください。'
      'また、回答の語尾を「ワン」で統一するため、句点の前に「ワン」をつけてください';

  /// Leap SDKのインスタンス
  final leap = LiquidAiLeap();

  /// モデルのランナー
  late final ModelRunner _runner;

  /// 会話の状態を管理するオブジェクト。コンテキストを保持しているみたい。
  /// チャットを切り替える場合、このインスタンスを増やすのかも。
  late final Conversation _conversation;

  /// 初期化処理
  Future<void> initialize() async {
    /// ダウンロード済みかチェック
    final isCached = await leap.isModelCached(
      model: _model,
      quantization: _quantization,
    );

    if (!isCached) {
      /// 所定の場所からモデルをダウンロードしてキャッシュする
      await leap.downloadModel(
        model: _model,
        quantization: _quantization,
        url: _modelUrl,
      );
      debugPrint("Model downloaded and cached successfully.");
    }

    /// モデルのロード
    _runner = await leap.loadModel(model: _model, quantization: _quantization);
    _conversation = await _runner.createConversation(
      systemPrompt: _systemPrompt,
    );
  }

  /// メッセージ送信。応答をStreamで返す。
  Stream<String> sendMessage(String message) async* {
    final msg = ChatMessage.user(message);
    final stream = _conversation.generateResponse(message: msg);

    yield* stream.map((res) => res is ChunkResponse ? res.text : '');
  }
}

isModelCached で初回起動かどうかを判定し、キャッシュがなければ downloadModel でHugging Faceからダウンロードします。

この部分は他に以下の選択肢があります。

  • アプリに同梱しておく
    • アプリサイズが増える。ダウンロードする必要がない
    • 更新が必要になった際、アプリ更新が必要になる
  • モデルデータを自分でホストする
    • HuggingFaceに影響を受けない

ストリーミングで応答を返す

sendMessageStream<String> を返すようにしています。
LLMの応答を1トークンずつ受け取り、UIへ逐次反映できます。

lib/chat_service.dart
Stream<String> sendMessage(String message) async* {
  final msg = ChatMessage.user(message);
  final stream = _conversation.generateResponse(message: msg);

  yield* stream.map((res) => res is ChunkResponse ? res.text : '');
}

UIを実装する

ここまででLLMで推論し、ユーザーのチャット内容に応答するトークンを取得できるようになっています。
あとは入力と表示部分を作れば、今回の目的は達成です。

UIでストリーミング表示する

チャット画面では await for でストリームを受け取り、setState でトークンを都度描画しています。

lib/main.dart
Future<void> _sendMessage(String text) async {
  final validMessage = text.trim();
  if (validMessage.isEmpty) return;

  setState(() => _responding = true);

  _messages.add(_MessageEntry(validMessage, isUser: true));
  _messages.add(_MessageEntry('', isUser: false));  // プレースホルダー

  final buffer = StringBuffer();

  await for (final res in service.sendMessage(validMessage)) {
    buffer.write(res);
    setState(() {
      final index = _messages.length - 1;
      _messages[index] = _MessageEntry(buffer.toString(), isUser: false);
    });
  }

  setState(() => _responding = false);
}

応答中はリストの末尾にプレースホルダーを置いておき、トークンが届くたびに末尾エントリを更新しています。
StringBuffer に追記していくことで、全文を再構築せずに済みます。

この先にできること

MCP・RAG
モバイルデバイスでLLMを動かす意義はこの辺りにあると思っています。Leap SDKでどうやるのか(できるのか)調べていないので、課題として残っています。

ソースコード

ソースコードはこちらのリポジトリにあります。

まとめ

Leap SDKがシンプルで使いやすい
APIの設計がシンプルで、LLM周りの複雑さをうまく隠蔽してくれています。必要最低限の手順で推論する準備が完了します。

実行速度は課題あり。端末依存かも
Pixel 7aで実行すると、ちょっとした質問でも応答を得るまで1分以上かかります。GPU/TPUを使っているかは不明です。最近のAndroid端末やiOSではもう少し応答が早いのかもしれません。
また、モバイルゲームに組み込めたら良いなと思っていましたが、思っていた以上に時間がかかることから、用途は厳選したほうが良さそうです。

「ガソリン価格が上下する要因は何ですか?」の実行時間
ie_llamacpp: llama_perf_context_print:        load time =    1857.39 ms
ie_llamacpp: llama_perf_context_print: prompt eval time =    1765.97 ms /   119 tokens (   14.84 ms per token,    67.39 tokens per second)
ie_llamacpp: llama_perf_context_print:        eval time =  152739.49 ms /  2137 runs   (   71.47 ms per token,    13.99 tokens per second)
ie_llamacpp: llama_perf_context_print:       total time =  155343.37 ms /  2256 tokens
ie_llamacpp: llama_perf_context_print:    graphs reused =       2128
ie_core::generate: 
 	Prompt Tokens: 119    Generated Tokens: 2137
 	Model Load Time:		  1.39 (seconds)
 	Total inference time:		155.34 (seconds)		 Rate: 	 13.76 (tokens/second)
 		Prompt evaluation:	  1.86 (seconds)		 Rate: 	 64.07 (tokens/second)
 		Generated 2137 tokens:	153.49 (seconds)		 Rate: 	 13.92 (tokens/second)
 	Time to first generated token:	  1.86 (seconds)

品質
800MB程度のモデルだと考えると、想像以上に良い品質だと思えます。システムプロンプトに対してそこそこ敏感な印象があるので、このあたりの調整が必要になるかもしれないです。

最後に

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

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

5
1
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
5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?