建構 Gemini 輔助的 Flutter 應用程式

1. 建構 Gemini 輔助的 Flutter 應用程式

建構項目

在本程式碼研究室中,您將建構 Colorist,這是一個互動式 Flutter 應用程式,可將 Gemini API 的強大功能直接帶入您的 Flutter 應用程式。您是否曾想讓使用者透過自然語言控制應用程式,但不知道從何處著手?本程式碼研究室會說明如何操作。

使用者可以透過自然語言 (例如「夕陽的橘色」或「深海藍色」) 描述顏色,應用程式會:

  • 使用 Google 的 Gemini API 處理這些說明
  • 將說明解讀為精確的 RGB 顏色值
  • 即時在螢幕上顯示顏色
  • 提供技術色彩詳細資料和有趣的色彩背景資訊
  • 保留最近產生的顏色記錄

顯示色彩顯示和聊天介面的 Colorist 應用程式螢幕截圖

應用程式採用分割畫面介面,一側顯示彩色顯示區域和互動式即時通訊系統,另一側則顯示詳細的記錄面板,顯示原始 LLM 互動情形。這份記錄可讓您進一步瞭解 LLM 整合功能的實際運作方式。

這對 Flutter 開發人員有何重要性

LLM 徹底改變了使用者與應用程式互動的方式,但要有效地將 LLM 整合至行動和電腦版應用程式,則會面臨獨特的挑戰。本程式碼研究室會教導您實用的模式,而非僅限於原始 API 呼叫。

您的學習歷程

本程式碼實驗室會逐步引導您建構 Colorist:

  1. 專案設定:您將從基本 Flutter 應用程式結構和 colorist_ui 套件開始
  2. 基本 Gemini 整合:將應用程式連結至 Firebase AI Logic,並實作 LLM 通訊
  3. 有效提示:建立系統提示,引導 LLM 瞭解顏色說明
  4. 函式宣告:定義 LLM 可用於在應用程式中設定顏色的工具
  5. 工具處理:處理大型語言模型的函式呼叫,並將這些呼叫連結至應用程式的狀態
  6. 串流回覆:透過即時串流 LLM 回覆提升使用者體驗
  7. LLM 內容同步:向 LLM 告知使用者動作,打造一致的體驗

課程內容

  • 為 Flutter 應用程式設定 Firebase AI Logic
  • 撰寫有效的系統提示,引導 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)'),
  },
);

本程式碼研究室的影片總覽

請觀看 Craig Labenz 和 Andrew Brogdon 在 Observable Flutter 節目第 59 集討論這個程式碼研究室:

必要條件

如要充分運用本程式碼研究室,您應具備下列條件:

  • Flutter 開發經驗:熟悉 Flutter 基礎知識和 Dart 語法
  • 非同步程式設計知識:瞭解 Future、async/await 和串流
  • Firebase 帳戶:您需要 Google 帳戶才能設定 Firebase

讓我們開始建構第一個採用 LLM 的 Flutter 應用程式吧!

2. 專案設定與回音服務

在這個第一步中,您將設定專案結構,並實作回音服務,這項服務稍後會改為 Gemini API 整合服務。這麼做可建立應用程式架構,並確保 UI 正常運作,再加入 LLM 呼叫的複雜性。

本步驟的學習重點

  • 設定含有必要依附元件的 Flutter 專案
  • 使用 colorist_ui 套件處理 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:結構化記錄
  • 用於程式碼產生和 linting 的開發依附元件

您的 pubspec.yaml 會類似如下:

pubspec.yaml

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

environment:
  sdk: ^3.8.0

dependencies:
  flutter:
    sdk: flutter
  colorist_ui: ^0.2.4
  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.5
  custom_lint: ^0.7.5

flutter:
  uses-material-design: true

設定分析選項

custom_lint 新增至專案根層級的 analysis_options.yaml 檔案:

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);
  }
}

這會設定 Flutter 應用程式實作回音服務,藉由傳回使用者的訊息模擬 LLM 的行為。

瞭解架構

請花點時間瞭解 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 整合:會在後續步驟中加入

這樣一來,您就能專注於實作 LLM 整合,而 UI 元件則已妥善處理。

執行應用程式

使用下列指令執行應用程式:

flutter run -d DEVICE

DEVICE 替換為目標裝置,例如 macoswindowschrome 或裝置 ID。

Colorist 應用程式螢幕截圖,顯示回音服務算繪 Markdown

你現在應該會看到 Colorist 應用程式,其中包含:

  1. 使用預設顏色的彩色顯示區
  2. 可輸入訊息的聊天介面
  3. 顯示即時通訊互動的記錄面板

輸入「我想要深藍色」之類的訊息,然後按下「傳送」。回音服務只會重複您的訊息。在後續步驟中,您將使用 Firebase AI 邏輯,將這項操作替換為實際的顏色解讀。

後續步驟

在下一個步驟中,您將設定 Firebase 並實作基本 Gemini API 整合,以便以 Gemini 即時通訊服務取代 echo 服務。這樣一來,應用程式就能解讀顏色說明並提供智慧回覆。

疑難排解

UI 套件問題

如果您在使用 colorist_ui 套件時遇到問題,請按照下列步驟操作:

  • 確認你使用的是最新版本
  • 確認已正確新增依附元件
  • 檢查是否有任何衝突的套件版本

建構錯誤

如果您看到建構錯誤,請採取下列行動:

  • 請確認您已安裝最新的穩定版 Flutter SDK
  • 執行 flutter clean,接著執行 flutter pub get
  • 檢查控制台輸出內容,查看特定錯誤訊息

學習的重要概念

  • 設定具有必要依附元件的 Flutter 專案
  • 瞭解應用程式的架構和元件職責
  • 實作模擬 LLM 行為的簡易服務
  • 將服務連結至 UI 元件
  • 使用 Riverpod 管理狀態

