Gemini を活用した Flutter アプリを作成する

Gemini を活用した Flutter アプリを作成する

この Codelab について

subject最終更新: 4月 7, 2025
account_circle作成者: Brett Morgan

1. Gemini を活用した Flutter アプリを作成する

作成するアプリの概要

この Codelab では、Gemini API の機能を Flutter アプリに直接組み込むインタラクティブな Flutter アプリ Colorist を作成します。ユーザーが自然言語でアプリを操作できるようにしたいと思っても、どこから始めればよいかわからない場合は、この Codelab を参考にしてください。この Codelab では、その方法について説明します。

Colorist では、ユーザーが自然言語(「夕焼けのオレンジ」や「深い海の青」など)で色を記述すると、アプリが次のように動作します。

  • Google の Gemini API を使用してこれらの説明を処理します。
  • 説明を解釈して正確な RGB カラー値に変換します。
  • 色をリアルタイムで画面に表示
  • 色に関する技術的な詳細情報と興味深いコンテキストを提供します
  • 最近生成された色の履歴を保持する

色の表示とチャット インターフェースを示す Colorist アプリのスクリーンショット

このアプリは、片側にカラー ディスプレイ領域とインタラクティブなチャット システムを備えた分割画面インターフェースと、もう一方に LLM の未加工のインタラクションが表示される詳細なログパネルを備えています。このログを使用すると、LLM 統合の実際の仕組みを把握できます。

Flutter デベロッパーにとっての重要性

LLM はユーザーがアプリを操作する方法に革命をもたらしていますが、モバイルアプリとパソコンアプリに LLM を効果的に統合するには、独自の課題があります。この Codelab では、単なる API 呼び出しを超える実用的なパターンについて説明します。

パートナー様の学習プロセス

この Codelab では、Colorist の作成プロセスを順を追って説明します。

  1. プロジェクトの設定 - 基本的な Flutter アプリ構造と colorist_ui パッケージから始めます。
  2. Gemini の基本的な統合 - アプリを Firebase の Vertex AI に接続し、シンプルな LLM 通信を実装する
  3. 効果的なプロンプト - LLM が色の説明を理解できるようにガイドするシステム プロンプトを作成する
  4. 関数宣言 - LLM がアプリで色を設定する際に使用できるツールを定義します。
  5. ツールの処理 - LLM からの関数呼び出しを処理し、アプリの状態に接続します。
  6. レスポンスのストリーミング - リアルタイム ストリーミング LLM レスポンスでユーザー エクスペリエンスを向上
  7. LLM コンテキストの同期 - ユーザー操作を LLM に通知して、統一されたエクスペリエンスを作成する

学習内容

  • Flutter アプリケーション用に Firebase で Vertex AI を構成する
  • LLM の動作をガイドする効果的なシステム プロンプトを作成する
  • 自然言語とアプリ機能をつなぐ関数宣言を実装する
  • ストリーミング レスポンスを処理してレスポンシブなユーザー エクスペリエンスを実現する
  • UI イベントと LLM 間で状態を同期する
  • Riverpod を使用して LLM の会話状態を管理する
  • LLM を活用したアプリケーションでエラーを適切に処理する

コードのプレビュー: 実装内容の概要

LLM がアプリで色を設定できるように作成する関数宣言の概要を次に示します。

FunctionDeclaration get setColorFuncDecl => FunctionDeclaration(
  'set_color',
  'Set the color of the display square based on red, green, and blue values.',
  parameters: {
    'red': Schema.number(description: 'Red component value (0.0 - 1.0)'),
    'green': Schema.number(description: 'Green component value (0.0 - 1.0)'),
    'blue': Schema.number(description: 'Blue component value (0.0 - 1.0)'),
  },
);

前提条件

この Codelab を最大限に活用するには、次の準備が必要です。

  • Flutter 開発の経験 - Flutter の基本と Dart 構文に精通している
  • 非同期プログラミングの知識 - Future、async/await、ストリームの理解
  • Firebase アカウント - Firebase を設定するには Google アカウントが必要です。
  • 課金が有効になっている Firebase プロジェクト - Firebase の Vertex AI には請求先アカウントが必要です

LLM を活用した最初の Flutter アプリの作成を始めましょう。

2. プロジェクトの設定とエコーサービス

この最初のステップでは、プロジェクト構造を設定し、シンプルなエコー サービスを実装します。このサービスは、後で Gemini API 統合に置き換えられます。これにより、アプリケーション アーキテクチャが確立され、LLM 呼び出しの複雑さを追加する前に UI が正しく機能していることを確認できます。

このステップで学習する内容

  • 必要な依存関係を使用して Flutter プロジェクトを設定する
  • UI コンポーネントの colorist_ui パッケージの操作
  • エコー メッセージ サービスを実装して UI に接続する

料金に関する重要な注意事項

新しい Flutter プロジェクトを作成する

まず、次のコマンドを使用して新しい Flutter プロジェクトを作成します。

flutter create -e colorist --platforms=android,ios,macos,web,windows

-e フラグは、デフォルトの counter アプリのない空のプロジェクトを作成することを示します。このアプリは、デスクトップ、モバイル、ウェブで動作するように設計されています。ただし、flutterfire は現在 Linux をサポートしていません。

依存関係を追加する

プロジェクト ディレクトリに移動し、必要な依存関係を追加します。

cd colorist
flutter pub add colorist_ui flutter_riverpod riverpod_annotation
flutter pub add
--dev build_runner riverpod_generator riverpod_lint json_serializable custom_lint

これにより、次の主要なパッケージが追加されます。

  • colorist_ui: Colorist アプリの UI コンポーネントを提供するカスタム パッケージ
  • flutter_riverpodriverpod_annotation: 状態管理用
  • logging: 構造化ロギングの場合
  • コード生成とリンティングの開発依存関係

pubspec.yaml は次のようになります。

pubspec.yaml

name: colorist
description: "A new Flutter project."
publish_to: 'none'
version: 0.1.0

environment:
  sdk: ^3.7.2

dependencies:
  flutter:
    sdk: flutter
  colorist_ui: ^0.1.0
  flutter_riverpod: ^2.6.1
  riverpod_annotation: ^2.6.1

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^5.0.0
  build_runner: ^2.4.15
  riverpod_generator: ^2.6.5
  riverpod_lint: ^2.6.5
  json_serializable: ^6.9.4
  custom_lint: ^0.7.5

flutter:
  uses-material-design: true

分析オプションを構成する

プロジェクトのルートにある analysis_options.yaml ファイルに custom_lint を追加します。

include: package:flutter_lints/flutter.yaml

analyzer:
  plugins:
    - custom_lint

この構成により、Riverpod 固有の lint が有効になり、コード品質の維持に役立ちます。

main.dart ファイルを実装する

lib/main.dart のコンテンツを次のように置き換えます。

lib/main.dart

import 'package:colorist_ui/colorist_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

void main() async {
 
runApp(ProviderScope(child: MainApp()));
}

class MainApp extends ConsumerWidget {
 
const MainApp({super.key});

 
@override
 
Widget build(BuildContext context, WidgetRef ref) {
   
return MaterialApp(
     
theme: ThemeData(
       
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
     
),
     
home: MainScreen(
       
sendMessage: (message) {
         
sendMessage(message, ref);
       
},
     
),
   
);
 
}

 
// A fake LLM that just echoes back what it receives.
 
void sendMessage(String message, WidgetRef ref) {
   
final chatStateNotifier = ref.read(chatStateNotifierProvider.notifier);
   
final logStateNotifier = ref.read(logStateNotifierProvider.notifier);

   
chatStateNotifier.addUserMessage(message);
   
logStateNotifier.logUserText(message);
   
chatStateNotifier.addLlmMessage(message, MessageState.complete);
   
logStateNotifier.logLlmText(message);
 
}
}

これにより、ユーザーのメッセージを返すだけで LLM の動作を模倣するシンプルなエコー サービスを実装する Flutter アプリが設定されます。

アーキテクチャについて

colorist アプリのアーキテクチャについて説明します。

colorist_ui パッケージ

colorist_ui パッケージには、ビルド済みの UI コンポーネントと状態管理ツールが用意されています。

  1. MainScreen: 以下を表示するメインの UI コンポーネント。
    • パソコンでの分割画面レイアウト(操作領域とログパネル)
    • モバイルのタブ形式のインターフェース
    • カラー表示、チャット インターフェース、履歴のサムネイル
  2. 状態管理: アプリは、次の複数の状態通知ツールを使用します。
    • ChatStateNotifier: チャット メッセージを管理します。
    • ColorStateNotifier: 現在の色と履歴を管理します。
    • LogStateNotifier: デバッグ用のログエントリを管理します。
  3. メッセージ処理: アプリは、次の状態を持つメッセージ モデルを使用します。
    • ユーザー メッセージ: ユーザーが入力
    • LLM メッセージ: LLM(または現時点ではエコーサービス)によって生成されます。
    • MessageState: LLM メッセージが完了したか、まだストリーミング中かをトラッキングします。

アプリケーション アーキテクチャ

このアプリのアーキテクチャは次のとおりです。

  1. UI レイヤ: colorist_ui パッケージによって提供されます。
  2. 状態管理: リアクティブな状態管理に Riverpod を使用
  3. サービスレイヤ: 現在はシンプルなエコー サービスが含まれていますが、これは Gemini Chat サービスに置き換えられます。
  4. LLM 統合: 後続のステップで追加

この分離により、UI コンポーネントはすでに処理されているため、LLM 統合の実装に集中できます。

アプリを実行する

次のコマンドでアプリを実行します。

flutter run -d DEVICE

DEVICE は、対象デバイス(macoswindowschrome、デバイス ID など)に置き換えます。

Markdown をレンダリングするエコー サービスが示されている Colorist アプリのスクリーンショット

次のような Colorist アプリが表示されます。

  1. デフォルトの色のカラー表示領域
  2. メッセージを入力できるチャット インターフェース
  3. チャット インタラクションを示すログパネル

「濃い青色が希望です」などのメッセージを入力して送信してみてください。エコー サービスは、メッセージを繰り返すだけです。後続のステップでは、Firebase の Vertex AI を介して Gemini API を使用して、実際の色の解釈に置き換えます。

次のステップ

次のステップでは、Firebase を構成し、基本的な Gemini API インテグレーションを実装して、エコーサービスを Gemini チャット サービスに置き換えます。これにより、アプリは色の説明を解釈し、インテリジェントなレスポンスを提供できるようになります。

トラブルシューティング

UI パッケージに関する問題

colorist_ui パッケージで問題が発生した場合:

  • 最新バージョンを使用していることを確認する
  • 依存関係が正しく追加されていることを確認する
  • 競合するパッケージ バージョンがないか確認する

ビルドエラー

ビルドエラーが表示された場合は、次のようにします。

  • 最新の安定版チャンネルの Flutter SDK がインストールされていることを確認してください
  • flutter clean の後に flutter pub get を実行します。
  • コンソール出力で特定のエラー メッセージを確認する

学んだ主なコンセプト

  • 必要な依存関係を使用して Flutter プロジェクトを設定する
  • アプリケーションのアーキテクチャとコンポーネントの責任を理解する
  • LLM の動作を模倣するシンプルなサービスを実装する
  • サービスを UI コンポーネントに接続する
  • 状態管理に Riverpod を使用する

3. Gemini Chat の基本的な統合

このステップでは、前のステップの echo サービスを、Firebase で Vertex AI を使用して Gemini API を統合したものに置き換えます。Firebase を構成し、必要なプロバイダを設定して、Gemini API と通信する基本的なチャット サービスを実装します。

このステップで学習する内容

  • Flutter アプリケーションで Firebase を設定する
  • Vertex AI in Firebase を構成して Gemini にアクセスする
  • Firebase サービスと Gemini サービスの Riverpod プロバイダを作成する
  • Gemini API を使用して基本的なチャット サービスを実装する
  • 非同期 API レスポンスとエラー状態の処理

Firebase を設定する

まず、Flutter プロジェクト用に Firebase を設定する必要があります。これには、Firebase プロジェクトの作成、アプリの追加、必要な Vertex AI の設定が含まれます。

