Flutter から Gemini Nano を動かす - Prompt API で自然文検索をタグに翻訳する
はじめに
Flutter から Gemini Nano を動かす - ML Kit GenAI API の6機能を試してみたでは、Android のオンデバイス生成 AI モデル Gemini Nano を Flutter から呼び出し、ML Kit GenAI API の6機能を一通り試しました。
今回はその中の Prompt API を深掘りします。
単なる API の使い方紹介ではなく、「Prompt API はどこで活きるのか」を、自然文検索のデモ実装を通して具体的に示します。
伝えたいこと
- Prompt API は6機能で唯一、自由にプロンプトを書ける API。テキストも画像も渡せる
- 呼び出し課金ゼロ。「高頻度 × 軽処理」の AI 機能が現実になる
- 小型モデルなので、タスクの絞り方が品質を決める
Prompt API の使いどころ
唯一のカスタムプロンプト API
他の5機能(要約・校正・書き換え・画像の説明・音声認識)は用途が固定された専用 API で、すぐ使える反面、アプリ固有の処理には使えません。
「うちのアプリのこの部分に AI を挟みたい」というとき、受け皿になるのが Prompt API です。
入力はテキストに加えて画像も渡せます。
呼び出し課金がない
クラウドの生成 AI API のコストは「1回あたりの課金 × 呼び出し回数」で決まるため、「軽い処理だけど全ユーザーが高頻度に呼ぶ」機能は採算が合わず、見送られがちです。
検索のたびに AI を1回挟む機能を、クラウド最安クラスのモデル(Gemini 2.5 Flash-Lite、入力 $0.10 / 出力 $0.40 per 1M トークン)で試算してみます(1回 = 入力500 / 出力100トークン、$1 = 150円と仮定)。
| 規模 | 月間呼び出し | クラウド月額 | オンデバイス |
|---|---|---|---|
| 1万ユーザー × 10検索/日 | 300万回 | 約$270 ≒ 4万円 | 0円 |
| 10万ユーザー × 10検索/日 | 3,000万回 | 約$2,700 ≒ 40万円 | 0円 |
1回あたり約0.014円でも、積み重なると軽くない金額になります。
オンデバイスの Gemini Nano なら、何回呼んでも追加コストはかかりません。
呼び出し課金がないこと自体は ML Kit GenAI API に共通する利点ですが、専用 API の用途に収まらない処理へこの利点を持ち込めるのは Prompt API だけです。
クラウドでは採算が合わなかった「高頻度 × 軽処理」を、アプリ固有の形で差し込む。ここが Prompt API の使いどころだと考えています。
作ったもの: 自然文検索 → タグ翻訳
デモの題材は小売アプリの商品検索です。
「春に着られる安いジャケット」のような自然文を、Gemini Nano がタグ列に翻訳し、既存のタグ検索に OR で渡します。
検索そのものは既存の仕組みのままです。
入力: 「春に着られる安いジャケット」
↓ Gemini Nano(Prompt API)
タグ: ["ジャケット", "アウター", "春", "プチプラ", "カジュアル"]
↓ 既存のタグ検索に OR で投げる
結果: 「ジャケット」の単語一致では当たらなかった商品もヒット

画像を添付して同じタグ翻訳を通すこともできます。
「写真から似た商品を探す」のライト版です。