3. 基本 Gemini 即時通訊整合

在這個步驟中,您將使用 Firebase AI Logic 將前一個步驟的回音服務替換為 Gemini API 整合。您將設定 Firebase、設定必要的供應器,並實作與 Gemini API 通訊的基本即時通訊服務。

本步驟的學習重點

  • 在 Flutter 應用程式中設定 Firebase
  • 設定 Firebase AI Logic 以存取 Gemini
  • 為 Firebase 和 Gemini 服務建立 Riverpod 供應器
  • 使用 Gemini API 實作基本即時通訊服務
  • 處理非同步 API 回應和錯誤狀態

設定 Firebase

首先,您需要為 Flutter 專案設定 Firebase。這包括建立 Firebase 專案、將應用程式加入專案,以及設定必要的 Firebase AI 邏輯設定。

建立 Firebase 專案

  1. 前往 Firebase 控制台,然後使用 Google 帳戶登入。
  2. 按一下「建立 Firebase 專案」或選取現有專案。
  3. 按照設定精靈的指示建立專案。

在 Firebase 專案中設定 Firebase AI Logic

  1. 在 Firebase 主控台中前往所需專案。
  2. 在左側欄中選取「AI」
  3. 在 AI 下拉式選單中,選取「AI 邏輯」
  4. 在 Firebase AI 邏輯資訊卡中,選取「開始使用」
  5. 按照提示為專案啟用 Gemini Developer API。

安裝 FlutterFire CLI

FlutterFire CLI 可簡化 Flutter 應用程式中的 Firebase 設定:

dart pub global activate flutterfire_cli

將 Firebase 新增至 Flutter 應用程式

  1. 將 Firebase 核心和 Firebase AI 邏輯套件新增至專案:
flutter pub add firebase_core firebase_ai
  1. 執行 FlutterFire 設定指令:
flutterfire configure

這個指令會:

  • 要求您選取剛剛建立的 Firebase 專案
  • 透過 Firebase 註冊 Flutter 應用程式
  • 使用專案設定產生 firebase_options.dart 檔案

這項指令會自動偵測所選平台 (iOS、Android、macOS、Windows、網頁),並進行適當設定。

平台專屬設定

Firebase 的最低版本需求高於 Flutter 的預設版本。您也必須具備網路存取權,才能與 Firebase AI Logic 伺服器通訊。

設定 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_ai/firebase_ai.dart';
import 'package:firebase_core/firebase_core.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 = FirebaseAI.googleAI().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();
}

這個檔案會定義三個主要供應者的基礎。當您透過 Riverpod 程式碼產生器執行 dart run build_runner 時,系統會產生這些供應器。

  1. firebaseAppProvider:使用專案設定初始化 Firebase
  2. geminiModelProvider:建立 Gemini 生成式模型例項
  3. chatSessionProvider:建立並維護與 Gemini 模型的對話階段

聊天工作階段上的 keepAlive: true 註解可確保在整個應用程式生命週期中持續保留,維持對話內容。

實作 Gemini 即時通訊服務

建立新檔案 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_ai/firebase_ai.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 檔案

更新 lib/main.dart 檔案,以便使用新的 Gemini 即時通訊服務:

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. 使用 Riverpod 的 AsyncValue 模式和 when 方法,新增載入和錯誤畫面
  3. 透過 sendMessage 回呼將 UI 連結至新的即時通訊服務

執行應用程式

使用下列指令執行應用程式:

flutter run -d DEVICE

DEVICE 替換為目標裝置,例如 macoswindowschrome 或裝置 ID。

Colorist 應用程式螢幕截圖,顯示 Gemini LLM 回應要求的陽光黃色

從現在起,當您輸入訊息時,系統會將訊息傳送至 Gemini API,您會收到 LLM 的回覆,而非回音。記錄面板會顯示與 API 的互動情形。

瞭解 LLM 通訊

請花點時間瞭解與 Gemini API 通訊時會發生什麼事:

通訊流程

  1. 使用者輸入內容:使用者在聊天介面中輸入文字
  2. 要求格式:應用程式會將文字格式化為 Gemini API 的 Content 物件
  3. API 通訊:系統會透過 Firebase AI Logic 將文字傳送至 Gemini API
  4. 大型語言模型處理:Gemini 模型會處理文字並產生回覆
  5. 回應處理:應用程式會接收回應並更新 UI
  6. 記錄:為了資訊公開,所有通訊內容都會記錄

即時通訊工作階段和對話情境

Gemini 聊天工作階段會保留訊息之間的脈絡,方便進行對話互動。也就是說,LLM 會「記住」目前工作階段中的先前互動,讓對話更連貫。

聊天工作階段供應器上的 keepAlive: true 註解可確保此內容在應用程式生命週期中持續存在。這個持續性背景資訊對於維持與大型語言模型的自然對話流程至關重要。

後續步驟

目前,您可以向 Gemini API 提出任何問題,因為系統不會限制回應內容。舉例來說,你可以要求系統提供玫瑰戰爭的摘要,但這與顏色應用程式的用途無關。

在下一個步驟中,您將建立系統提示,引導 Gemini 更有效地解讀顏色說明。本節將說明如何根據應用程式需求自訂 LLM 行為,並將其功能集中在應用程式網域上。

疑難排解

Firebase 設定問題

如果您在 Firebase 初始化時遇到錯誤,請按照下列步驟操作:

  • 確認 firebase_options.dart 檔案已正確產生
  • 確認您已升級至 Blaze 方案,以便存取 Firebase AI Logic

API 存取錯誤

如果您在存取 Gemini API 時收到錯誤訊息,請按照下列步驟操作:

  • 確認 Firebase 專案已正確設定結帳資訊
  • 確認 Firebase 專案已啟用 Firebase AI Logic 和 Cloud AI API
  • 檢查網路連線和防火牆設定
  • 確認模型名稱 (gemini-2.0-flash) 正確且可用

