Liquid AIからモデルサイズが1GBを切るテキスト専用モデル LFM2.5-1.2B-Thinking が発表されました。
この記事ではFlutterからそのモデルを動かしてみたいと思います。
どうやってLLMを動かすか
LiquidAIが開発したLFM(Liquid Foundation Models)シリーズのモデルには、モバイル端末上で実行するためにLeap SDKがあり、このSDKのFlutter用パッケージを使用します。
このSDKはモデルのダウンロード・キャッシュ・ロード・推論をすべてFlutterから扱えるようになっており、サーバーなしでLLMを動かしたい場面で重宝します。
何か作ってみる
今回はAndroid端末で動作するシンプルなチャットアプリを作ってみます。
以下の点は今回触っていません。
- 複数ターンの会話履歴の永続化
- モデルダウンロードの進捗表示
- エラーハンドリングの作り込み
実行結果
今回作ったアプリの実行画面です。システムプロンプトによって応答内容がけっこう異なったため、2つの画像を掲載します。
質問の仕方でも、応答の品質が少し変わって見えます。
LLMを動かすまでの手順
liquid_ai_leap を追加する
fvm flutter pub add liquid_ai_leap
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.8
liquid_ai_leap: ^0.3.2
build.gradle.ktを修正する
defaultConfig {
:
minSdk = 31
:
}
モデルのロード
今回は ChatService クラスにLLM周りの処理をまとめました。
initializeメソッドで準備が全て終わります。 やることは「モデルがキャッシュされているか確認 → なければダウンロード → ロード → 会話セッション作成」の4ステップです。
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に影響を受けない
ストリーミングで応答を返す
sendMessage は Stream<String> を返すようにしています。
LLMの応答を1トークンずつ受け取り、UIへ逐次反映できます。
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 でトークンを都度描画しています。
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を使ったお仕事を募集中です。
お問い合わせフォームからご連絡ください。
また、一緒に働く仲間も募集しています。
詳細は採用情報ページをご覧ください。