Firebase プロジェクトを作成する

  1. Firebase コンソールに移動し、Google アカウントでログインします。
  2. [Firebase プロジェクトを作成] をクリックするか、既存のプロジェクトを選択します。
  3. セットアップ ウィザードに沿ってプロジェクトを作成します。
  4. プロジェクトを作成したら、Vertex AI サービスにアクセスするには Blaze プラン(従量制)にアップグレードする必要があります。Firebase コンソールの左下にある [アップグレード] ボタンをクリックします。

Firebase プロジェクトで Vertex AI を設定する

  1. Firebase コンソールで、プロジェクトに移動します。
  2. 左側のサイドバーで [AI] を選択します。
  3. Vertex AI in Firebase カードで、[使ってみる] を選択します。
  4. 画面の指示に沿って、プロジェクトで Vertex AI in Firebase API を有効にします。

FlutterFire CLI をインストールする

FlutterFire CLI を使用すると、Flutter アプリでの Firebase のセットアップが簡素化されます。

dart pub global activate flutterfire_cli

Flutter アプリに Firebase を追加する

  1. Firebase Core パッケージと Vertex AI パッケージをプロジェクトに追加します。
flutter pub add firebase_core firebase_vertexai
  1. FlutterFire 構成コマンドを実行します。
flutterfire configure

このコマンドは次のことを行います。

  • 作成した Firebase プロジェクトを選択するよう求められる
  • Flutter アプリを Firebase に登録する
  • プロジェクトの構成を含む firebase_options.dart ファイルを生成する

このコマンドを実行すると、選択したプラットフォーム(iOS、Android、macOS、Windows、ウェブ)が自動的に検出され、適切に構成されます。

プラットフォーム固有の構成

Firebase では、Flutter のデフォルトよりも高い最小バージョンが必要です。また、Firebase サーバーの Vertex AI と通信するために、ネットワーク アクセスも必要です。

macOS の権限を構成する

macOS の場合、アプリの利用資格でネットワーク アクセスを有効にする必要があります。

  1. macos/Runner/DebugProfile.entitlements を開き、次のコードを追加します。

macos/Runner/DebugProfile.entitlements

<key>com.apple.security.network.client</key>
<true/>
  1. また、macos/Runner/Release.entitlements を開いて、同じエントリを追加します。
  2. macos/Podfile の上部にある macOS の最小バージョンを更新します。

macos/Podfile

# Firebase requires at least macOS 10.15
platform
:osx, '10.15'

iOS の権限を構成する

iOS の場合、ios/Podfile の上部にある最小バージョンを更新します。

ios/Podfile

# Firebase requires at least iOS 13.0
platform
:ios, '13.0'

Android の設定を構成する

Android の場合は、android/app/build.gradle.kts を更新します。

android/app/build.gradle.kts

android {
   
// ...
    ndkVersion
= "27.0.12077973"

    defaultConfig
{
       
// ...
        minSdk
= 23
       
// ...
   
}
}

Gemini モデル プロバイダを作成する

次に、Firebase と Gemini の Riverpod プロバイダを作成します。新しいファイル lib/providers/gemini.dart を作成します。

lib/providers/gemini.dart

import 'dart:async';

import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_vertexai/firebase_vertexai.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

import '../firebase_options.dart';

part 'gemini.g.dart';

@riverpod
Future<FirebaseApp> firebaseApp(Ref ref) =>
   
Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);

@riverpod
Future<GenerativeModel> geminiModel(Ref ref) async {
 
await ref.watch(firebaseAppProvider.future);

 
final model = FirebaseVertexAI.instance.generativeModel(
   
model: 'gemini-2.0-flash',
 
);
 
return model;
}

@Riverpod(keepAlive: true)
Future<ChatSession> chatSession(Ref ref) async {
 
final model = await ref.watch(geminiModelProvider.future);
 
return model.startChat();
}

このファイルには、3 つの主要プロバイダの基盤が定義されています。これらのプロバイダは、Riverpod コード生成ツールによって dart run build_runner を実行するときに生成されます。

  1. firebaseAppProvider: プロジェクト構成を使用して Firebase を初期化します。
  2. geminiModelProvider: Gemini 生成モデル インスタンスを作成します。
  3. chatSessionProvider: Gemini モデルとのチャット セッションを作成して維持します。

チャット セッションの keepAlive: true アノテーションにより、アプリのライフサイクル全体でセッションが保持され、会話のコンテキストが維持されます。

Gemini Chat サービスを実装する

新しいファイル lib/services/gemini_chat_service.dart を作成して、チャット サービスを実装します。

lib/services/gemini_chat_service.dart

import 'dart:async';

import 'package:colorist_ui/colorist_ui.dart';
import 'package:firebase_vertexai/firebase_vertexai.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

import '../providers/gemini.dart';

part 'gemini_chat_service.g.dart';

class GeminiChatService {
 
GeminiChatService(this.ref);
 
final Ref ref;

 
Future<void> sendMessage(String message) async {
   
final chatSession = await ref.read(chatSessionProvider.future);
   
final chatStateNotifier = ref.read(chatStateNotifierProvider.notifier);
   
final logStateNotifier = ref.read(logStateNotifierProvider.notifier);

   
chatStateNotifier.addUserMessage(message);
   
logStateNotifier.logUserText(message);
   
final llmMessage = chatStateNotifier.createLlmMessage();
   
try {
     
final response = await chatSession.sendMessage(Content.text(message));

     
final responseText = response.text;
     
if (responseText != null) {
       
logStateNotifier.logLlmText(responseText);
       
chatStateNotifier.appendToMessage(llmMessage.id, responseText);
     
}
   
} catch (e, st) {
     
logStateNotifier.logError(e, st: st);
     
chatStateNotifier.appendToMessage(
       
llmMessage.id,
       
"\nI'm sorry, I encountered an error processing your request. "
       
"Please try again.",
     
);
   
} finally {
     
chatStateNotifier.finalizeMessage(llmMessage.id);
   
}
 
}
}

@riverpod
GeminiChatService geminiChatService(Ref ref) => GeminiChatService(ref);

このサービスは、

  1. ユーザー メッセージを受信して Gemini API に送信します。
  2. モデルからのレスポンスでチャット インターフェースを更新する
  3. すべての通信をログに記録し、実際の LLM フローを簡単に把握できるようにします。
  4. 適切なユーザー フィードバックを使用してエラーを処理する

注: この時点では、ログ ウィンドウはチャット ウィンドウとほぼ同じになります。関数呼び出しとレスポンスのストリーミングを導入すると、ログはさらに興味深いものになります。

Riverpod コードを生成する

ビルドランナー コマンドを実行して、必要な Riverpod コードを生成します。

dart run build_runner build --delete-conflicting-outputs

これにより、Riverpod の機能に必要な .g.dart ファイルが作成されます。

main.dart ファイルを更新する

新しい Gemini Chat サービスを使用するように lib/main.dart ファイルを更新します。

lib/main.dart

import 'package:colorist_ui/colorist_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import 'providers/gemini.dart';
import 'services/gemini_chat_service.dart';

void main() async {
 
runApp(ProviderScope(child: MainApp()));
}

class MainApp extends ConsumerWidget {
 
const MainApp({super.key});

 
@override
 
Widget build(BuildContext context, WidgetRef ref) {
   
final model = ref.watch(geminiModelProvider);

   
return MaterialApp(
     
theme: ThemeData(
       
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
     
),
     
home: model.when(
       
data:
           
(data) => MainScreen(
             
sendMessage: (text) {
               
ref.read(geminiChatServiceProvider).sendMessage(text);
             
},
           
),
       
loading: () => LoadingScreen(message: 'Initializing Gemini Model'),
       
error: (err, st) => ErrorScreen(error: err),
     
),
   
);
 
}
}

今回の更新の主な変更点は次のとおりです。

  1. エコーサービスを Gemini API ベースのチャット サービスに置き換える
  2. when メソッドで Riverpod の AsyncValue パターンを使用して読み込み画面とエラー画面を追加する
  3. sendMessage コールバックを介して UI を新しいチャット サービスに接続する

アプリを実行する

次のコマンドでアプリを実行します。

flutter run -d DEVICE

DEVICE は、対象デバイス(macoswindowschrome、デバイス ID など)に置き換えます。

明るい黄色の色のリクエストに Gemini LLM が応答している様子を示す Colorist アプリのスクリーンショット

これで、メッセージを入力すると Gemini API に送信され、エコーではなく LLM からレスポンスが返されるようになります。ログパネルには、API とのやり取りが表示されます。

LLM 通信について

Gemini API と通信する際の流れを説明します。

通信フロー

  1. ユーザー入力: ユーザーがチャット インターフェースにテキストを入力する
  2. リクエストのフォーマット: アプリは、Gemini API の Content オブジェクトとしてテキストをフォーマットします。
  3. API 通信: テキストは Firebase の Vertex AI を介して Gemini API に送信されます。
  4. LLM 処理: Gemini モデルがテキストを処理し、レスポンスを生成します。
  5. レスポンスの処理: アプリがレスポンスを受信し、UI を更新します。
  6. ロギング: 透明性を確保するために、すべての通信がロギングされます。

チャット セッションと会話のコンテキスト

Gemini チャット セッションは、メッセージ間のコンテキストを保持し、会話型のインタラクションを可能にします。つまり、LLM は現在のセッションでの以前のやり取りを「記憶」し、より一貫性のある会話を可能にします。

チャット セッション プロバイダに keepAlive: true アノテーションを追加すると、このコンテキストがアプリのライフサイクル全体で維持されます。この永続的なコンテキストは、LLM との自然な会話フローを維持するために重要です。

次のステップ

この時点では、Gemini API に何でも質問できます。回答内容に制限はありません。たとえば、バラ戦争の概要を尋ねることができますが、これはカラーアプリの目的とは関係ありません。

次のステップでは、Gemini が色の説明をより効果的に解釈できるようにガイドするシステム プロンプトを作成します。ここでは、アプリ固有のニーズに合わせて LLM の動作をカスタマイズし、アプリのドメインにその機能を集中させる方法について説明します。

トラブルシューティング

Firebase の構成に関する問題

Firebase の初期化でエラーが発生した場合:

  • firebase_options.dart ファイルが正しく生成されていることを確認する
  • Vertex AI アクセス用の Blaze プランにアップグレードしていることを確認する

API アクセス エラー

Gemini API へのアクセスでエラーが発生した場合:

  • Firebase プロジェクトで課金が正しく設定されていることを確認する
  • Firebase プロジェクトで Vertex AI と Cloud AI API が有効になっていることを確認する
  • ネットワーク接続とファイアウォールの設定を確認する
  • モデル名(gemini-2.0-flash)が正しく、使用可能であることを確認する

会話のコンテキストに関する問題

Gemini がチャットの以前のコンテキストを記憶していない場合は、次の手順を行います。

  • chatSession 関数に @Riverpod(keepAlive: true) がアノテーションされていることを確認する
  • すべてのメッセージ交換で同じチャット セッションを再利用していることを確認する
  • メッセージを送信する前に、チャット セッションが適切に初期化されていることを確認する

プラットフォーム固有の問題

プラットフォーム固有の問題の場合:

  • iOS/macOS: 適切な利用資格が設定され、最小バージョンが構成されていることを確認する
  • Android: 最小 SDK バージョンが正しく設定されていることを確認する
  • コンソールでプラットフォーム固有のエラー メッセージを確認する

学んだ主なコンセプト

  • Flutter アプリケーションで Firebase を設定する
  • Gemini へのアクセス用に Vertex AI in Firebase を構成する
  • 非同期サービスの Riverpod プロバイダを作成する
  • LLM と通信するチャット サービスの実装
  • 非同期 API の状態(読み込み、エラー、データ)の処理
  • LLM の通信フローおよびチャット セッションについて

4. 色の説明に効果的なプロンプト

このステップでは、Gemini が色の説明を解釈できるようにガイドするシステム プロンプトを作成して実装します。システム プロンプトは、コードを変更せずに特定のタスクの LLM 動作をカスタマイズするための強力な方法です。