對話情境問題

如果您發現 Gemini 沒有記住對話中的先前內容,請按照下列步驟操作:

  • 確認 chatSession 函式已加上 @Riverpod(keepAlive: true) 註解
  • 確認您是否針對所有訊息交換作業重複使用同一個即時通訊工作階段
  • 確認即時通訊工作階段已正確初始化,再傳送訊息

平台專屬問題

如有特定平台的問題:

  • iOS/macOS:確保已設定適當的授權,並設定最低版本
  • Android:確認已正確設定 SDK 最低版本
  • 在控制台中查看平台專屬錯誤訊息

學習的重要概念

  • 在 Flutter 應用程式中設定 Firebase
  • 設定 Firebase AI Logic 以存取 Gemini
  • 為非同步服務建立 Riverpod 供應器
  • 實作與大型語言模型通訊的聊天服務
  • 處理非同步 API 狀態 (載入、錯誤、資料)
  • 瞭解大型語言模型通訊流程和即時通訊工作階段

4. 有效提示顏色說明

在這個步驟中,您將建立並實作系統提示,引導 Gemini 解讀顏色說明。系統提示是一種強大的功能,可讓您針對特定工作自訂 LLM 行為,而無須變更程式碼。

本步驟的學習重點

  • 瞭解系統提示,以及在 LLM 應用程式中的重要性
  • 針對特定領域任務設計有效的提示
  • 在 Flutter 應用程式中載入及使用系統提示
  • 引導大型語言模型提供一致格式的回覆
  • 測試系統提示對 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 底部,加入資產目錄:

pubspec.yaml

flutter:
  uses-material-design: true

  assets:
    - assets/

執行 flutter pub get 即可重新整理資產 Bundle。

建立系統提示供應器

建立新檔案 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_ai/firebase_ai.dart';
import 'package:firebase_core/firebase_core.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 = FirebaseAI.googleAI().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)。這會指示 Gemini 在這個即時通訊工作階段中,將您的指示用作系統提示。

產生 Riverpod 程式碼

執行 build runner 指令,產生所需的 Riverpod 程式碼:

dart run build_runner build --delete-conflicting-outputs

執行及測試應用程式

現在請執行應用程式:

flutter run -d DEVICE

Colorist 應用程式螢幕截圖:顯示 Gemini LLM 回應顏色選取應用程式中的字元

請嘗試使用各種顏色描述進行測試:

  • 「我想要天藍色」
  • 「給我一個森林綠」
  • 「製作鮮豔的夕陽橘色」
  • 「I want the color of fresh lavender」(我想訂新鮮薰衣草的顏色)
  • 「Show me something like a deep ocean blue」

你應該會發現,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 頻率限制

如果遇到與速率限制相關的錯誤,請按照下列步驟操作:

  • 請注意,Firebase AI Logic 服務有使用限制
  • 考慮以指數輪詢方式實作重試邏輯
  • 查看 Firebase 控制台是否有任何配額問題

學習的重要概念

  • 瞭解系統提示在 LLM 應用程式中的角色和重要性
  • 設計有效的提示,提供明確的指示、範例和限制
  • 在 Flutter 應用程式中載入及使用系統提示
  • 引導 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 應用程式中,函式呼叫可讓使用者說出「我想要森林綠」,並讓 UI 立即更新該顏色,而不需要從文字中剖析 RGB 值。

定義函式宣告

建立新的檔案 lib/services/gemini_tools.dart 來定義函式宣告:

lib/services/gemini_tools.dart

import 'package:firebase_ai/firebase_ai.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_ai/firebase_ai.dart';
import 'package:firebase_core/firebase_core.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 = FirebaseAI.googleAI().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 瞭解可供呼叫的函式。

更新系統提示

您現在需要修改系統提示,指示 LLM 使用新的 set_color 工具。更新 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. 工具介紹:您現在可以向 LLM 說明 set_color 工具,而非要求格式化的 RGB 值
  2. 修改後的程序:您將步驟 3 從「在回覆中格式化值」變更為「使用工具設定值」
  3. 更新範例:您說明回應應如何包含工具呼叫,而非格式化文字
  4. 移除格式規定:由於您使用的是結構化函式呼叫,因此不再需要特定的文字格式

這個更新的提示會引導 LLM 使用函式呼叫,而非僅以文字形式提供 RGB 值。

產生 Riverpod 程式碼

執行 build runner 指令,產生所需的 Riverpod 程式碼:

dart run build_runner build --delete-conflicting-outputs

執行應用程式

此時,Gemini 會產生嘗試使用函式呼叫的內容,但您尚未為函式呼叫導入處理常式。執行應用程式並描述顏色時,Gemini 會回應,就像已叫用工具一樣,但您不會在 UI 中看到任何顏色變更,直到下一個步驟才會看到。

執行應用程式:

flutter run -d DEVICE

Colorist 應用程式螢幕截圖,顯示 Gemini LLM 回覆部分內容

嘗試描述顏色,例如「深海藍」或「森林綠」,並觀察回應。LLM 會嘗試呼叫上述定義的函式,但您的程式碼尚未偵測到函式呼叫。

函式呼叫程序

讓我們瞭解 Gemini 使用函式呼叫時會發生什麼事:

  1. 函式選取:大型語言模型會根據使用者的要求,決定是否呼叫函式
  2. 參數產生:LLM 會產生符合函式結構定義的參數值
  3. 函式呼叫格式:大型語言模型會在回應中傳送結構化函式呼叫物件
  4. 應用程式處理:應用程式會接收這個呼叫,並執行相關函式 (在下一個步驟中實作)
  5. 回覆整合:在多輪對話中,大型語言模型會預期函式的結果會傳回

在應用程式的目前狀態中,前三個步驟正在執行,但您尚未實作步驟 4 或 5 (處理函式呼叫),我們會在下一個步驟中說明如何執行這兩個步驟。