設計のポイントは2つあります。
1. 出力は JSON ではなくカンマ区切りのタグ列にする
小型モデルに JSON スキーマを厳密に守らせるのは不安定で、崩れた出力はパースエラーに直結します。
カンマ区切りの文字列なら split で取り出せて、多少の揺れも後段の正規化で吸収できます。
2. モデルに検索はさせず、言い換えだけさせる
商品知識や検索ロジックは既存のタグ検索に任せ、Gemini Nano には「安い」→「プチプラ」のようなキーワードの言い換え・拡張だけを頼みます。
これは書き換え系のタスクに近く、小型モデルでも安定しやすい領域です。
実装
Prompt API は現時点では Android SDK(Kotlin/Java)向けのみの提供で、公式の Flutter プラグインはありません。
そこで今回は「Pigeon でブリッジを定義 → Kotlin 側で Prompt API を呼ぶ → Dart 側でタグ翻訳に使う」の3ステップで実装しました。
ここでは Prompt API 固有の要点に絞って示します(Pigeon の導入・登録などの定型部分は省略します)。
Step 1: Pigeon でブリッジを定義する
Pigeon で API を定義してコード生成すると、Dart 側の呼び出しクラスと Kotlin 側のインターフェースが生成されます。
// pigeons/gemini_nano.dart
class PromptRequest {
PromptRequest({required this.prompt, this.imageBytes});
final String prompt;
final Uint8List? imageBytes; // マルチモーダル入力用
}
@HostApi()
abstract class GeminiNanoHostApi {
@async
String generatePrompt(PromptRequest request);
}
Step 2: Kotlin 側で Prompt API を呼ぶ
依存に com.google.mlkit:genai-prompt(執筆時点 1.0.0-beta2)を追加し、生成されたインターフェースを実装します。
ポイントは「状態確認 → 必要ならモデルダウンロード → 推論」の順で呼ぶことです。
val generativeModel = GenerativeModelFutures.from(Generation.getClient())
// 状態確認 → モデル未DLなら推論前にダウンロードする
val status = generativeModel.checkStatus().await()
if (status == FeatureStatus.UNAVAILABLE) {
throw IllegalStateException("この端末は Gemini Nano に対応していません")
}
if (status != FeatureStatus.AVAILABLE) {
generativeModel.download(downloadCallback).await() // 進捗コールバックは省略
}
// 推論。画像が添付されていれば ImagePart を足すだけでマルチモーダル入力になる
val contentRequest =
imageBytes?.let { bytes ->
val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
generateContentRequest(ImagePart(bitmap), TextPart(prompt)) {}
} ?: generateContentRequest(TextPart(prompt)) {}
val response = generativeModel.generateContent(contentRequest).await()
val text = response.candidates.firstOrNull()?.text ?: ""
Step 3: Dart 側でタグ翻訳に使う
タグ翻訳の実装は「プロンプト」と「出力の正規化」の2つだけです。
プロンプトは出力形式の絞り込みがポイントです。
タグのみ・カンマ区切り・上限件数を明示します(画像入力時は英語で応答しがちなため、日本語出力も明示します)。
String _buildTagPrompt(String query, {required bool hasImage}) {
if (hasImage) {
return '添付した画像に写っている商品を検索するための日本語タグを5個まで、'
'カンマ区切りで出力してください。\n'
'タグのみを出力し、説明文・記号・前置きは不要です。日本語で出力してください。\n';
}
return '次の検索文から、商品検索に使う日本語タグを5個まで、カンマ区切りで出力してください。\n'
'タグのみを出力し、説明文・記号・前置きは不要です。\n'
'検索文: $query';
}
それでも小型モデルの出力は揺れる(改行区切り・箇条書きで返ってくる等)ため、正規化関数で吸収します。
/// Prompt API の生テキスト出力を検索タグ列に正規化する。
List<String> normalizeSearchTags(String rawOutput, {int maxTags = 5}) {
final tags = rawOutput
.split(RegExp(r'[,、\n]'))
.map((tag) => tag.trim())
.map((tag) => tag.replaceFirst(RegExp(r'^[-・*\d.\s]+'), '').trim())
.where((tag) => tag.isNotEmpty)
.toList();
final seen = <String>{};
return tags.where(seen.add).take(maxTags).toList();
}
検索処理の本体では、生成したタグで商品カタログを検索します。
商品とのマッチングは、生成タグの表記揺れを緩く拾えるよう双方向の部分一致(t.contains(tag) || tag.contains(t))にしています。
Future<void> searchByTags(String query, {Uint8List? imageBytes}) async {
final raw = await hostApi.generatePrompt(
PromptRequest(
prompt: _buildTagPrompt(query, hasImage: imageBytes != null),
imageBytes: imageBytes,
),
);
final tags = normalizeSearchTags(raw);
final products = catalog.where((p) => p.matchesAnyTag(tags)).toList();
// ...結果を状態に反映(エラーハンドリングは省略)
}
Prompt API 固有の実装はこれで全部です。
プロンプト1本と正規化関数だけで、既存の検索機能に AI を挟めました。

実機で確認した注意点
対応デバイスの制約やフォアグラウンド限定の推論など、ML Kit GenAI API に共通する注意点は前回記事のとおりです。
ここでは今回の検証で実感した点を補足します。
- 初回のモデルダウンロードが重い: 1GB超あるため、実運用ではダウンロード進捗の表示が必要です(モデルは6機能で共有されるため、これも API 共通の話です)
- プロンプトの指示が完全には守られない: 出力形式や言語の指定が無視されることがあり、プロンプトでの明示と正規化処理(Step 3)で吸収しました
まとめ
現時点の Prompt API は、クラウドの生成 AI と賢さで競うものではない、というのが個人的な見立てです。
コンテキストウィンドウは小さめで、複雑な推論は不得意に見えます。
それでも、呼び出しコストを気にせずアプリ固有の軽いタスクを差し込めるのは、クラウド前提では得られなかった自由だと感じています。
フリマアプリの出品写真からのタグ自動入力、食事記録アプリでの料理写真からのメニュー名入力補助など、同じパターンで広げられる場所はまだまだありそうです。
賢さが要る処理はクラウドに任せて、高頻度の軽処理はオンデバイスへ。
GA と対応端末の拡大が進めば、「全ユーザーに無料で配れる軽い AI 処理」は当たり前の選択肢になっていくはずです。
次はどんな処理を差し込むか、考えるのが楽しみです。