このステップで学習する内容

  • システム プロンプトと LLM アプリケーションにおけるその重要性
  • ドメイン固有のタスクに効果的なプロンプトを作成する
  • Flutter アプリでシステム プロンプトを読み込んで使用する
  • 一貫した形式の回答を提供するように LLM を誘導する
  • システム プロンプトが LLM の動作に与える影響のテスト

システム プロンプトについて

実装に入る前に、システム プロンプトとは何で、なぜ重要なのかを理解しましょう。

システム プロンプトとは

システム プロンプトは、LLM に与えられる特別なタイプの指示で、コンテキスト、動作ガイドライン、回答の期待値を設定します。ユーザー メッセージとは異なり、システム プロンプトは次のようになります。

  • LLM の役割とペルソナを設定する
  • 専門的な知識や能力を定義する
  • フォーマット手順を提供する
  • レスポンスに制約を設定する
  • さまざまなシナリオを処理する方法について説明する

システム プロンプトは、LLM に「職務内容」を与えるものと考えてください。会話全体でモデルがどのように振る舞うかを指定します。

システム プロンプトが重要な理由

システム プロンプトは、一貫性があり有用な LLM インタラクションの作成に不可欠です。その理由は次のとおりです。

  1. 一貫性を保つ: 一貫した形式で回答を提供するようにモデルをガイドします。
  2. 関連性を高める: 特定のドメイン(この場合は色)にモデルをフォーカスする
  3. 境界を確立する: モデルがすべきこととすべきでないこと
  4. ユーザー エクスペリエンスを向上させる: より自然で有用なインタラクション パターンを作成
  5. 後処理を減らす: 解析や表示が容易な形式でレスポンスを取得する

Colorist アプリでは、LLM を使用して色の説明を一貫して解釈し、特定の形式で RGB 値を提供する必要があります。

システム プロンプト アセットを作成する

まず、実行時に読み込まれるシステム プロンプト ファイルを作成します。この方法では、アプリを再コンパイルせずにプロンプトを変更できます。

次の内容の新しいファイルを assets/system_prompt.md という名前で作成します。

assets/system_prompt.md

# Colorist System Prompt

You are a color expert assistant integrated into a desktop app called Colorist. Your job is to interpret natural language color descriptions and provide the appropriate RGB values that best represent that description.

## Your Capabilities

You are knowledgeable about colors, color theory, and how to translate natural language descriptions into specific RGB values. When users describe a color, you should:

1. Analyze their description to understand the color they are trying to convey
2. Determine the appropriate RGB values (values should be between 0.0 and 1.0)
3. Respond with a conversational explanation and explicitly state the RGB values

## How to Respond to User Inputs

When users describe a color:

1. First, acknowledge their color description with a brief, friendly response
2. Interpret what RGB values would best represent that color description
3. Always include the RGB values clearly in your response, formatted as: `RGB: (red=X.X, green=X.X, blue=X.X)`
4. Provide a brief explanation of your interpretation

Example:
User: "I want a sunset orange"
You: "Sunset orange is a warm, vibrant color that captures the golden-red hues of the setting sun. It combines a strong red component with moderate orange tones.

RGB: (red=1.0, green=0.5, blue=0.25)

I've selected values with high red, moderate green, and low blue to capture that beautiful sunset glow. This creates a warm orange with a slightly reddish tint, reminiscent of the sun low on the horizon."


## When Descriptions are Unclear

If a color description is ambiguous or unclear, please ask the user clarifying questions, one at a time.

## Important Guidelines

- Always keep RGB values between 0.0 and 1.0
- Always format RGB values as: `RGB: (red=X.X, green=X.X, blue=X.X)` for easy parsing
- Provide thoughtful, knowledgeable responses about colors
- When possible, include color psychology, associations, or interesting facts about colors
- Be conversational and engaging in your responses
- Focus on being helpful and accurate with your color interpretations

システム プロンプトの構造を理解する

このプロンプトの内容を詳しく見てみましょう。

  1. ロールの定義: LLM を「カラー エキスパート アシスタント」として設定します。
  2. タスクの説明: 色の説明を RGB 値に変換することをメインタスクとして定義します。
  3. レスポンスの形式: 一貫性を保つために RGB 値の形式を正確に指定します。
  4. やり取りの例: 想定されるインタラクション パターンの具体的な例を示す
  5. エッジケースの処理: 不明確な説明に対処する方法について説明します。
  6. 制約とガイドライン: RGB 値を 0.0 ~ 1.0 に保つなどの境界を設定します。

この構造化されたアプローチにより、LLM のレスポンスは一貫性があり、有益で、RGB 値をプログラムで抽出する場合に解析しやすい形式になります。

pubspec.yaml を更新する

次に、pubspec.yaml の末尾を更新して、assets ディレクトリを追加します。

pubspec.yaml

flutter:
  uses-material-design: true

  assets:
    - assets/

flutter pub get を実行してアセット バンドルを更新します。

システム プロンプト プロバイダを作成する

システム プロンプトを読み込む新しいファイル lib/providers/system_prompt.dart を作成します。

lib/providers/system_prompt.dart

import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'system_prompt.g.dart';

@riverpod
Future<String> systemPrompt(Ref ref) =>
   
rootBundle.loadString('assets/system_prompt.md');

このプロバイダは、Flutter のアセット読み込みシステムを使用して、実行時にプロンプト ファイルを読み取ります。

Gemini モデル プロバイダを更新する

システム プロンプトを含めるように lib/providers/gemini.dart ファイルを変更します。

lib/providers/gemini.dart

import 'dart:async';

import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_vertexai/firebase_vertexai.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

import '../firebase_options.dart';
import 'system_prompt.dart';                                          // Add this import

part 'gemini.g.dart';

@riverpod
Future<FirebaseApp> firebaseApp(Ref ref) =>
   
Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);

@riverpod
Future<GenerativeModel> geminiModel(Ref ref) async {
 
await ref.watch(firebaseAppProvider.future);
 
final systemPrompt = await ref.watch(systemPromptProvider.future);  // Add this line

 
final model = FirebaseVertexAI.instance.generativeModel(
   
model: 'gemini-2.0-flash',
   
systemInstruction: Content.system(systemPrompt),                  // And this line
 
);
 
return model;
}

@Riverpod(keepAlive: true)
Future<ChatSession> chatSession(Ref ref) async {
 
final model = await ref.watch(geminiModelProvider.future);
 
return model.startChat();
}

主な変更点は、生成モデルの作成時に systemInstruction: Content.system(systemPrompt) を追加することです。これにより、このチャット セッションのすべてのインタラクションのシステム プロンプトとして指示が使用されます。

Riverpod コードを生成する

ビルドランナー コマンドを実行して、必要な Riverpod コードを生成します。

dart run build_runner build --delete-conflicting-outputs

アプリケーションを実行してテストする

続いてアプリケーションを実行します。

flutter run -d DEVICE

カラー選択アプリの Gemini LLM が文字列で回答している様子を示したカラーリスト アプリのスクリーンショット

さまざまな色の説明でテストしてみてください。

  • 「スカイブルーをお願いします」
  • 「フォレスト グリーンを」
  • 「夕日のような鮮やかなオレンジ色を作成する」
  • 「I want the color of fresh lavender」
  • 「深い海のような青色を教えて」

Gemini が、色に関する会話形式の説明と、一貫した形式の RGB 値を返すようになりました。システム プロンプトにより、必要なタイプの回答を提供するように LLM が効果的に誘導されています。

色のコンテキスト外のコンテンツについても質問してみてください。例えば、薔薇戦争の主な原因などです。前の手順との違いを確認してください。

特殊なタスクのプロンプト エンジニアリングの重要性

システム プロンプトは、芸術と科学の両面があります。LLM の統合において重要な部分であり、特定のアプリケーションでのモデルの有用性に大きく影響する可能性があります。ここで行ったことは、プロンプト エンジニアリングの一形態です。つまり、アプリケーションのニーズに合わせてモデルの動作を調整するために、指示を調整しました。

効果的なプロンプト設計には、次のものが必要です。

  1. 明確なロール定義: LLM の目的を明確にする
  2. 明示的な指示: LLM がどのように回答すべきかを詳細に指定する
  3. 具体的な例: 良い回答の例を示す
  4. エッジケースの処理: 曖昧なシナリオに対処する方法を LLM に指示する
  5. 形式設定の仕様: 回答が一貫した使いやすい方法で構成されていることを確認する

作成したシステム プロンプトにより、Gemini の汎用機能が、アプリケーションのニーズに合わせてフォーマットされた回答を提供する、特殊な色解釈アシスタントに変換されます。これは、さまざまなドメインやタスクに適用できる強力なパターンです。

次のステップ

次のステップでは、関数宣言を追加してこの基盤を構築します。これにより、LLM は RGB 値を提案するだけでなく、アプリの関数を実際に呼び出して色を直接設定できるようになります。これは、LLM が自然言語と具体的なアプリケーション機能のギャップを埋める方法を示しています。

トラブルシューティング

アセットの読み込みに関する問題

システム プロンプトの読み込み中にエラーが発生した場合:

  • pubspec.yaml がアセット ディレクトリを正しく一覧表示していることを確認する
  • rootBundle.loadString() のパスとファイルの場所が一致していることを確認します。
  • flutter clean の後に flutter pub get を実行してアセット バンドルを更新する

一貫性のない対応

LLM がフォーマット手順に常に従っていない場合:

  • システム プロンプトでフォーマットの要件を明確にします
  • 期待されるパターンを示す例を追加する
  • リクエストする形式がモデルにとって妥当であることを確認する

API レート制限

レート制限に関連するエラーが発生した場合:

  • Vertex AI サービスには使用上限があることに注意してください。
  • 指数バックオフによる再試行ロジックの実装を検討する(読者の演習として残す)
  • Firebase コンソールで割り当てに関する問題を確認する

学んだ主なコンセプト

  • LLM アプリケーションにおけるシステム プロンプトの役割と重要性
  • 明確な指示、例、制約を使用して効果的なプロンプトを作成する
  • Flutter アプリでシステム プロンプトを読み込んで使用する
  • ドメイン固有のタスクの LLM 動作のガイド
  • プロンプト エンジニアリングを使用して LLM の回答を形作る

このステップでは、システム プロンプトで明確な指示を提供するだけで、コードを変更せずに LLM の動作を大幅にカスタマイズする方法を示します。

5. LLM ツールの関数宣言

このステップでは、関数宣言を実装して、Gemini がアプリでアクションを実行できるようにします。この強力な機能により、LLM は RGB 値を提案するだけでなく、専用のツール呼び出しを介してアプリの UI に RGB 値を実際に設定できます。ただし、Flutter アプリで実行された LLM リクエストを確認するには、次のステップが必要です。

このステップで学習する内容

  • LLM 関数呼び出しと Flutter アプリケーションでのメリット
  • Gemini のスキーマベースの関数宣言を定義する
  • 関数宣言を Gemini モデルと統合する
  • ツールの機能を利用するためのシステム プロンプトの更新

関数呼び出しについて

関数宣言を実装する前に、関数宣言とその重要性について理解しましょう。

関数呼び出しとは何ですか?

関数呼び出し(「ツールの使用」と呼ばれることもあります)は、LLM が次のことができる機能です。

  1. ユーザー リクエストで特定の関数を呼び出すと効果がある場合を認識する
  2. その関数に必要なパラメータを含む構造化 JSON オブジェクトを生成する
  3. アプリケーションで、これらのパラメータを使用して関数を実行します。
  4. 関数の結果を受信してレスポンスを作成する

LLM が何をすべきかを記述するだけでなく、関数呼び出しによって LLM がアプリで具体的なアクションをトリガーできるようにします。

Flutter アプリで関数呼び出しが重要な理由

関数呼び出しは、自然言語とアプリケーション機能の間に強力なブリッジを構築します。

  1. 直接アクション: ユーザーが目的を自然言語で説明すると、アプリが具体的なアクションで応答します。
  2. 構造化出力: LLM は、解析が必要なテキストではなく、クリーンな構造化データを生成します。
  3. 複雑なオペレーション: LLM が外部データにアクセスしたり、計算を実行したり、アプリケーションの状態を変更したりできるようにします。
  4. ユーザー エクスペリエンスの向上: 会話と機能がシームレスに統合されます。