技術細節:Gemini 如何決定使用函式

Gemini 會根據下列資訊,判斷何時使用函式:

  1. 使用者意圖:使用者要求是否最適合由函式處理
  2. 功能相關性:可用的函式與任務的匹配程度
  3. 參數可用性:是否能確定參數值
  4. 系統指示:系統提示提供的功能使用說明

您提供明確的函式宣告和系統指示,讓 Gemini 在辨識顏色說明要求時,能呼叫 set_color 函式。

後續步驟

在下一個步驟中,您將為 Gemini 的函式呼叫實作處理程序。這樣一來,使用者說明就能透過 LLM 的函式呼叫,在 UI 中觸發實際的顏色變更。

疑難排解

函式宣告問題

如果您在函式宣告中遇到錯誤,請按照下列步驟操作:

  • 確認參數名稱和類型符合預期
  • 確認函式名稱清晰且具描述性
  • 確保功能說明如實說明其用途

系統提示問題

如果 LLM 未嘗試使用函式:

  • 確認系統提示清楚指示 LLM 使用 set_color 工具
  • 確認系統提示中的範例說明函式用法
  • 請嘗試讓使用工具的操作說明更明確

一般問題

如果遇到其他問題,請按照下列步驟操作:

  • 檢查控制台,找出與函式宣告相關的任何錯誤
  • 確認工具是否已正確傳遞至模型
  • 確保所有 Riverpod 產生的程式碼皆為最新版本

學習的重要概念

  • 定義函式宣告,以擴充 Flutter 應用程式中的 LLM 功能
  • 為結構化資料收集作業建立參數結構定義
  • 將函式宣告與 Gemini 模型整合
  • 更新系統提示,鼓勵使用功能
  • 瞭解大型語言模型如何選取及呼叫函式

這個步驟說明大型語言模型如何彌補自然語言輸入內容與結構化函式呼叫之間的差距,為對話和應用程式功能的無縫整合奠定基礎。

6. 實作工具處理

在這個步驟中,您將為 Gemini 的函式呼叫實作處理程序。這樣一來,自然語言輸入內容和具體應用程式功能之間的溝通循環就會完成,讓大型語言模型能夠根據使用者說明直接操控 UI。

本步驟的學習重點

  • 瞭解大型語言模型應用程式中的完整函式呼叫管道
  • 在 Flutter 應用程式中處理 Gemini 的函式呼叫
  • 實作可修改應用程式狀態的函式處理常式
  • 處理函式回應並將結果傳回大型語言模型
  • 建立 LLM 與 UI 之間的完整通訊流程
  • 記錄函式呼叫和回應,以便公開資訊

瞭解函式呼叫管道

在深入瞭解實作方式前,讓我們先瞭解完整的函式呼叫管道:

端對端流程

  1. 使用者輸入內容:使用者以自然語言描述顏色 (例如「森林綠」)
  2. 大型語言模型處理:Gemini 會分析說明,並決定是否呼叫 set_color 函式
  3. 函式呼叫產生:Gemini 會建立含有參數 (紅、綠、藍值) 的結構化 JSON
  4. 函式呼叫接收:應用程式會從 Gemini 接收這類結構化資料
  5. 函式執行:應用程式會使用提供的參數執行函式
  6. 狀態更新:函式會更新應用程式的狀態 (變更顯示的顏色)
  7. 回覆產生:函式會將結果傳回大型語言模型
  8. 回覆整合:大型語言模型會將這些結果整合至最終回覆
  9. 使用者介面更新:使用者介面會根據狀態變更顯示新顏色

完整的通訊週期對於正確整合 LLM 至關重要。當 LLM 進行函式呼叫時,不會只是傳送要求並繼續執行。而是等待應用程式執行函式並傳回結果。接著,LLM 會使用這些結果擬定最終回覆,建立自然的對話流程,確認已採取的動作。

實作函式處理常式

讓我們更新 lib/services/gemini_tools.dart 檔案,為函式呼叫新增處理常式:

lib/services/gemini_tools.dart

import 'package:colorist_ui/colorist_ui.dart';
import 'package:firebase_ai/firebase_ai.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:中央調度器,可執行以下操作:
    • 在記錄面板中記錄透明度函式呼叫
    • 根據函式名稱轉送至適當的處理常式
    • 傳回結構化回應,並傳回大型語言模型
  2. handleSetColorset_color 函式的特定處理常式,可執行以下操作:
    • 從引數對應區域擷取 RGB 值
    • 將這些值轉換為預期的類型 (double)
    • 使用 colorStateNotifier 更新應用程式的顏色狀態
    • 建立含有成功狀態和目前顏色資訊的結構化回應
    • 記錄函式結果以便偵錯
  3. handleUnknownFunction:不明函式的備用處理常式,具備下列功能:
    • 記錄警告,指出不支援的函式
    • 傳回錯誤回應給 LLM

handleSetColor 函式特別重要,因為它可彌補 LLM 的自然語言理解與具體 UI 變更之間的差距。

更新 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_ai/firebase_ai.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. 檢查大型語言模型回應是否包含任何函式呼叫
  2. 針對每個函式呼叫,使用函式名稱和引數叫用 handleFunctionCall 方法
  3. 收集每個函式呼叫的結果
  4. 使用 Content.functionResponses 將這些結果傳回大型語言模型
  5. 處理 LLM 對函式結果的回應
  6. 根據最終回覆文字更新 UI

這會建立往返流程:

  • 使用者 → LLM:要求顏色
  • LLM → 應用程式:含有參數的函式呼叫
  • 應用程式 → 使用者:顯示新顏色
  • 應用程式 → LLM:函式結果
  • LLM → 使用者:最終回覆,內含函式結果

產生 Riverpod 程式碼

執行 build runner 指令,產生所需的 Riverpod 程式碼:

dart run build_runner build --delete-conflicting-outputs