Colorist アプリでは、関数呼び出しにより、ユーザーが「フォレストグリーンにしたい」と話しかけると、テキストから RGB 値を解析することなく、その色で UI がすぐに更新されます。

関数宣言を定義する

新しいファイル lib/services/gemini_tools.dart を作成して、関数宣言を定義します。

lib/services/gemini_tools.dart

import 'package:firebase_vertexai/firebase_vertexai.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'gemini_tools.g.dart';

class GeminiTools {
 
GeminiTools(this.ref);

 
final Ref ref;

 
FunctionDeclaration get setColorFuncDecl => FunctionDeclaration(
   
'set_color',
   
'Set the color of the display square based on red, green, and blue values.',
   
parameters: {
     
'red': Schema.number(description: 'Red component value (0.0 - 1.0)'),
     
'green': Schema.number(description: 'Green component value (0.0 - 1.0)'),
     
'blue': Schema.number(description: 'Blue component value (0.0 - 1.0)'),
   
},
 
);

 
List<Tool> get tools => [
   
Tool.functionDeclarations([setColorFuncDecl]),
 
];
}

@riverpod
GeminiTools geminiTools(Ref ref) => GeminiTools(ref);

関数宣言について

このコードが何をしているのか、分解して確認してみましょう。

  1. 関数の名前付け: 関数に set_color という名前を付け、その目的を明確に示します。
  2. 関数の説明: LLM が関数を使用するタイミングを理解できるように、明確な説明を入力します。
  3. パラメータの定義: 構造化パラメータは、独自の説明とともに定義します。
    • red: RGB の赤色コンポーネント。0.0 ~ 1.0 の間で指定します。
    • green: RGB の緑色コンポーネント。0.0 ~ 1.0 の数値で指定します。
    • blue: RGB の青色コンポーネント。0.0 ~ 1.0 の値で指定します。
  4. スキーマタイプ: Schema.number() を使用して、これらが数値であることを示します。
  5. ツール コレクション: 関数宣言を含むツールのリストを作成します。

この構造化されたアプローチは、Gemini LLM が以下を理解するのに役立ちます。

  • この関数を呼び出すタイミング
  • 指定する必要があるパラメータ
  • これらのパラメータに適用される制約(値の範囲など)

Gemini モデル プロバイダを更新する

次に、lib/providers/gemini.dart ファイルを変更して、Gemini モデルの初期化時に関数宣言を追加します。

lib/providers/gemini.dart

import 'dart:async';

import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_vertexai/firebase_vertexai.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

import '../firebase_options.dart';
import '../services/gemini_tools.dart';                              // Add this import
import 'system_prompt.dart';

part 'gemini.g.dart';

@riverpod
Future<FirebaseApp> firebaseApp(Ref ref) =>
   
Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);

@riverpod
Future<GenerativeModel> geminiModel(Ref ref) async {
 
await ref.watch(firebaseAppProvider.future);
 
final systemPrompt = await ref.watch(systemPromptProvider.future);
 
final geminiTools = ref.watch(geminiToolsProvider);                // Add this line

 
final model = FirebaseVertexAI.instance.generativeModel(
   
model: 'gemini-2.0-flash',
   
systemInstruction: Content.system(systemPrompt),
   
tools: geminiTools.tools,                                        // And this line
 
);
 
return model;
}

@Riverpod(keepAlive: true)
Future<ChatSession> chatSession(Ref ref) async {
 
final model = await ref.watch(geminiModelProvider.future);
 
return model.startChat();
}

主な変更点は、生成モデルの作成時に tools: geminiTools.tools パラメータを追加したことです。これにより、Gemini は呼び出せる関数を認識します。

システム プロンプトを更新する

次に、新しい set_color ツールの使用について LLM に指示するようにシステム プロンプトを変更する必要があります。assets/system_prompt.md を更新します。

assets/system_prompt.md

# Colorist System Prompt

You are a color expert assistant integrated into a desktop app called Colorist. Your job is to interpret natural language color descriptions and set the appropriate color values using a specialized tool.

## Your Capabilities

You are knowledgeable about colors, color theory, and how to translate natural language descriptions into specific RGB values. You have access to the following tool:

`set_color` - Sets the RGB values for the color display based on a description

## How to Respond to User Inputs

When users describe a color:

1. First, acknowledge their color description with a brief, friendly response
2. Interpret what RGB values would best represent that color description
3. Use the `set_color` tool to set those values (all values should be between 0.0 and 1.0)
4. After setting the color, provide a brief explanation of your interpretation

Example:
User: "I want a sunset orange"
You: "Sunset orange is a warm, vibrant color that captures the golden-red hues of the setting sun. It combines a strong red component with moderate orange tones."

[Then you would call the set_color tool with approximately: red=1.0, green=0.5, blue=0.25]

After the tool call: "I've set a warm orange with strong red, moderate green, and minimal blue components that is reminiscent of the sun low on the horizon."

## When Descriptions are Unclear

If a color description is ambiguous or unclear, please ask the user clarifying questions, one at a time.

## Important Guidelines

- Always keep RGB values between 0.0 and 1.0
- Provide thoughtful, knowledgeable responses about colors
- When possible, include color psychology, associations, or interesting facts about colors
- Be conversational and engaging in your responses
- Focus on being helpful and accurate with your color interpretations

システム プロンプトの主な変更点は次のとおりです。

  1. ツールの導入: フォーマットされた RGB 値を尋ねる代わりに、LLM に set_color ツールについて伝えます。
  2. 変更されたプロセス: ステップ 3 を「レスポンスの値をフォーマットする」から「ツールを使用して値を設定する」に変更します。
  3. 更新された例: レスポンスに、フォーマットされたテキストではなくツール呼び出しを含める方法を示します。
  4. 書式設定の要件を削除: 構造化関数呼び出しを使用するため、特定のテキスト形式は不要になりました

この更新されたプロンプトは、RGB 値をテキスト形式で提供するのではなく、関数呼び出しを使用するように LLM に指示します。

Riverpod コードを生成する

ビルドランナー コマンドを実行して、必要な Riverpod コードを生成します。

dart run build_runner build --delete-conflicting-outputs

アプリケーションを実行する

この時点で、Gemini は関数呼び出しを使用しようとするコンテンツを生成しますが、関数呼び出しのハンドラはまだ実装されていません。アプリを実行して色を記述すると、Gemini はツールを呼び出したかのように応答しますが、次のステップまで UI の色は変更されません。

アプリを実行します。

flutter run -d DEVICE

Gemini LLM が部分的な回答で応答している様子を示す Colorist アプリのスクリーンショット

「深いオーシャン ブルー」や「フォレスト グリーン」などの色を説明して、回答を確認してみてください。LLM は上記で定義された関数を呼び出そうとしていますが、コードでまだ関数呼び出しが検出されていません。

関数呼び出しプロセス

Gemini が関数呼び出しを使用すると、次のような処理が行われます。

  1. 関数の選択: LLM は、ユーザーのリクエストに基づいて、関数呼び出しが有用かどうかを判断します。
  2. パラメータの生成: LLM は、関数のスキーマに合ったパラメータ値を生成します。
  3. 関数呼び出しの形式: LLM はレスポンスで構造化された関数呼び出しオブジェクトを送信します。
  4. アプリの処理: アプリはこの呼び出しを受け取り、関連する関数(次のステップで実装)を実行します。
  5. レスポンスの統合: マルチターンの会話では、LLM は関数の結果が返されることを想定しています。

アプリの現在の状態では、最初の 3 つのステップは実行されていますが、ステップ 4 または 5(関数呼び出しの処理)はまだ実装されていません。これは次のステップで実装します。

技術的な詳細: Gemini が関数を使用するタイミングを判断する仕組み

Gemini は、次の情報に基づいて、関数を使用するタイミングをインテリジェントに判断します。

  1. ユーザーの意図: ユーザーのリクエストを関数で処理するのが最適かどうか
  2. 関数の関連性: 使用可能な関数がタスクにどれだけ適しているか
  3. パラメータの可用性: パラメータ値を自信を持って決定できるかどうか
  4. システム指示: 関数の使用に関するシステム プロンプトのガイダンス

明確な関数宣言とシステム インストラクションを指定することで、色の説明リクエストを set_color 関数を呼び出す機会として認識するように Gemini を設定しました。

次のステップ

次のステップでは、Gemini からの関数呼び出しのハンドラを実装します。これにより、ユーザーの説明が LLM の関数呼び出しを介して UI で実際の色の変更をトリガーできるようになります。

トラブルシューティング

関数宣言に関する問題

関数宣言でエラーが発生した場合:

  • パラメータ名とタイプが想定どおりであることを確認する
  • 関数名が明確でわかりやすいことを確認する
  • 関数の説明が目的を正確に説明していることを確認する

システム プロンプトに関する問題

LLM が関数を使用することを試行していない場合:

  • システム プロンプトで LLM に set_color ツールを使用するよう明確に指示されていることを確認します。
  • システム プロンプトの例で関数の使用方法が示されていることを確認する
  • ツールの使用手順をより明確にしてください

一般的な問題

その他の問題が発生した場合:

  • コンソールで、関数宣言に関連するエラーがないか確認します。
  • ツールがモデルに正しく渡されていることを確認する
  • Riverpod によって生成されたすべてのコードが最新であることを確認する

学んだ主なコンセプト

  • Flutter アプリで LLM 機能を拡張するための関数宣言の定義
  • 構造化データ収集用のパラメータ スキーマを作成する
  • 関数宣言と Gemini モデルの統合
  • 関数の使用を促すシステム プロンプトを更新
  • LLM が関数を選択して呼び出す仕組み

このステップでは、LLM が自然言語入力と構造化関数呼び出しのギャップを埋め、会話機能とアプリケーション機能のシームレスな統合の基盤を築く方法について説明します。

6. ツールの処理を実装する

このステップでは、Gemini から受信する関数呼び出しのハンドラを実装します。これにより、自然言語入力と具体的なアプリケーション機能間の通信のループが完成し、LLM はユーザーの説明に基づいて UI を直接操作できるようになります。

このステップで学習する内容

  • LLM アプリケーションの完全な関数呼び出しパイプラインについて
  • Flutter アプリで Gemini からの関数呼び出しを処理する
  • アプリケーションの状態を変更する関数ハンドラの実装
  • 関数レスポンスを処理して LLM に結果を返す
  • LLM と UI 間の完全な通信フローを作成する
  • 透明性を確保するための関数呼び出しとレスポンスをロギングする

関数呼び出しパイプラインについて

実装に入る前に、関数呼び出しパイプラインの全体を理解しましょう。

エンドツーエンドのフロー

  1. ユーザー入力: ユーザーが自然言語で色を説明します(例: 「forest green」)
  2. LLM 処理: Gemini が説明を分析し、set_color 関数を呼び出すことを決定します。
  3. 関数呼び出しの生成: Gemini は、パラメータ(赤、緑、青の値)を含む構造化 JSON を作成します。
  4. 関数呼び出しの受信: アプリは、この構造化データを Gemini から受信します。
  5. 関数の実行: アプリは、指定されたパラメータを使用して関数を実行します。
  6. 状態の更新: 関数によってアプリの状態が更新されます(表示される色が変更されます)。
  7. レスポンスの生成: 関数は結果を LLM に返します。
  8. レスポンスの統合: LLM はこれらの結果を最終的なレスポンスに組み込みます。
  9. UI の更新: UI が状態の変化に反応し、新しい色を表示します。

LLM を適切に統合するには、完全な通信サイクルが必要です。LLM が関数呼び出しを行う場合、単にリクエストを送信して先に進むわけではありません。代わりに、アプリケーションが関数を実行して結果を返すのを待ちます。LLM は、これらの結果を使用して最終的なレスポンスを作成し、実行されたアクションを認識する自然な会話フローを作成します。