執行並測試完整流程

現在請執行應用程式:

flutter run -d DEVICE

Colorist 應用程式螢幕截圖,顯示 Gemini 大型語言模型透過函式呼叫回應

請嘗試輸入各種顏色說明:

  • 「我想要深猩紅色」
  • 「請顯示令人平靜的淺藍色」
  • 「給我新鮮薄荷葉的顏色」
  • 「I want to see a warm sunset orange」(我想看暖色調的橘色)
  • 「將其變成濃郁的皇家紫色」

您現在應該會看到:

  1. 訊息顯示在即時通訊介面
  2. Gemini 的回覆會顯示在即時通訊中
  3. 記錄在「Logs」面板中的函式呼叫
  4. 函式結果會在以下情況下立即記錄:
  5. 色塊更新為顯示所述顏色
  6. RGB 值更新,以顯示新顏色的元件
  7. Gemini 的最終回覆,通常會針對設定的顏色發表評論

記錄面板可讓您深入瞭解幕後發生的情況。您會看到:

  • Gemini 所做的確切函式呼叫
  • 為每個 RGB 值選擇的參數
  • 函式傳回的結果
  • Gemini 的後續回覆

顏色狀態通知器

您用來更新顏色的 colorStateNotifiercolorist_ui 套件的一部分。管理項目包括:

  • 目前在 UI 中顯示的顏色
  • 顏色記錄 (最近 10 種顏色)
  • 通知 UI 元件的狀態變更

當您使用新的 RGB 值呼叫 updateColor 時,系統會執行以下操作:

  1. 使用提供的值建立新的 ColorData 物件
  2. 更新應用程式狀態中的目前顏色
  3. 為記錄新增顏色
  4. 透過 Riverpod 的狀態管理觸發 UI 更新

colorist_ui 套件中的 UI 元件會監控此狀態,並在狀態變更時自動更新,藉此打造回應式體驗。

瞭解錯誤處理

實作內容應包含完善的錯誤處理機制:

  1. Try-catch 區塊:包裝所有 LLM 互動,以便擷取任何例外狀況
  2. 記錄錯誤:在記錄面板中記錄含有堆疊追蹤的錯誤
  3. 使用者意見回饋:在即時通訊中提供友善的錯誤訊息
  4. 狀態清理:即使發生錯誤,也能完成訊息狀態

這可確保應用程式保持穩定,並在 LLM 服務或執行功能時發生問題時提供適當的意見回饋。

函式呼叫對使用者體驗的影響

您在這裡完成的任務,說明瞭大型語言模型如何建立強大的自然介面:

  1. 自然語言介面:使用者可用日常用語表達意圖
  2. 智慧解讀:LLM 會將含糊的說明轉譯為精確的值
  3. 直接操控:UI 會根據自然語言更新
  4. 情境回應:LLM 會提供變更內容的對話情境
  5. 低認知負荷:使用者不需要瞭解 RGB 值或色彩理論

這種使用大型語言模型函式呼叫來連結自然語言和 UI 動作的模式,可擴展至顏色選擇以外的無數其他領域。

後續步驟

在下一個步驟中,您將實作串流回應,提升使用者體驗。您不必等待完整回應,而是可在收到文字區塊和函式呼叫時立即處理,打造更有回應性且更吸引人的應用程式。

疑難排解

函式呼叫問題

如果 Gemini 未呼叫函式或參數有誤:

  • 確認函式宣告與系統提示中所述內容相符
  • 檢查參數名稱和類型是否一致
  • 請確認系統提示明確指示 LLM 使用該工具
  • 確認處理程序中的函式名稱與宣告中的函式名稱完全一致
  • 檢查記錄面板,瞭解函式呼叫的詳細資訊

函式回應問題

如果函式結果未正確傳回至 LLM:

  • 檢查函式是否傳回格式正確的 Map
  • 確認 Content.functionResponses 是否正確建構
  • 在記錄中尋找與函式回應相關的任何錯誤
  • 請確認您使用的是相同的即時通訊工作階段

色彩顯示問題

如果無法正常顯示顏色,請按照下列步驟操作:

  • 確保 RGB 值已正確轉換為雙精度值 (LLM 可能會將這些值傳送為整數)
  • 確認值是否在預期範圍內 (0.0 到 1.0)
  • 確認是否正確呼叫顏色狀態通知器
  • 檢查記錄檔,找出傳遞至函式的確切值

一般問題

一般問題:

  • 檢查記錄中的錯誤或警告
  • 驗證 Firebase AI Logic 連線
  • 檢查函式參數中的類型是否不符
  • 確保所有 Riverpod 產生的程式碼皆為最新版本

學習的重要概念

  • 在 Flutter 中實作完整的函式呼叫管道
  • 在 LLM 與應用程式之間建立完整通訊
  • 處理 LLM 回覆中的結構化資料
  • 將函式結果傳回大型語言模型,以便納入回應
  • 使用記錄面板查看 LLM 與應用程式的互動情形
  • 將自然語言輸入內容連結至具體的 UI 變更

完成這個步驟後,您的應用程式現在會示範 LLM 整合最強大的模式之一:將自然語言輸入內容轉譯為具體 UI 動作,同時維持連貫的對話,確認這些動作。這麼做可建立直覺式對話介面,讓使用者有如身歷其境。

7. 逐句顯示回覆,提供更佳的使用者體驗

在這個步驟中,您將透過實作 Gemini 的串流回覆,提升使用者體驗。您不必等待整個回應產生,而是在收到文字區塊和函式呼叫時立即處理,藉此打造更有回應性且更吸引人的應用程式。

本步驟的內容

  • 為採用 LLM 的應用程式串流傳輸的重要性
  • 在 Flutter 應用程式中實作串流 LLM 回覆
  • 處理從 API 傳送的部分文字區塊
  • 管理對話狀態,避免訊息衝突
  • 在串流回應中處理函式呼叫
  • 為進行中的回應建立視覺指標