関数ハンドラを実装する

lib/services/gemini_tools.dart ファイルを更新して、関数呼び出しのハンドラを追加しましょう。

lib/services/gemini_tools.dart

import 'package:colorist_ui/colorist_ui.dart';
import 'package:firebase_vertexai/firebase_vertexai.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'gemini_tools.g.dart';

class GeminiTools {
 
GeminiTools(this.ref);

 
final Ref ref;

 
FunctionDeclaration get setColorFuncDecl => FunctionDeclaration(
   
'set_color',
   
'Set the color of the display square based on red, green, and blue values.',
   
parameters: {
     
'red': Schema.number(description: 'Red component value (0.0 - 1.0)'),
     
'green': Schema.number(description: 'Green component value (0.0 - 1.0)'),
     
'blue': Schema.number(description: 'Blue component value (0.0 - 1.0)'),
   
},
 
);

 
List<Tool> get tools => [
   
Tool.functionDeclarations([setColorFuncDecl]),
 
];

 
Map<String, Object?> handleFunctionCall(                           // Add from here
   
String functionName,
   
Map<String, Object?> arguments,
 
) {
   
final logStateNotifier = ref.read(logStateNotifierProvider.notifier);
   
logStateNotifier.logFunctionCall(functionName, arguments);
   
return switch (functionName) {
     
'set_color' => handleSetColor(arguments),
     
_ => handleUnknownFunction(functionName),
   
};
 
}

 
Map<String, Object?> handleSetColor(Map<String, Object?> arguments) {
   
final colorStateNotifier = ref.read(colorStateNotifierProvider.notifier);
   
final red = (arguments['red'] as num).toDouble();
   
final green = (arguments['green'] as num).toDouble();
   
final blue = (arguments['blue'] as num).toDouble();
   
final functionResults = {
     
'success': true,
     
'current_color':
         
colorStateNotifier
             
.updateColor(red: red, green: green, blue: blue)
             
.toLLMContextMap(),
   
};

   
final logStateNotifier = ref.read(logStateNotifierProvider.notifier);
   
logStateNotifier.logFunctionResults(functionResults);
   
return functionResults;
 
}

 
Map<String, Object?> handleUnknownFunction(String functionName) {
   
final logStateNotifier = ref.read(logStateNotifierProvider.notifier);
   
logStateNotifier.logWarning('Unsupported function call $functionName');
   
return {
     
'success': false,
     
'reason': 'Unsupported function call $functionName',
   
};
 
}                                                                  // To here.
}

@riverpod
GeminiTools geminiTools(Ref ref) => GeminiTools(ref);

関数ハンドラについて

これらの関数ハンドラの処理内容を詳しく見てみましょう。

  1. handleFunctionCall: 次の機能を備えた中央ディスパッチャ。
    • ログパネルに透明性に関する関数呼び出しをログに記録する
    • 関数名に基づいて適切なハンドラにルーティングする
    • LLM に返される構造化されたレスポンスを返します。
  2. handleSetColor: set_color 関数に固有のハンドラ。次の処理を行います。
    • 引数マップから RGB 値を抽出します。
    • 想定される型(double)に変換します。
    • colorStateNotifier を使用してアプリの色の状態を更新します。
    • 成功ステータスと現在の色情報を含む構造化レスポンスを作成します。
    • デバッグ用に関数の結果をログに記録します。
  3. handleUnknownFunction: 不明な関数に対するフォールバック ハンドラ。次のことを行います。
    • サポートされていない関数に関する警告をログに記録します。
    • LLM にエラー レスポンスを返す

LLM の自然言語理解と具体的な UI 変更のギャップを埋めるため、handleSetColor 関数は特に重要です。

関数呼び出しとレスポンスを処理するように Gemini Chat サービスを更新する

次に、lib/services/gemini_chat_service.dart ファイルを更新して、LLM レスポンスからの関数呼び出しを処理し、結果を LLM に返します。

lib/services/gemini_chat_service.dart

import 'dart:async';

import 'package:colorist_ui/colorist_ui.dart';
import 'package:firebase_vertexai/firebase_vertexai.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

import '../providers/gemini.dart';
import 'gemini_tools.dart';                                          // Add this import

part 'gemini_chat_service.g.dart';

class GeminiChatService {
 
GeminiChatService(this.ref);
 
final Ref ref;

 
Future<void> sendMessage(String message) async {
   
final chatSession = await ref.read(chatSessionProvider.future);
   
final chatStateNotifier = ref.read(chatStateNotifierProvider.notifier);
   
final logStateNotifier = ref.read(logStateNotifierProvider.notifier);

   
chatStateNotifier.addUserMessage(message);
   
logStateNotifier.logUserText(message);
   
final llmMessage = chatStateNotifier.createLlmMessage();
   
try {
     
final response = await chatSession.sendMessage(Content.text(message));

     
final responseText = response.text;
     
if (responseText != null) {
       
logStateNotifier.logLlmText(responseText);
       
chatStateNotifier.appendToMessage(llmMessage.id, responseText);
     
}

     
if (response.functionCalls.isNotEmpty) {                       // Add from here
       
final geminiTools = ref.read(geminiToolsProvider);
       
final functionResultResponse = await chatSession.sendMessage(
         
Content.functionResponses([
           
for (final functionCall in response.functionCalls)
             
FunctionResponse(
               
functionCall.name,
               
geminiTools.handleFunctionCall(
                 
functionCall.name,
                 
functionCall.args,
               
),
             
),
         
]),
       
);
       
final responseText = functionResultResponse.text;
       
if (responseText != null) {
         
logStateNotifier.logLlmText(responseText);
         
chatStateNotifier.appendToMessage(llmMessage.id, responseText);
       
}
     
}                                                              // To here.
   
} catch (e, st) {
     
logStateNotifier.logError(e, st: st);
     
chatStateNotifier.appendToMessage(
       
llmMessage.id,
       
"\nI'm sorry, I encountered an error processing your request. "
       
"Please try again.",
     
);
   
} finally {
     
chatStateNotifier.finalizeMessage(llmMessage.id);
   
}
 
}
}

@riverpod
GeminiChatService geminiChatService(Ref ref) => GeminiChatService(ref);

コミュニケーションのフローを理解する

ここでの主な追加点は、関数呼び出しとレスポンスを完全に処理することです。

if (response.functionCalls.isNotEmpty) {
 
final geminiTools = ref.read(geminiToolsProvider);
 
final functionResultResponse = await chatSession.sendMessage(
   
Content.functionResponses([
     
for (final functionCall in response.functionCalls)
       
FunctionResponse(
          functionCall
.name,
          geminiTools
.handleFunctionCall(
            functionCall
.name,
            functionCall
.args,
         
),
       
),
   
]),
 
);
 
final responseText = functionResultResponse.text;
 
if (responseText != null) {
    logStateNotifier
.logLlmText(responseText);
    chatStateNotifier
.appendToMessage(llmMessage.id, responseText);
 
}
}

このコード:

  1. LLM レスポンスに関数呼び出しが含まれているかどうかを確認します。
  2. 関数呼び出しごとに、関数名と引数を指定して handleFunctionCall メソッドを呼び出します。
  3. 各関数呼び出しの結果を収集する
  4. Content.functionResponses を使用してこれらの結果を LLM に返します。
  5. 関数の結果に対する LLM の回答を処理します。
  6. 最終的なレスポンス テキストで UI を更新します。

これにより、ラウンドトリップ フローが作成されます。

  • ユーザー → LLM: 色をリクエストする
  • LLM → アプリ: パラメータ付きの関数呼び出し
  • アプリ → ユーザー: 新しい色が表示される
  • アプリ → LLM: 関数の結果
  • LLM → ユーザー: 関数の結果を含む最終的なレスポンス

Riverpod コードを生成する

ビルドランナー コマンドを実行して、必要な Riverpod コードを生成します。

dart run build_runner build --delete-conflicting-outputs

フロー全体を実行してテストする

続いてアプリケーションを実行します。

flutter run -d DEVICE

Gemini LLM が関数呼び出しで応答している様子を示す Colorist アプリのスクリーンショット

さまざまな色の説明を入力してみます。

  • 「深い深紅色をお願いします」
  • 「落ち着いた空色を表示して」
  • 「新鮮なミントの葉の色を教えて」
  • 「夕日のような暖かいオレンジ色にしたい」
  • 「濃いロイヤルパープルにしてください」

次のように表示されます。

  1. チャット インターフェースに表示されるメッセージ
  2. チャットに表示される Gemini の回答
  3. ログパネルに記録される関数呼び出し
  4. 関数の結果が直後にログに記録される
  5. 説明されている色を表示するように更新される色の長方形
  6. 新しい色のコンポーネントを示すように更新された RGB 値
  7. Gemini の最終的な回答が表示される(多くの場合、設定された色についてコメントする)

ログパネルには、バックグラウンドで何が起こっているかに関する分析情報が表示されます。表示される項目

  • Gemini が行っている正確な関数呼び出し
  • 各 RGB 値に選択されているパラメータ
  • 関数が返す結果
  • Gemini からのフォローアップの回答

色の状態通知

色の更新に使用している colorStateNotifier は、colorist_ui パッケージの一部です。管理対象の項目は次のとおりです。

  • UI に表示されている現在の色
  • 色の履歴(直近 10 色)
  • UI コンポーネントの状態変化の通知

新しい RGB 値で updateColor を呼び出すと、次のことが行われます。

  1. 指定された値を使用して新しい ColorData オブジェクトを作成します。
  2. アプリの状態の現在の色を更新します。
  3. 色を履歴に追加します
  4. Riverpod の状態管理を介して UI の更新をトリガーする

colorist_ui パッケージの UI コンポーネントは、この状態を監視し、状態が変更されると自動的に更新して、リアクティブなエクスペリエンスを実現します。

エラー処理について

実装に堅牢なエラー処理が含まれている:

  1. try-catch ブロック: すべての LLM インタラクションをラップして例外をキャッチします。
  2. エラーロギング: ログパネルにスタック トレースとともにエラーを記録します。
  3. ユーザー フィードバック: チャットでわかりやすいエラー メッセージを提供します。
  4. 状態のクリーンアップ: エラーが発生した場合でも、メッセージの状態をファイナライズします。

これにより、LLM サービスまたは関数の実行で問題が発生した場合でも、アプリの安定性が維持され、適切なフィードバックが提供されます。

ユーザー エクスペリエンスにおける関数呼び出しの威力

ここで作成したインターフェースは、LLM を使用して強力な自然なインターフェースを作成する方法を示しています。

  1. 自然言語インターフェース: ユーザーは日常的な言語で意図を表現します。
  2. インテリジェントな解釈: LLM は、あいまいな説明を正確な値に変換します。
  3. 直接操作: 自然言語に応じて UI が更新されます。
  4. コンテキストに応じた回答: LLM は、変更に関する会話のコンテキストを提供します。
  5. 認知負荷が低い: ユーザーは RGB 値や色彩理論を理解する必要がない

LLM 関数呼び出しを使用して自然言語と UI アクションを橋渡しするこのパターンは、色選択以外の無数のドメインに拡張できます。

次のステップ

次のステップでは、ストリーミング レスポンスを実装してユーザー エクスペリエンスを向上させます。レスポンスが完了するのを待つのではなく、テキスト チャンクと関数呼び出しが受信されたときに処理することで、より応答性が高く魅力的なアプリケーションを作成できます。

トラブルシューティング

関数呼び出しに関する問題

Gemini が関数を呼び出していない場合、またはパラメータが正しくない場合は、次の手順を行います。

  • 関数の宣言がシステム プロンプトに記載されている内容と一致していることを確認する
  • パラメータ名と型が一貫していることを確認する
  • システム プロンプトで、ツールを使用するように LLM に明示的に指示していることを確認する
  • ハンドラの関数名が宣言の内容と完全に一致していることを確認します。
  • ログパネルで関数呼び出しの詳細を確認する

関数のレスポンスに関する問題

関数の結果が LLM に正しく送信されない場合:

  • 関数が正しくフォーマットされた Map を返すことを確認する
  • Content.functionResponses が正しく構築されていることを確認する
  • 関数のレスポンスに関連するエラーがログにないか確認します。
  • レスポンスに同じチャット セッションを使用していることを確認する

色の表示に関する問題

色が正しく表示されない場合は、次の手順を行います。

  • RGB 値が適切に倍精度浮動小数点数に変換されるようにする(LLM は整数として送信することがある)
  • 値が想定される範囲(0.0 ~ 1.0)内であることを確認します。
  • カラー状態通知が正しく呼び出されていることを確認する
  • 関数に渡される正確な値がログに記録されていることを確認する

全般的な問題

一般的な問題の場合:

  • ログでエラーや警告を確認する
  • Vertex AI in Firebase の接続を確認する
  • 関数パラメータの型の不一致を確認する
  • Riverpod によって生成されたすべてのコードが最新であることを確認する

学んだ主なコンセプト

  • Flutter で完全な関数呼び出しパイプラインを実装する
  • LLM とアプリケーション間の完全な通信を作成する
  • LLM レスポンスからの構造化データの処理
  • 関数の結果を LLM に返してレスポンスを作成する
  • ログパネルを使用して LLM とアプリケーションのやり取りを可視化する
  • 自然言語入力を具体的な UI 変更に結び付ける

この手順が完了すると、アプリは LLM 統合の最も強力なパターンの 1 つを実証します。このパターンでは、自然言語入力を具体的な UI アクションに変換しながら、これらのアクションを認識する一貫した会話を維持します。これにより、ユーザーにとって魔法のような直感的な会話型インターフェースが実現します。

7. ストリーミング レスポンスで UX を改善

このステップでは、Gemini からのストリーミング レスポンスを実装して、ユーザー エクスペリエンスを向上させます。レスポンス全体が生成されるのを待つのではなく、テキスト チャンクと関数呼び出しが受信されたときに処理することで、より応答性が高く魅力的なアプリケーションを作成できます。

このステップで説明する内容

  • LLM を活用したアプリケーションでストリーミングが重要である理由
  • Flutter アプリにストリーミング LLM レスポンスを実装する
  • API から受信したテキストの一部を処理する
  • 会話の状態を管理してメッセージの競合を防ぐ
  • ストリーミング レスポンスでの関数呼び出しの処理
  • 処理中の回答の視覚的なインジケーターの作成

LLM アプリケーションでストリーミングが重要な理由

実装する前に、LLM で優れたユーザー エクスペリエンスを実現するためにレスポンスのストリーミングが不可欠である理由を理解しましょう。

ユーザー エクスペリエンスの向上

レスポンスのストリーミングには、次のようなユーザー エクスペリエンスのメリットがあります。

  1. レイテンシの認識を低減: ユーザーは、回答が完全に表示されるまで数秒待つ必要はなく、テキストがすぐに(通常は 100 ~ 300 ミリ秒以内に)表示され始めます。こうした即時性に対する認識は、ユーザー満足度の大幅な向上につながります。
  2. 自然な会話のリズム: テキストが段階的に表示されるため、人間のコミュニケーションを模倣し、より自然な会話体験を実現できます。
  3. 情報の段階的な処理: 大量のテキストを一気に処理するのではなく、情報の受信に合わせて処理を開始できます。
  4. 早期の中断の機会: 完全なアプリでは、LLM が役に立たない方向に進んでいると判断した場合、ユーザーは LLM を中断またはリダイレクトできます。
  5. アクティビティの視覚的な確認: ストリーミング テキストは、システムが動作していることを即座にフィードバックし、不確実性を軽減します。

技術的なメリット

ストリーミングには、UX の改善以外にも次のような技術的なメリットがあります。

  1. 関数の早期実行: 関数呼び出しは、レスポンスの完了を待たずに、ストリームに表示され次第検出して実行できます。
  2. UI の段階的な更新: 新しい情報が届くたびに UI を段階的に更新し、よりダイナミックなエクスペリエンスを実現できます。
  3. 会話状態の管理: ストリーミングでは、レスポンスが完了したのか、まだ処理中なのかを明確に示すシグナルが提供されるため、状態を適切に管理できます。
  4. タイムアウトのリスクの軽減: 非ストリーミング レスポンスでは、生成に時間がかかり、接続タイムアウトが発生するリスクがあります。ストリーミングは、接続を早期に確立して維持します。

Colorist アプリにストリーミングを実装すると、テキスト レスポンスと色の変更の両方がより迅速に表示されるため、応答性が大幅に向上します。

会話状態管理を追加する

まず、アプリが現在ストリーミング レスポンスを処理しているかどうかを追跡する状態プロバイダを追加しましょう。lib/services/gemini_chat_service.dart ファイルを更新します。

lib/services/gemini_chat_service.dart

import 'dart:async';

import 'package:colorist_ui/colorist_ui.dart';
import 'package:firebase_vertexai/firebase_vertexai.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

import '../providers/gemini.dart';
import 'gemini_tools.dart';

part 'gemini_chat_service.g.dart';

final conversationStateProvider = StateProvider(                     // Add from here...
 
(ref) => ConversationState.idle,
);                                                                   // To here.

class GeminiChatService {
 
GeminiChatService(this.ref);
 
final Ref ref;

 
Future<void> sendMessage(String message) async {
   
final chatSession = await ref.read(chatSessionProvider.future);
   
final conversationState = ref.read(conversationStateProvider);   // Add this line
   
final chatStateNotifier = ref.read(chatStateNotifierProvider.notifier);
   
final logStateNotifier = ref.read(logStateNotifierProvider.notifier);

   
if (conversationState == ConversationState.busy) {               // Add from here...
     
logStateNotifier.logWarning(
       
"Can't send a message while a conversation is in progress",
     
);
     
throw Exception(
       
"Can't send a message while a conversation is in progress",
     
);
   
}
   
final conversationStateNotifier = ref.read(
     
conversationStateProvider.notifier,
   
);
   
conversationStateNotifier.state = ConversationState.busy;        // To here.
   
chatStateNotifier.addUserMessage(message);
   
logStateNotifier.logUserText(message);
   
final llmMessage = chatStateNotifier.createLlmMessage();
   
try {                                                            // Modify from here...
     
final responseStream = chatSession.sendMessageStream(
       
Content.text(message),
     
);
     
await for (final block in responseStream) {
       
await _processBlock(block, llmMessage.id);
     
}                                                              // To here.
   
} catch (e, st) {
     
logStateNotifier.logError(e, st: st);
     
chatStateNotifier.appendToMessage(
       
llmMessage.id,
       
"\nI'm sorry, I encountered an error processing your request. "
       
"Please try again.",
     
);
   
} finally {
     
chatStateNotifier.finalizeMessage(llmMessage.id);
     
conversationStateNotifier.state = ConversationState.idle;      // Add this line.
   
}
 
}

 
Future<void> _processBlock(                                        // Add from here...
   
GenerateContentResponse block,
   
String llmMessageId,
 
) async {
   
final chatSession = await ref.read(chatSessionProvider.future);
   
final chatStateNotifier = ref.read(chatStateNotifierProvider.notifier);
   
final logStateNotifier = ref.read(logStateNotifierProvider.notifier);
   
final blockText = block.text;

   
if (blockText != null) {
     
logStateNotifier.logLlmText(blockText);
     
chatStateNotifier.appendToMessage(llmMessageId, blockText);
   
}

   
if (block.functionCalls.isNotEmpty) {
     
final geminiTools = ref.read(geminiToolsProvider);
     
final responseStream = chatSession.sendMessageStream(
       
Content.functionResponses([
         
for (final functionCall in block.functionCalls)
           
FunctionResponse(
             
functionCall.name,
             
geminiTools.handleFunctionCall(
               
functionCall.name,
               
functionCall.args,
             
),
           
),
       
]),
     
);
     
await for (final response in responseStream) {
       
final responseText = response.text;
       
if (responseText != null) {
         
logStateNotifier.logLlmText(responseText);
         
chatStateNotifier.appendToMessage(llmMessageId, responseText);
       
}
     
}
   
}
 
}                                                                  // To here.
}

@riverpod
GeminiChatService geminiChatService(Ref ref) => GeminiChatService(ref);

ストリーミングの実装について

このコードが何をしているのか、分解して確認してみましょう。

  1. 会話の状態のトラッキング:
    • conversationStateProvider は、アプリが現在レスポンスを処理しているかどうかを追跡します。
    • 処理中に状態が idlebusy に移行し、その後 idle に戻る
    • これにより、競合する可能性のある複数の同時リクエストを防ぐことができます。
  2. ストリームの初期化:
    • sendMessageStream() は、完全なレスポンスを含む Future ではなく、レスポンス チャンクの Stream を返します。
    • 各チャンクには、テキスト、関数呼び出し、またはその両方を含めることができます。
  3. プログレッシブ処理:
    • await for は、受信した各チャンクをリアルタイムで処理します。
    • テキストが UI にすぐに追加され、ストリーミング効果が作成されます
    • 関数呼び出しは検出されるとすぐに実行される
  4. 関数呼び出しの処理:
    • チャンクで関数呼び出しが検出されると、すぐに実行されます。
    • 結果は別のストリーミング呼び出しを介して LLM に返されます。
    • これらの結果に対する LLM のレスポンスもストリーミング形式で処理されます。
  5. エラー処理とクリーンアップ:
    • try/catch は堅牢なエラー処理を提供します
    • finally ブロックにより、会話の状態が適切にリセットされる
    • エラーが発生しても、メッセージは常に確定されます

この実装により、適切な会話状態を維持しながら、レスポンシブで信頼性の高いストリーミング エクスペリエンスを実現できます。

メイン画面を更新して会話の状態を接続

会話の状態をメイン画面に渡すように lib/main.dart ファイルを変更します。

lib/main.dart

import 'package:colorist_ui/colorist_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import 'providers/gemini.dart';
import 'services/gemini_chat_service.dart';

void main() async {
 
runApp(ProviderScope(child: MainApp()));
}

class MainApp extends ConsumerWidget {
 
const MainApp({super.key});

 
@override
 
Widget build(BuildContext context, WidgetRef ref) {
   
final model = ref.watch(geminiModelProvider);
   
final conversationState = ref.watch(conversationStateProvider);  // Add this line

   
return MaterialApp(
     
theme: ThemeData(
       
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
     
),
     
home: model.when(
       
data:
           
(data) => MainScreen(
             
conversationState: conversationState,                  // And this line
             
sendMessage: (text) {
               
ref.read(geminiChatServiceProvider).sendMessage(text);
             
},
           
),
       
loading: () => LoadingScreen(message: 'Initializing Gemini Model'),
       
error: (err, st) => ErrorScreen(error: err),
     
),
   
);
 
}
}

ここでの主な変更点は、conversationStateMainScreen ウィジェットに渡すことです。MainScreencolorist_ui パッケージが提供する)は、この状態を使用して、レスポンスの処理中にテキスト入力を無効にします。

これにより、会話の現在の状態が UI に反映される、一貫したユーザー エクスペリエンスが実現します。

Riverpod コードを生成する

ビルドランナー コマンドを実行して、必要な Riverpod コードを生成します。

dart run build_runner build --delete-conflicting-outputs

ストリーミング レスポンスを実行してテストする

アプリケーションを実行します。

flutter run -d DEVICE

Gemini LLM がストリーミング形式で応答していることを示す Colorist アプリのスクリーンショット

次に、さまざまな色の説明を使用してストリーミング動作をテストします。次のような説明を試してみましょう。

  • 「夕暮れ時の海の深いターコイズ色を見せて」
  • 「熱帯の花を思わせる鮮やかなサンゴを探しています」
  • 「古い軍服のような落ち着いたオリーブグリーンを作成する」

ストリーミングの技術的なフロー(詳細)

レスポンスをストリーミングすると、次のような処理が行われます。

接続の確立

sendMessageStream() を呼び出すと、次のようになります。

  1. アプリが Vertex AI サービスへの接続を確立します。
  2. ユーザー リクエストがサービスに送信される
  3. サーバーがリクエストの処理を開始します
  4. ストリーム接続は開いたままで、チャンクを送信する準備ができています