為何串流對 LLM 應用程式十分重要

在導入前,請先瞭解為何串流回覆對於打造出色的 LLM 使用者體驗至關重要:

改善使用者體驗

串流回應可提供幾項重要的使用者體驗優勢:

  1. 減少感知延遲時間:使用者會立即看到文字開始顯示 (通常在 100 到 300 毫秒內),而不需要等待幾秒鐘才能看到完整回應。這種即時感覺大幅提升了使用者滿意度。
  2. 自然的對話節奏:文字會逐漸顯示,模仿人類的溝通方式,打造更自然的對話體驗。
  3. 逐步處理資訊:使用者可在資訊到達時開始處理,而不會一次看到大量文字而感到不知所措。
  4. 提早中斷的機會:在完整應用程式中,如果使用者發現 LLM 的方向不正確,可能會中斷或重新導向 LLM。
  5. 活動視覺確認:串流文字會立即提供系統運作情形的即時回饋,減少不確定性。

技術優勢

除了改善使用者體驗,串流播放還提供以下技術福利:

  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_ai/firebase_ai.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
    • 每個區塊都可能包含文字、函式呼叫或兩者皆有
  3. 漸進式處理
    • await for 會在每個區塊即時到達時處理
    • 文字會立即附加至 UI,產生串流效果
    • 只要系統偵測到函式呼叫,就會立即執行
  4. 函式呼叫處理
    • 當系統在區塊中偵測到函式呼叫時,就會立即執行
    • 結果會透過另一個串流呼叫傳回大型語言模型
    • 大型語言模型對這些結果的回應也會以串流方式處理
  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),
      ),
    );
  }
}

這裡的主要變更是將 conversationState 傳遞至 MainScreen 小工具。MainScreen (由 colorist_ui 套件提供) 會使用這個狀態,在處理回應時停用文字輸入功能。

這麼做可打造一致的使用者體驗,讓使用者介面反映對話的目前狀態。

產生 Riverpod 程式碼

執行 build runner 指令,產生所需的 Riverpod 程式碼:

dart run build_runner build --delete-conflicting-outputs

執行及測試串流回應

執行應用程式:

flutter run -d DEVICE

色彩設計師應用程式螢幕截圖,顯示 Gemini LLM 以串流方式回應

接下來,請嘗試使用各種顏色說明測試串流行為。請嘗試使用以下說明:

  • 「請顯示黃昏時海洋的深藍綠色」
  • 「我想看到色彩鮮豔的珊瑚,讓我聯想到熱帶花卉」
  • 「建立像舊軍裝一樣的橄欖綠色」

串流技術流程詳細說明

我們來看看串流回應時會發生什麼事:

連線建立

呼叫 sendMessageStream() 時,會發生下列情況:

  1. 應用程式會建立與 Firebase AI Logic 服務的連線
  2. 使用者要求已傳送至服務
  3. 伺服器開始處理要求
  4. 串流連線會保持開啟狀態,以便傳輸區塊

分塊傳輸

當 Gemini 生成內容時,系統會透過串流傳送片段:

  1. 伺服器會在產生文字片段時傳送 (通常是幾個字詞或句子)
  2. Gemini 決定要進行函式呼叫時,會傳送函式呼叫資訊
  3. 函式呼叫後可能會出現其他文字片段
  4. 這項作業會持續進行,直到產生作業完成為止

漸進式處理

應用程式會逐漸處理每個區塊:

  1. 每個文字區塊都會附加至現有回覆
  2. 只要系統偵測到函式呼叫,就會立即執行
  3. UI 會即時更新,顯示文字和函式結果
  4. 追蹤狀態,顯示回應仍在串流傳輸

串流完成

生成完成後:

  1. 串流已由伺服器關閉
  2. await for 迴圈會自然結束
  3. 訊息已標示為完成
  4. 對話狀態會重新設為閒置
  5. UI 會更新,反映已完成的狀態

串流與非串流比較

為進一步瞭解串流的優點,我們將串流和非串流方法做個比較:

切面

非串流

串流

感知延遲

在完整回應準備好之前,使用者不會看到任何內容

使用者在幾毫秒內看到第一個字

使用者體驗

等待時間過長,接著文字突然顯示

自然且漸進式的文字顯示

狀態管理

更簡單 (訊息為待處理或完成)

更複雜 (訊息可能處於串流狀態)

函式執行

只有在收到完整回應後才會發生

發生於回應產生期間

實作複雜度

導入方式更簡單

需要額外的狀態管理

錯誤復原

全部或無回應

部分回應仍可能有用

程式碼複雜程度

較不複雜

由於需要處理串流,因此更為複雜

對於 Colorist 這類應用程式而言,串流的使用者體驗優勢遠大於實作複雜度,尤其是對於可能需要幾秒鐘才能產生的色彩解讀。

串流使用者體驗的最佳做法

在自己的大型語言模型應用程式中實作串流時,請考慮採用下列最佳做法:

  1. 明確的視覺指標:請務必提供明確的視覺提示,區分串流和完整訊息
  2. 輸入封鎖:在串流期間停用使用者輸入,以免發生多個重疊要求
  3. 錯誤復原:設計 UI,以便在串流中斷時妥善處理
  4. 狀態轉換:確保在閒置、串流和完成狀態之間流暢轉換
  5. 進度視覺化:考慮使用輕微的動畫或指標,顯示正在進行的處理作業
  6. 取消選項:在完整的應用程式中,提供讓使用者取消正在產生的內容的選項
  7. 函式結果整合:設計 UI 以處理在流程中顯示的函式結果
  8. 效能最佳化:在快速串流更新期間盡量減少 UI 重建作業

colorist_ui 套件會為您實作許多這些最佳做法,但這些做法對於任何串流 LLM 實作都是重要的考量重點。

後續步驟