チャンクの送信

Gemini がコンテンツを生成するときに、チャンクがストリームを介して送信されます。

  1. サーバーは、生成されたテキスト チャンク(通常は数語または数文)を送信します。
  2. Gemini が関数呼び出しを行うことを決定すると、関数呼び出し情報を送信します。
  3. 関数呼び出しの後に追加のテキスト チャンクが続く可能性がある
  4. 生成が完了するまでストリーミングが続行されます。

プログレッシブ処理

アプリは各チャンクを段階的に処理します。

  1. 各テキスト チャンクが既存のレスポンスに追加されます。
  2. 関数呼び出しは検出されるとすぐに実行される
  3. UI は、テキストと関数の結果の両方でリアルタイムで更新されます。
  4. レスポンスがまだストリーミング中であることを示すステータスが追跡される

ライブ配信の完了

生成が完了すると、次のようになります。

  1. ストリームがサーバーによって閉じられる
  2. await for ループが自然に終了する
  3. メッセージが完了としてマークされている
  4. 会話のステータスがアイドル状態に戻ります
  5. UI が更新され、完了した状態が反映されます

ストリーミングと非ストリーミングの比較

ストリーミングのメリットをより深く理解するために、ストリーミング アプローチとストリーミング以外のアプローチを比較してみましょう。

Aspect

非ストリーミング

ストリーミング

知覚レイテンシ

完全なレスポンスが準備されるまで、ユーザーには何も表示されません

ユーザーが最初の単語をミリ秒以内に表示

ユーザー エクスペリエンス

長い待ち時間の後に突然テキストが表示される

自然で段階的なテキストの表示

状態管理

シンプル(メッセージは保留中または完了)

より複雑(メッセージがストリーミング状態になる可能性がある)

関数の実行

レスポンスの完了後にのみ発生

レスポンス生成中に発生

実装の複雑さ

実装が簡単

追加の状態管理が必要

エラーの復元

オール オア ナッシング レスポンス

部分的な回答でも役に立つことがある

コードの複雑さ

複雑さの低減

ストリーム処理が複雑

Colorist のようなアプリケーションでは、ストリーミングの UX のメリットが実装の複雑さよりも優先されます。特に、生成に数秒かかる可能性のある色の解釈では、この傾向が強くなります。

ストリーミング UX のベスト プラクティス

独自の LLM アプリケーションにストリーミングを実装する場合は、次のベスト プラクティスを検討してください。

  1. 明確な視覚的な指標: ストリーミング メッセージと完全なメッセージの違いを明確に示す視覚的な手がかりを常に提供します。
  2. 入力のブロック: ストリーミング中にユーザー入力を無効にして、複数の重複するリクエストを防ぐ
  3. エラー復元: ストリーミングが中断された場合にグレースフル リカバリを処理するように UI を設計する
  4. 状態の遷移: アイドル状態、ストリーミング状態、完了状態をスムーズに遷移する
  5. 進行状況の可視化: 処理中であることを示す控えめなアニメーションやインジケーターを検討してください。
  6. キャンセル オプション: 完成したアプリでは、進行中の生成をユーザーがキャンセルできるようにします。
  7. 関数の結果の統合: ストリーミング中に表示される関数の結果を処理するように UI を設計する
  8. パフォーマンスの最適化: 急速なストリーム更新中の UI の再ビルドを最小限に抑える

colorist_ui パッケージでは、これらのベスト プラクティスの多くを実装していますが、ストリーミング LLM の実装では重要な考慮事項となります。

次のステップ

次のステップでは、ユーザーが履歴から色を選択したときに Gemini に通知することで、LLM の同期を実装します。これにより、ユーザーが開始したアプリケーション状態の変更を LLM が認識する、より統合されたエクスペリエンスが実現します。

トラブルシューティング

ストリーム処理に関する問題

ストリーム処理で問題が発生した場合:

  • 症状: 部分的なレスポンス、テキストがない、ストリームが突然終了する
  • 解決策: ネットワーク接続を確認し、コードに適切な非同期/待機パターンがあることを確認する
  • 診断: ログパネルで、ストリーム処理に関連するエラー メッセージや警告がないか確認します。
  • 修正: すべてのストリーム処理で try/catch ブロックを使用して適切なエラー処理を行うようにしました

関数の呼び出しがない

ストリームで関数呼び出しが検出されない場合は、次の操作を行います。

  • 事象: テキストは表示されるが色が更新されない、またはログに関数呼び出しが表示されない
  • 解決策: 関数呼び出しの使用に関するシステム プロンプトの指示を確認する
  • 診断: ログパネルで、関数呼び出しが受信されているかどうかを確認します。
  • 修正: set_color ツールを使用するように LLM に明示的に指示するようにシステム プロンプトを調整しました

一般的なエラー処理

その他の問題:

  • ステップ 1: ログパネルでエラー メッセージを確認する
  • ステップ 2: Firebase での Vertex AI の接続を確認する
  • ステップ 3: Riverpod によって生成されたすべてのコードが最新であることを確認する
  • ステップ 4: ストリーミングの実装で await ステートメントが不足していないか確認する

学んだ主なコンセプト

  • Gemini API を使用してストリーミング レスポンスを実装し、レスポンシブな UX を実現する
  • 会話状態を管理してストリーミング インタラクションを適切に処理する
  • リアルタイム テキストと関数呼び出しを着信時に処理する
  • ストリーミング中に増分更新されるレスポンシブ UI を作成する
  • 適切な非同期パターンで同時ストリームを処理する
  • レスポンスのストリーミング中に適切な視覚的なフィードバックを提供

ストリーミングを実装することで、Colorist アプリのユーザー エクスペリエンスが大幅に向上し、より応答性が高く魅力的なインターフェースが実現し、会話のように感じられるようになりました。

8. LLM コンテキストの同期

このボーナス ステップでは、ユーザーが履歴から色を選択したときに Gemini に通知することで、LLM コンテキスト同期を実装します。これにより、明示的なメッセージだけでなく、インターフェースでのユーザー操作も LLM が認識する、より統合されたエクスペリエンスが実現します。

このステップで説明する内容

  • UI と LLM 間の LLM コンテキスト同期を作成する
  • UI イベントを LLM が理解できるコンテキストにシリアル化する
  • ユーザーの操作に基づいて会話コンテキストを更新する
  • さまざまなインタラクション方法で統一されたエクスペリエンスを作成する
  • 明示的なチャット メッセージ以外の LLM コンテキスト認識の強化

LLM コンテキストの同期について

従来の chatbot は、明示的なユーザー メッセージにのみ応答するため、ユーザーが他の手段でアプリを操作すると、連携が途切れます。LLM コンテキスト同期は、この制限に対処します。

LLM コンテキスト同期が重要な理由

ユーザーが UI 要素(履歴から色を選択するなど)を使用してアプリを操作した場合、LLM は明示的に指示しない限り、何が起こったのかを把握できません。LLM コンテキストの同期:

  1. コンテキストを維持する: 関連するすべてのユーザー操作について LLM に通知します。
  2. 整合性を作成する: LLM が UI 操作を認識する統一されたエクスペリエンスを生成します。
  3. インテリジェンスを強化: LLM がすべてのユーザー操作に適切に対応できるようにします。
  4. ユーザー エクスペリエンスの向上: アプリ全体がより統合され、レスポンシブに感じられるようになります。
  5. ユーザーの負担を軽減: ユーザーが UI 操作を手動で説明する必要がなくなります。

Colorist アプリでは、ユーザーが履歴から色を選択すると、Gemini がこのアクションを認識し、選択した色についてインテリジェントにコメントして、シームレスで認識力のあるアシスタントの錯覚を維持します。

色選択通知の Gemini Chat サービスを更新

まず、ユーザーが履歴から色を選択したときに LLM に通知するメソッドを GeminiChatService に追加します。lib/services/gemini_chat_service.dart ファイルを更新します。

lib/services/gemini_chat_service.dart

import 'dart:async';
import 'dart:convert';                                               // Add this import

import 'package:colorist_ui/colorist_ui.dart';
import 'package:firebase_vertexai/firebase_vertexai.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

import '../providers/gemini.dart';
import 'gemini_tools.dart';

part 'gemini_chat_service.g.dart';

final conversationStateProvider = StateProvider(
 
(ref) => ConversationState.idle,
);

class GeminiChatService {
 
GeminiChatService(this.ref);
 
final Ref ref;

 
Future<void> notifyColorSelection(ColorData color) => sendMessage(  // Add from here...
   
'User selected color from history: ${json.encode(color.toLLMContextMap())}',
 
);                                                                  // To here.

 
Future<void> sendMessage(String message) async {
   
final chatSession = await ref.read(chatSessionProvider.future);
   
final conversationState = ref.read(conversationStateProvider);
   
final chatStateNotifier = ref.read(chatStateNotifierProvider.notifier);
   
final logStateNotifier = ref.read(logStateNotifierProvider.notifier);

   
if (conversationState == ConversationState.busy) {
     
logStateNotifier.logWarning(
       
"Can't send a message while a conversation is in progress",
     
);
     
throw Exception(
       
"Can't send a message while a conversation is in progress",
     
);
   
}
   
final conversationStateNotifier = ref.read(
     
conversationStateProvider.notifier,
   
);
   
conversationStateNotifier.state = ConversationState.busy;
   
chatStateNotifier.addUserMessage(message);
   
logStateNotifier.logUserText(message);
   
final llmMessage = chatStateNotifier.createLlmMessage();
   
try {
     
final responseStream = chatSession.sendMessageStream(
       
Content.text(message),
     
);
     
await for (final block in responseStream) {
       
await _processBlock(block, llmMessage.id);
     
}
   
} catch (e, st) {
     
logStateNotifier.logError(e, st: st);
     
chatStateNotifier.appendToMessage(
       
llmMessage.id,
       
"\nI'm sorry, I encountered an error processing your request. "
       
"Please try again.",
     
);
   
} finally {
     
chatStateNotifier.finalizeMessage(llmMessage.id);
     
conversationStateNotifier.state = ConversationState.idle;
   
}
 
}

 
Future<void> _processBlock(
   
GenerateContentResponse block,
   
String llmMessageId,
 
) async {
   
final chatSession = await ref.read(chatSessionProvider.future);
   
final chatStateNotifier = ref.read(chatStateNotifierProvider.notifier);
   
final logStateNotifier = ref.read(logStateNotifierProvider.notifier);
   
final blockText = block.text;

   
if (blockText != null) {
     
logStateNotifier.logLlmText(blockText);
     
chatStateNotifier.appendToMessage(llmMessageId, blockText);
   
}

   
if (block.functionCalls.isNotEmpty) {
     
final geminiTools = ref.read(geminiToolsProvider);
     
final responseStream = chatSession.sendMessageStream(
       
Content.functionResponses([
         
for (final functionCall in block.functionCalls)
           
FunctionResponse(
             
functionCall.name,
             
geminiTools.handleFunctionCall(
               
functionCall.name,
               
functionCall.args,
             
),
           
),
       
]),
     
);
     
await for (final response in responseStream) {
       
final responseText = response.text;
       
if (responseText != null) {
         
logStateNotifier.logLlmText(responseText);
         
chatStateNotifier.appendToMessage(llmMessageId, responseText);
       
}
     
}
   
}
 
}
}

@riverpod
GeminiChatService geminiChatService(Ref ref) => GeminiChatService(ref);

主な追加点は notifyColorSelection メソッドです。このメソッドは次の処理を行います。

  1. 選択した色を表す ColorData オブジェクトを受け取ります。
  2. メッセージを含めることができる JSON 形式にエンコードします。
  3. ユーザーの選択を示す特別な形式のメッセージを LLM に送信します。
  4. 既存の sendMessage メソッドを再利用して通知を処理する

このアプローチでは、既存のメッセージ処理インフラストラクチャを利用して重複を回避します。

メインアプリを更新して、色選択通知を接続

次に、lib/main.dart ファイルを変更して、色選択通知関数をメイン画面に渡します。

lib/main.dart

import 'package:colorist_ui/colorist_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import 'providers/gemini.dart';
import 'services/gemini_chat_service.dart';

void main() async {
 
runApp(ProviderScope(child: MainApp()));
}

class MainApp extends ConsumerWidget {
 
const MainApp({super.key});

 
@override
 
Widget build(BuildContext context, WidgetRef ref) {
   
final model = ref.watch(geminiModelProvider);
   
final conversationState = ref.watch(conversationStateProvider);

   
return MaterialApp(
     
theme: ThemeData(
       
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
     
),
     
home: model.when(
       
data:
           
(data) => MainScreen(
             
conversationState: conversationState,
             
notifyColorSelection: (color) {                        // Add from here...
               
ref.read(geminiChatServiceProvider).notifyColorSelection(color);
             
},                                                     // To here.
             
sendMessage: (text) {
               
ref.read(geminiChatServiceProvider).sendMessage(text);
             
},
           
),
       
loading: () => LoadingScreen(message: 'Initializing Gemini Model'),
       
error: (err, st) => ErrorScreen(error: err),
     
),
   
);
 
}
}

主な変更点は、notifyColorSelection コールバックの追加です。このコールバックは、UI イベント(履歴から色を選択する)を LLM 通知システムに接続します。

システム プロンプトを更新する

次に、システム プロンプトを更新して、色選択通知にどのように応答するかを LLM に指示する必要があります。assets/system_prompt.md ファイルを変更します。

assets/system_prompt.md

# Colorist System Prompt

You are a color expert assistant integrated into a desktop app called Colorist. Your job is to interpret natural language color descriptions and set the appropriate color values using a specialized tool.

## Your Capabilities

You are knowledgeable about colors, color theory, and how to translate natural language descriptions into specific RGB values. You have access to the following tool:

`set_color` - Sets the RGB values for the color display based on a description

## How to Respond to User Inputs

When users describe a color:

1. First, acknowledge their color description with a brief, friendly response
2. Interpret what RGB values would best represent that color description
3. Use the `set_color` tool to set those values (all values should be between 0.0 and 1.0)
4. After setting the color, provide a brief explanation of your interpretation

Example:
User: "I want a sunset orange"
You: "Sunset orange is a warm, vibrant color that captures the golden-red hues of the setting sun. It combines a strong red component with moderate orange tones."

[Then you would call the set_color tool with approximately: red=1.0, green=0.5, blue=0.25]

After the tool call: "I've set a warm orange with strong red, moderate green, and minimal blue components that is reminiscent of the sun low on the horizon."

## When Descriptions are Unclear

If a color description is ambiguous or unclear, please ask the user clarifying questions, one at a time.

## When Users Select Historical Colors

Sometimes, the user will manually select a color from the history panel. When this happens, you'll receive a notification about this selection that includes details about the color. Acknowledge this selection with a brief response that recognizes what they've done and comments on the selected color.

Example notification:
User: "User selected color from history: {red: 0.2, green: 0.5, blue: 0.8, hexCode: #3380CC}"
You: "I see you've selected an ocean blue from your history. This tranquil blue with a moderate intensity has a calming, professional quality to it. Would you like to explore similar shades or create a contrasting color?"

## Important Guidelines

- Always keep RGB values between 0.0 and 1.0
- Provide thoughtful, knowledgeable responses about colors
- When possible, include color psychology, associations, or interesting facts about colors
- Be conversational and engaging in your responses
- Focus on being helpful and accurate with your color interpretations

主な追加点は、[ユーザーが過去の色を選択した場合] セクションです。このセクションでは、次のことを行えます。

  1. LLM への履歴選択通知のコンセプトを説明する
  2. これらの通知の例を提示します。
  3. 適切な回答の例を表示する
  4. 選択内容の確認と色に関するコメントの期待値を設定します。

これにより、LLM はこれらの特別なメッセージに適切に対応する方法を理解できます。

Riverpod コードを生成する

ビルドランナー コマンドを実行して、必要な Riverpod コードを生成します。

dart run build_runner build --delete-conflicting-outputs

LLM コンテキスト同期を実行してテストする

アプリケーションを実行します。

flutter run -d DEVICE

カラーリストから選択された色に Gemini LLM が応答している様子を示す Colorist アプリのスクリーンショット

LLM コンテキスト同期のテストでは、次の操作を行います。

  1. まず、Chat で色を説明していくつかの色を生成します。
    • 「鮮やかな紫色を見せて」
    • 「フォレスト グリーンをお願いします」
    • 「明るい赤を」
  2. 次に、履歴ストリップの色のいずれかのサムネイルをクリックします。

次のように表示されます。

  1. 選択した色がメインディスプレイに表示されます
  2. 色の選択を示すユーザー メッセージがチャットに表示されます
  3. LLM は、選択を認識して色についてコメントすることで応答します
  4. やり取り全体が自然でまとまりがある

これにより、LLM がダイレクト メッセージと UI 操作の両方を認識し、適切に応答するシームレスなエクスペリエンスが実現します。

LLM コンテキスト同期の仕組み

この同期の仕組みについて、技術的な詳細を説明します。

Data Flow

  1. ユーザー アクション: ユーザーが履歴ストリップの色をクリックする
  2. UI イベント: MainScreen ウィジェットがこの選択を検出します。
  3. コールバックの実行: notifyColorSelection コールバックがトリガーされます。
  4. メッセージの作成: 色データを使用して、特別な形式のメッセージが作成されます。
  5. LLM 処理: メッセージは Gemini に送信され、Gemini が形式を認識します。
  6. コンテキスト レスポンス: Gemini はシステム プロンプトに基づいて適切に応答します。
  7. UI の更新: レスポンスがチャットに表示され、統一感のあるエクスペリエンスが実現

データのシリアル化

このアプローチの重要な要素は、カラーデータをシリアル化する方法です。

'User selected color from history: ${json.encode(color.toLLMContextMap())}'

toLLMContextMap() メソッド(colorist_ui パッケージで提供)は、LLM が理解できるキー プロパティを含むマップに ColorData オブジェクトを変換します。通常は次のようなメンバーで構成します。

  • RGB 値(赤、緑、青)
  • 16 進数コードの表現
  • 色に関連付けられた名前または説明

このデータを一貫した形式でメッセージに含めることで、LLM に適切に回答するために必要なすべての情報が確実に提供されます。

LLM コンテキスト同期の幅広いアプリケーション

UI イベントについて LLM に通知するこのパターンは、色選択以外にも多くの用途があります。

その他のユースケース

  1. フィルタの変更: ユーザーがデータにフィルタを適用したときに LLM に通知する
  2. ナビゲーション イベント: ユーザーが別のセクションに移動したときに LLM に通知します。
  3. 選択の変更: ユーザーがリストまたはグリッドからアイテムを選択したときに LLM を更新します。
  4. 設定の更新: ユーザーが設定を変更したときに LLM に通知する
  5. データ操作: ユーザーがデータを追加、編集、削除したときに LLM に通知する

いずれの場合も、パターンは同じです。

  1. UI イベントを検出する
  2. 関連データをシリアル化する
  3. 特別な形式の通知を LLM に送信する
  4. システム プロンプトを通じて LLM が適切に回答するようにガイドする

LLM コンテキスト同期のベスト プラクティス

実装に基づいて、LLM コンテキストの同期を効果的に行うためのベスト プラクティスを以下に示します。

1. 一貫性のある形式

LLM が通知を簡単に識別できるように、通知に一貫した形式を使用します。

"User [action] [object]: [structured data]"

2. 豊富なコンテキスト

LLM がインテリジェントに回答できるように、通知に十分な詳細情報を含めます。色の場合は、RGB 値、16 進数コード、その他の関連するプロパティです。

3. 明確な指示

通知の処理方法について、システム プロンプトで明確な指示を示す(例を示すのが理想的)。

4. 自然な統合

技術的な中断ではなく、会話の流れに沿って自然に表示される通知を設計します。

5. 選択通知

会話に関連するアクションのみを LLM に通知します。すべての UI イベントを通知する必要はありません。

トラブルシューティング

通知に関する問題

LLM が色選択に正しく応答しない場合は、次の手順を行います。

  • 通知メッセージの形式がシステム プロンプトに記載されている内容と一致していることを確認する
  • カラーデータが適切にシリアル化されていることを確認する
  • 選択内容を処理するための明確な手順がシステム プロンプトに表示されている
  • 通知の送信時にチャット サービスにエラーがないか確認する

コンテキスト管理

LLM がコンテキストを失っていると思われる場合:

  • チャット セッションが適切に維持されていることを確認する
  • 会話の状態が正しく遷移することを確認する
  • 通知が同じチャット セッションで送信されていることを確認する

全般的な問題

一般的な問題の場合:

  • ログでエラーや警告を確認する
  • Vertex AI in Firebase の接続を確認する
  • 関数パラメータの型の不一致を確認する
  • Riverpod によって生成されたすべてのコードが最新であることを確認する

学んだ主なコンセプト

  • UI と LLM 間の LLM コンテキスト同期を作成する
  • UI イベントを LLM に適したコンテキストにシリアル化する
  • さまざまなインタラクション パターンに対する LLM の動作のガイダンス
  • メッセージとメッセージ以外のインタラクション全体にわたる統一されたエクスペリエンスの作成
  • LLM による広範なアプリケーション状態の認識の強化

LLM コンテキスト同期を実装することで、LLM が単なるテキスト生成ツールではなく、意識があり、応答性の高いアシスタントのように感じられる、真に統合されたエクスペリエンスを実現できます。このパターンは、他の無数のアプリケーションにも適用でき、より自然で直感的な AI を活用したインターフェースを作成できます。

9. お疲れさまでした

これで、Colorist Codelab は終了です。🎉

作成した内容

Google の Gemini API を統合し、自然言語による色の説明を解釈する、完全な機能を持つ Flutter アプリケーションを作成しました。アプリで次のことが可能になりました。

  • 「夕日オレンジ」や「深い海の青」などの自然言語による説明を処理する
  • Gemini を使用して、これらの説明を RGB 値にインテリジェントに変換する
  • ストリーミング レスポンスで解釈された色をリアルタイムで表示する
  • チャット要素と UI 要素の両方を通じてユーザー操作を処理する
  • さまざまなインタラクション メソッドでコンテキスト認識を維持する

次のステップ

Gemini と Flutter の統合の基本を学んだので、次は次の方法で学習を進めることができます。

Colorist アプリを強化する

  • カラーパレット: 補色または一致する配色を生成する機能を追加
  • 音声入力: 音声認識を統合して、色を口頭で説明できるようにする
  • 履歴管理: 色セットの名前の変更、整理、エクスポートのオプションを追加
  • カスタム プロンプト: ユーザーがシステム プロンプトをカスタマイズできるインターフェースを作成する
  • 高度な分析: 効果的な説明と問題のある説明をトラッキングする

Gemini のその他の機能を確認する

  • マルチモーダル入力: 画像入力を追加して写真から色を抽出する
  • コンテンツの生成: Gemini を使用して、説明やストーリーなど、色に関連するコンテンツを生成します。
  • 関数呼び出しの機能強化: 複数の関数を使用してより複雑なツール統合を作成
  • 安全設定: さまざまな安全設定と、それらが回答に与える影響を確認する

これらのパターンを他のドメインに適用する

  • ドキュメント分析: ドキュメントを理解して分析できるアプリを作成する
  • 創造的な文章作成支援: LLM を活用した候補を使用して文章作成ツールを構築する
  • タスクの自動化: 自然言語を自動化されたタスクに変換するアプリを設計する
  • 知識ベース アプリケーション: 特定のドメインのエキスパート システムを作成します。

リソース

学習を続けるうえで役立つリソースを以下にご紹介します。

公式ドキュメント

プロンプト コースとガイド

コミュニティ

フィードバック

この Codelab のご利用について、ぜひご意見をお聞かせください。フィードバックをお寄せいただける場合は、以下の方法をご利用ください。

この Codelab を完了していただきありがとうございました。Flutter と AI の組み合わせによるエキサイティングな可能性を今後も探求していただければ幸いです。