在下一個步驟中,您將實作 LLM 同步處理功能,在使用者從歷史記錄中選取顏色時通知 Gemini。這樣一來,LLM 就能瞭解使用者對應用程式狀態所做的變更,進而提供更一致的體驗。

疑難排解

串流處理問題

如果遇到串流處理問題,請按照下列步驟操作:

  • 症狀:部分回應、缺少文字或突然中斷串流
  • 解決方案:檢查網路連線狀態,並確認程式碼中的異步/等待模式是否正確
  • 診斷:檢查記錄面板,找出與串流處理相關的錯誤訊息或警告
  • 修正:確保所有串流處理作業都使用 try/catch 區塊進行適當的錯誤處理

缺少函式呼叫

如果未在串流中偵測到函式呼叫,請執行下列操作:

  • 症狀:文字會顯示,但顏色不會更新,或記錄中沒有任何函式呼叫
  • 解決方法:確認系統提示的函式呼叫使用說明
  • 診斷:檢查記錄面板,瞭解是否收到函式呼叫
  • 修正方法:調整系統提示,更明確地指示 LLM 使用 set_color 工具

一般錯誤處理

如有其他問題:

  • 步驟 1:檢查記錄面板是否有錯誤訊息
  • 步驟 2:驗證 Firebase AI Logic 連線
  • 步驟 3:確認所有 Riverpod 產生的程式碼皆為最新版本
  • 步驟 4:檢查串流實作是否缺少任何 await 陳述式

學習的重要概念

  • 使用 Gemini API 實作串流回應,以便提供更即時的使用者體驗
  • 管理對話狀態,以便妥善處理串流互動
  • 處理即時文字和功能呼叫
  • 建立可在串流期間逐步更新的回應式 UI
  • 使用適當的非同步模式處理並行串流
  • 在串流回應期間提供適當的視覺回饋

透過導入串流功能,您大幅提升了 Colorist 應用程式的使用者體驗,打造出更即時、更引人入勝的介面,讓使用者有如與人對話般輕鬆自在。

8. 大型語言模型脈絡同步

在這個額外步驟中,您將在使用者從歷史記錄中選取顏色時,通知 Gemini 並實作 LLM 情境同步處理。這樣一來,大型語言模型就能瞭解使用者在介面中的動作,而非僅是他們明確傳達的訊息,進而打造出更一致的體驗。

本步驟的內容

  • 在 UI 和 LLM 之間建立 LLM 情境同步處理
  • 將 UI 事件序列化為大型語言模型可理解的內容
  • 根據使用者動作更新對話內容
  • 在不同互動方式中打造一致的體驗
  • 除了明確的即時通訊訊息外,提升 LLM 的脈絡意識

瞭解 LLM 情境同步

傳統的聊天機器人只會回應使用者明確傳送的訊息,因此當使用者透過其他方式與應用程式互動時,就會造成斷連。大型語言模型脈絡同步處理功能可解決這項限制:

大型語言模型情境同步處理的重要性

當使用者透過 UI 元素 (例如從瀏覽記錄中選取顏色) 與應用程式互動時,除非您明確告知,否則 LLM 無法得知發生了什麼事。大型語言模型情境同步:

  1. 維持脈絡:讓 LLM 瞭解所有相關的使用者動作
  2. 建立一致性:產生一致的體驗,讓 LLM 確認 UI 互動
  3. 提升智慧功能:讓 LLM 能適當回應所有使用者動作
  4. 改善使用者體驗:讓整個應用程式更具整合性和回應速度
  5. 減少使用者工作量:使用者不必手動解釋自己的 UI 動作

在 Colorist 應用程式中,當使用者從記錄中選取顏色時,您希望 Gemini 能確認這項操作,並針對所選顏色提供明智的意見,讓使用者感覺到這項服務是無縫且具備意識的助理。

更新 Gemini Chat 服務,接收顏色選取通知

首先,您會在 GeminiChatService 中新增方法,以便在使用者從歷史記錄中選取顏色時通知 LLM。更新 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_ai/firebase_ai.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 程式碼

執行 build runner 指令,產生所需的 Riverpod 程式碼:

dart run build_runner build --delete-conflicting-outputs

執行及測試 LLM 內容同步處理

執行應用程式:

flutter run -d DEVICE

Colorist 應用程式螢幕截圖,顯示 Gemini LLM 對色彩記錄中的選項做出回應

測試 LLM 內容同步處理作業包括:

  1. 首先,請在聊天室中描述幾種顏色,產生幾個顏色
    • 「Show me a vibrant purple」
    • 「我想要森林綠」
    • 「給我亮紅色」
  2. 然後按一下記錄列中其中一個色彩縮圖

您應該觀察:

  1. 所選顏色會顯示在主畫面
  2. 聊天室中顯示使用者訊息,指出所選顏色
  3. 大型語言模型會回應,確認選取項目並評論顏色
  4. 整個互動過程都很自然且一致

這樣一來,LLM 就能瞭解並適當回應直接訊息和 UI 互動,打造順暢的使用體驗。

LLM 情境同步處理的運作方式

讓我們一起來瞭解這項同步處理機制的技術細節:

資料流

  1. 使用者動作:使用者在歷史記錄列中點選某個顏色
  2. UI 事件MainScreen 小工具會偵測這項選取動作
  3. 回呼執行:觸發 notifyColorSelection 回呼
  4. 訊息建立:使用顏色資料建立格式特別的訊息
  5. LLM 處理:系統會將訊息傳送給 Gemini,讓 Gemini 辨識格式
  6. 情境回應:Gemini 會根據系統提示做出適當回應
  7. 使用者介面更新:回覆會顯示在即時通訊中,打造一致的體驗

資料序列化

此方法的關鍵部分是如何序列化顏色資料:

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

toLLMContextMap() 方法 (由 colorist_ui 套件提供) 會將 ColorData 物件轉換為 LLM 可理解的關鍵屬性地圖。這通常包括:

  • RGB 值 (紅、綠、藍)
  • 十六進位代碼表示法
  • 與顏色相關的任何名稱或說明

只要一律採用一致的格式,並將這些資料納入訊息中,就能確保 LLM 擁有所有必要資訊,以便做出適當回應。

更廣泛的 LLM 情境同步處理應用

這種向 LLM 通知 UI 事件的模式,除了顏色選取之外,還有許多應用方式:

其他使用情況

  1. 篩選器變更:在使用者對資料套用篩選器時通知 LLM
  2. 導覽事件:在使用者前往不同區塊時通知 LLM
  3. 選取項目變更:當使用者從清單或格狀檢視畫面中選取項目時,更新 LLM
  4. 偏好設定更新:在使用者變更設定或偏好設定時通知 LLM
  5. 資料操縱:在使用者新增、編輯或刪除資料時通知 LLM

無論是哪種情況,模式都會保持不變:

  1. 偵測 UI 事件
  2. 序列化相關資料
  3. 傳送特殊格式的通知給 LLM
  4. 透過系統提示引導 LLM 做出適當回應

大型語言模型內容同步的最佳做法

根據您的實作方式,以下是有效執行 LLM 內容同步處理的最佳做法:

1. 格式一致

請使用一致的通知格式,方便 LLM 辨識:

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

2. 豐富內容

在通知中加入足夠的詳細資料,讓 LLM 做出智慧回應。對於顏色,這表示 RGB 值、十六進位代碼和任何其他相關屬性。

3. 清楚的操作說明

在系統提示中提供明確的操作說明,說明如何處理通知,最好附上範例。

4. 自然整合

設計通知時,請讓通知自然地融入對話,而非技術上的中斷。

5. 選擇性通知

只將與對話相關的動作通知給大型語言模型。並非所有 UI 事件都需要傳送。

疑難排解

通知問題

如果 LLM 無法正確回應顏色選項:

  • 確認通知訊息格式符合系統提示中所述格式
  • 確認色彩資料是否正確序列化
  • 確保系統提示提供明確的操作選項指示
  • 傳送通知時,請留意即時通訊服務是否有任何錯誤

管理資訊脈絡

如果 LLM 似乎遺失了背景資訊:

  • 確認即時通訊會話是否正常進行
  • 確認對話狀態轉換正確
  • 確認通知是透過同一個即時通訊會話傳送

一般問題

一般問題:

  • 檢查記錄中的錯誤或警告
  • 驗證 Firebase AI Logic 連線
  • 檢查函式參數中的類型是否不符
  • 確保所有 Riverpod 產生的程式碼皆為最新版本

學習的重要概念

  • 在 UI 和 LLM 之間建立 LLM 內容同步處理
  • 將 UI 事件序列化為 LLM 友善的上下文
  • 針對不同互動模式引導 LLM 行為
  • 在訊息和非訊息互動中打造一致的體驗
  • 提升 LLM 對更廣泛應用程式狀態的瞭解

實作 LLM 內容同步處理後,您就能打造真正整合的體驗,讓 LLM 看起來像是能即時回應的智慧助理,而非單純的文字產生器。這個模式可套用至無數其他應用程式,以便打造更自然、直覺的 AI 技術輔助介面。

9. 恭喜!

您已成功完成 Colorist 程式碼研究室!🎉

建構內容

您已建立功能完整的 Flutter 應用程式,整合 Google 的 Gemini API 來解讀自然語言的顏色說明。您的應用程式現在可以:

  • 處理自然語言描述,例如「夕陽橙色」或「深海藍」
  • 使用 Gemini 將這些說明轉譯為 RGB 值
  • 透過串流回應即時顯示解析的顏色
  • 透過即時通訊和 UI 元素處理使用者互動
  • 在不同互動方式中維持情境意識

下一步該做什麼?

您現在已掌握將 Gemini 與 Flutter 整合的基本概念,接下來可以透過以下方式繼續學習:

強化 Colorist 應用程式

  • 調色盤:新增功能,產生互補或相配的配色方案
  • 語音輸入:整合語音辨識功能,以便口頭描述顏色
  • 記錄管理:新增選項,以便命名、整理及匯出色彩組合
  • 自訂提示:建立可讓使用者自訂系統提示的介面
  • 進階數據分析:追蹤哪些說明最有效或造成困擾

探索更多 Gemini 功能

  • 多模態輸入:新增圖片輸入內容,從相片中擷取顏色
  • 內容生成:使用 Gemini 生成顏色相關內容,例如說明或故事
  • 函式呼叫功能強化:使用多個函式建立更複雜的工具整合
  • 安全設定:瞭解各種安全設定及其對回覆的影響

將這些模式套用至其他網域

  • 文件分析:建立可理解及分析文件的應用程式
  • 創作輔助:建構採用 LLM 技術的建議功能,提供寫作輔助
  • 工作自動化:設計可將自然語言轉換為自動化工作流程的應用程式
  • 知識型應用程式:建立特定領域的專家系統

資源

以下是一些可供您繼續學習的實用資源:

官方說明文件

提示課程和指南

社群

可觀察的 Flutter Agentic 系列

在第 59 集的「程式碼研究室」中,Craig Labenz 和 Andrew Brogden 將探索這個程式碼研究室,並強調應用程式建構作業中的有趣部分。

在第 60 集,Craig 和 Andrew 將再次加入,為程式碼研究室應用程式擴充新功能,並努力讓 LLM 按照指示執行。

在第 61 集,Craig 與 Chris Sells 將共同分析新聞標題,並產生對應的圖片。

意見回饋

我們很樂意瞭解您使用這個程式碼研究室的體驗!歡迎透過以下管道提供意見回饋:

感謝您完成本程式碼研究室,希望您能繼續探索 Flutter 與 AI 交會的無限可能!