פיתוח אפליקציה מבוססת-Gemini ב-Flutter

1. יצירת אפליקציית Flutter שמבוססת על Gemini

מה תפַתחו

ב-codelab הזה תיצרו את Colorist – אפליקציית Flutter אינטראקטיבית שמביאה את העוצמה של Gemini API ישירות לאפליקציית Flutter שלכם. תמיד רציתם לאפשר למשתמשים לשלוט באפליקציה שלכם באמצעות שפה טבעית, אבל לא ידעתם מאיפה להתחיל? ב-codelab הזה מוסבר איך עושים את זה.

אפליקציית Colorist מאפשרת למשתמשים לתאר צבעים בשפה טבעית (למשל, "הכתום של שקיעה" או "כחול עמוק של האוקיינוס"), והאפליקציה:

  • מעבד את התיאורים האלה באמצעות Gemini API מבית Google
  • מפרש את התיאורים לערכי צבע מדויקים של RGB
  • הצגת הצבע במסך בזמן אמת
  • מספק פרטים טכניים על הצבע והקשר מעניין לגביו
  • שומר היסטוריה של צבעים שנוצרו לאחרונה

צילום מסך של אפליקציית Colorist שבו מוצגת תצוגת הצבעים וממשק הצ'אט

ממשק האפליקציה כולל מסך מפוצל עם אזור תצוגה צבעוני ומערכת צ'אט אינטראקטיבית בצד אחד, וחלונית יומן מפורטת שמציגה את האינטראקציות הגולמיות עם ה-LLM בצד השני. היומן הזה מאפשר לכם להבין טוב יותר איך שילוב של LLM באמת פועל מתחת לפני השטח.

למה זה חשוב למפתחי Flutter

מודלים גדולים של שפה (LLM) משנים את האופן שבו משתמשים מקיימים אינטראקציה עם אפליקציות, אבל שילוב יעיל שלהם באפליקציות לנייד ולמחשב מציב אתגרים ייחודיים. ב-codelab הזה נלמד דפוסים מעשיים שהם מעבר לקריאות API גולמיות.

תהליך הלמידה שלכם

בשיעור הזה תלמדו איך לפתח את Colorist שלב אחר שלב:

  1. הגדרת הפרויקט – מתחילים עם מבנה בסיסי של אפליקציית Flutter וחבילת colorist_ui
  2. שילוב בסיסי של Gemini – קישור האפליקציה ל-Firebase AI Logic והטמעה של תקשורת LLM
  3. יצירת הנחיות יעילות – יוצרים הנחיה למערכת שמנחה את ה-LLM להבין תיאורים של צבעים
  4. הצהרות על פונקציות – הגדרה של כלים שבהם ה-LLM יכול להשתמש כדי להגדיר צבעים באפליקציה
  5. טיפול בכלי – עיבוד של קריאות לפונקציות מ-LLM וקישור שלהן למצב האפליקציה
  6. סטרימינג של תשובות – שיפור חוויית המשתמש באמצעות סטרימינג בזמן אמת של תשובות מ-LLM
  7. סנכרון הקשר של LLM – יצירת חוויה עקבית על ידי עדכון ה-LLM לגבי פעולות המשתמש

מה תלמדו?

  • הגדרת Firebase AI Logic לאפליקציות Flutter
  • איך כותבים הנחיות מערכת יעילות כדי להנחות את התנהגות ה-LLM
  • הטמעה של הצהרות פונקציה שמגשרות בין שפה טבעית לבין תכונות של האפליקציה
  • עיבוד תשובות בסטרימינג כדי לשפר את חוויית המשתמש
  • סנכרון המצב בין אירועים בממשק המשתמש לבין מודל שפה גדול (LLM)
  • ניהול מצב השיחה עם מודל שפה גדול באמצעות Riverpod
  • טיפול בשגיאות בצורה חלקה באפליקציות שמבוססות על מודלי שפה גדולים

תצוגה מקדימה של הקוד: טעימה של מה שתטמיעו

דוגמה להצהרת הפונקציה שתיצרו כדי לאפשר למודל ה-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 הזה

מומלץ לצפות בפרק 59 של Observable Flutter, שבו קרייג לאבנז ואנדרו ברוגדון דנים ב-codelab הזה:

דרישות מוקדמות

כדי להפיק את המרב מה-codelab הזה, צריך:

  • ניסיון בפיתוח באמצעות Flutter – היכרות עם היסודות של Flutter ותחביר Dart
  • ידע בתכנות אסינכרוני – הבנה של Futures,‏ async/await וזרמים
  • חשבון Firebase – צריך חשבון Google כדי להגדיר את Firebase

הגיע הזמן להתחיל ליצור את אפליקציית Flutter הראשונה שמבוססת על LLM.

2. הגדרת פרויקט ושירות הד

בשלב הראשון, מגדירים את מבנה הפרויקט ומיישמים שירות הד שיוחלף בהמשך בשילוב של Gemini API. כך מגדירים את ארכיטקטורת האפליקציה ומוודאים שממשק המשתמש פועל בצורה תקינה לפני שמוסיפים את המורכבות של קריאות ל-LLM.

מה תלמדו בשלב הזה

  • הגדרה של פרויקט Flutter עם יחסי התלות הנדרשים
  • עבודה עם חבילת colorist_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

הפעולה הזו תוסיף את חבילות המפתח הבאות:

  • colorist_ui: חבילה מותאמת אישית שמספקת את רכיבי ממשק המשתמש לאפליקציית Colorist
  • flutter_riverpod ו-riverpod_annotation: לניהול מצב
  • logging: לרישום ביומן עם נתונים מובְנים
  • יחסי תלות בפיתוח ליצירת קוד ולניתוח קוד

התג pubspec.yaml אמור להיראות כך:

pubspec.yaml

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

environment:
  sdk: ^3.9.2

dependencies:
  flutter:
    sdk: flutter
  colorist_ui: ^0.3.0
  flutter_riverpod: ^3.0.0
  riverpod_annotation: ^3.0.0

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^6.0.0
  build_runner: ^2.7.1
  riverpod_generator: ^3.0.0
  riverpod_lint: ^3.0.0
  json_serializable: ^6.11.1

flutter:
  uses-material-design: true

הטמעה של הקובץ 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(chatStateProvider.notifier);
    final logStateNotifier = ref.read(logStateProvider.notifier);

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

הפעולה הזו מגדירה אפליקציית Flutter שמטמיעה שירות הד שמדמה את ההתנהגות של מודל שפה גדול (LLM) על ידי החזרת ההודעה של המשתמש.

הסבר על הארכיטקטורה

כדאי להקדיש רגע להבנת הארכיטקטורה של אפליקציית colorist:

חבילת colorist_ui

חבילת colorist_ui מספקת רכיבי ממשק משתמש מוכנים מראש וכלים לניהול מצב:

  1. MainScreen: רכיב ממשק המשתמש הראשי שמציג:
    • פריסת מסך מפוצל במחשב (אזור אינטראקציה וחלונית יומן)
    • ממשק עם כרטיסיות בנייד
    • תצוגת צבע, ממשק צ'אט ותמונות ממוזערות של היסטוריה
  2. ניהול מצב: האפליקציה משתמשת בכמה התראות על מצב:
    • ChatStateNotifier: ניהול הודעות הצ'אט
    • ColorStateNotifier: ניהול הצבע הנוכחי וההיסטוריה
    • LogStateNotifier: ניהול של רשומות ביומן לצורך ניפוי באגים
  3. טיפול בהודעות: האפליקציה משתמשת במודל הודעות עם מצבים שונים:
    • הודעות למשתמש: הודעות שהוזנו על ידי המשתמש
    • הודעות LLM: נוצרות על ידי מודל שפה גדול (או שירות האקו שלכם בשלב הזה)
    • MessageState: מעקב אחרי הודעות של LLM כדי לדעת אם הן הושלמו או עדיין מוזרמות

ארכיטקטורה של אפליקציות

הארכיטקטורה של האפליקציה היא כזו:

  1. שכבת ממשק המשתמש: מסופקת על ידי חבילת colorist_ui
  2. ניהול מצב: שימוש ב-Riverpod לניהול מצב תגובתי
  3. שכבת השירות: כרגע היא מכילה את שירות האקו הפשוט שלכם, והיא תוחלף בשירות Gemini Chat
  4. שילוב LLM: יתווסף בשלבים מאוחרים יותר

ההפרדה הזו מאפשרת לכם להתמקד בהטמעה של שילוב ה-LLM, כי רכיבי ממשק המשתמש כבר מטופלים.

הפעלת האפליקציה

מריצים את האפליקציה באמצעות הפקודה הבאה:

flutter run -d DEVICE

מחליפים את DEVICE במכשיר היעד, כמו macos,‏ windows,‏ chrome או מזהה מכשיר.

צילום מסך של אפליקציית Colorist שבו מוצג שירות ההד של עיבוד Markdown

עכשיו אמורה להופיע אפליקציית Colorist עם:

  1. אזור תצוגה של צבע עם צבע ברירת מחדל
  2. ממשק צ'אט שבו אפשר להקליד הודעות
  3. חלונית יומן שבה מוצגות האינטראקציות בצ'אט

אפשר לנסות להקליד הודעה כמו "אני רוצה צבע כחול עמוק" וללחוץ על סמל השליחה. שירות האקו פשוט יחזור על ההודעה שלכם. בשלבים הבאים, תחליפו את זה בפרשנות צבעים בפועל באמצעות Firebase AI Logic.

מה השלב הבא?

בשלב הבא, תגדירו את Firebase ותטמיעו שילוב בסיסי של Gemini API כדי להחליף את שירות האקו בשירות הצ'אט של Gemini. כך האפליקציה תוכל לפרש תיאורי צבעים ולספק תשובות חכמות.

פתרון בעיות

בעיות בחבילת ממשק המשתמש

אם נתקלים בבעיות בחבילה colorist_ui:

  • מוודאים שמשתמשים בגרסה העדכנית ביותר
  • מוודאים שהוספתם את התלות בצורה נכונה
  • בדיקה אם יש גרסאות חבילה שמתנגשות

שגיאות בבנייה

אם מופיעות שגיאות בבנייה:

  • מוודאים שמותקנת במכשיר גרסת ה-SDK היציבה האחרונה של Flutter
  • מריצים את flutter clean ואז את flutter pub get
  • בדיקת הפלט של המסוף כדי למצוא הודעות שגיאה ספציפיות

מושגים מרכזיים שנלמדו

  • הגדרת פרויקט Flutter עם הרכיבים התלויים הנדרשים
  • הסבר על הארכיטקטורה של האפליקציה ועל האחריות של הרכיבים
  • הטמעה של שירות פשוט שמחק את ההתנהגות של מודל שפה גדול (LLM)
  • חיבור השירות לרכיבי ממשק המשתמש
  • שימוש ב-Riverpod לניהול מצב

‫3. שילוב בסיסי של Gemini Chat

בשלב הזה, תחליפו את שירות האקו מהשלב הקודם בשילוב של Gemini API באמצעות Firebase AI Logic. תגדירו את Firebase, תגדירו את הספקים הנדרשים ותטמיעו שירות צ'אט בסיסי שמתקשר עם Gemini API.

מה תלמדו בשלב הזה

  • הגדרת Firebase באפליקציית Flutter
  • הגדרת Firebase AI Logic לגישה ל-Gemini
  • יצירת ספקי Riverpod לשירותי Firebase ו-Gemini
  • הטמעה של שירות צ'אט בסיסי באמצעות Gemini API
  • טיפול בתגובות אסינכרוניות של API ובמצבי שגיאה

הגדרת Firebase

קודם כל, צריך להגדיר את Firebase לפרויקט Flutter. התהליך כולל יצירה של פרויקט Firebase, הוספה של האפליקציה לפרויקט והגדרה של ההגדרות הנדרשות של Firebase AI Logic.

יצירת פרויקט Firebase

  1. עוברים אל מסוף Firebase ונכנסים באמצעות חשבון Google.
  2. לוחצים על יצירת פרויקט Firebase או בוחרים פרויקט קיים.
  3. פועלים לפי ההוראות באשף ההגדרה כדי ליצור את הפרויקט.

הגדרת Firebase AI Logic בפרויקט Firebase

  1. במסוף Firebase, עוברים לפרויקט.
  2. בסרגל הצד שמימין, לוחצים על AI.
  3. בתפריט הנפתח של ה-AI, בוחרים באפשרות AI Logic (לוגיקה של AI).
  4. בכרטיס 'לוגיקה של AI ב-Firebase', בוחרים באפשרות תחילת העבודה.
  5. פועלים לפי ההנחיות כדי להפעיל את Gemini Developer API בפרויקט.

התקנת FlutterFire CLI

ה-CLI של FlutterFire מפשט את ההגדרה של Firebase באפליקציות Flutter:

dart pub global activate flutterfire_cli

הוספת Firebase לאפליקציית Flutter

  1. מוסיפים את חבילות Firebase core ו-Firebase AI Logic לפרויקט:
flutter pub add firebase_core firebase_ai
  1. מריצים את פקודת ההגדרה של FlutterFire:
flutterfire configure

הפקודה הזו:

  • מבקשים לבחור את הפרויקט ב-Firebase שיצרתם
  • רישום אפליקציות Flutter ב-Firebase
  • יצירת קובץ firebase_options.dart עם הגדרות הפרויקט

הפקודה תזהה באופן אוטומטי את הפלטפורמות שבחרתם (iOS, ‏ Android, ‏ macOS, ‏ Windows, ‏ web) ותגדיר אותן בהתאם.

הגדרה ספציפית לפלטפורמה

ב-Firebase נדרשות גרסאות מינימליות גבוהות יותר מאלה שמוגדרות כברירת מחדל ב-Flutter. נדרשת גם גישה לרשת כדי לתקשר עם שרתי הלוגיקה של ה-AI ב-Firebase.

הגדרת הרשאות ב-macOS

ב-macOS, צריך להפעיל גישה לרשת בהרשאות של האפליקציה:

  1. פותחים את macos/Runner/DebugProfile.entitlements ומוסיפים:

macos/Runner/DebugProfile.entitlements

<key>com.apple.security.network.client</key>
<true/>
  1. פותחים גם את macos/Runner/Release.entitlements ומוסיפים את אותה רשומה.

הגדרת הגדרות iOS

ב-iOS, מעדכנים את הגרסה המינימלית בחלק העליון של ios/Podfile:

ios/Podfile

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

יצירת ספקי מודלים של Gemini

עכשיו יוצרים את ספקי Riverpod ל-Firebase ול-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: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();
}

הקובץ הזה מגדיר את הבסיס לשלושה ספקים מרכזיים. הספקים האלה נוצרים כשמריצים את dart run build_runner על ידי מחוללי הקוד של Riverpod.

  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_ai/firebase_ai.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(chatStateProvider.notifier);
    final logStateNotifier = ref.read(logStateProvider.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

מריצים את הפקודה של כלי ההרצה של ה-build כדי ליצור את קוד Riverpod הנדרש:

dart run build_runner build --delete-conflicting-outputs

הפעולה הזו תיצור את הקבצים .g.dart שנדרשים כדי ש-Riverpod יפעל.

עדכון הקובץ 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. הוספת מסכי טעינה ושגיאה באמצעות התבנית AsyncValue של Riverpod עם ה-method‏ when
  3. חיבור ממשק המשתמש לשירות הצ'אט החדש באמצעות sendMessage קריאה חוזרת (callback)

הפעלת האפליקציה

מריצים את האפליקציה באמצעות הפקודה הבאה:

flutter run -d DEVICE

מחליפים את DEVICE במכשיר היעד, כמו macos,‏ windows,‏ chrome או מזהה מכשיר.

צילום מסך של אפליקציית Colorist שבו רואים את מודל Gemini LLM מגיב לבקשה לצבע צהוב שטוף שמש

עכשיו, כשמקלידים הודעה, היא נשלחת אל Gemini API, ותקבלו תשובה מ-LLM ולא הד. בחלונית היומן יוצגו האינטראקציות עם ה-API.

הסבר על תקשורת עם מודלים גדולים של שפה

כדאי להבין מה קורה כשמתקשרים עם Gemini API:

תהליך התקשורת

  1. קלט משתמש: המשתמש מזין טקסט בממשק הצ'אט
  2. פורמט הבקשה: האפליקציה מעצבת את הטקסט כאובייקט Content עבור Gemini API
  3. תקשורת עם ה-API: הטקסט נשלח אל Gemini API דרך Firebase AI Logic
  4. עיבוד LLM: מודל Gemini מעבד את הטקסט ויוצר תשובה
  5. טיפול בתגובה: האפליקציה מקבלת את התגובה ומעדכנת את ממשק המשתמש
  6. רישום ביומן: כל התקשורת נרשמת ביומן לצורך שקיפות

סשנים של צ'אט והקשר לשיחה

במהלך השיחה עם Gemini, ההקשר נשמר בין ההודעות, כך שאפשר לנהל אינטראקציות כמו בשיחה רגילה. המשמעות היא שמודל ה-LLM 'זוכר' את ההחלפות הקודמות בסשן הנוכחי, וכך מאפשר שיחות עקביות יותר.

ההערה keepAlive: true בספק של סשן הצ'אט מבטיחה שההקשר הזה יישמר לאורך כל מחזור החיים של האפליקציה. ההקשר המתמשך הזה חיוני לשמירה על רצף שיחה טבעי עם מודל ה-LLM.

מה השלב הבא?

בשלב הזה, אפשר לשאול את Gemini API כל דבר, כי אין הגבלות על התשובות שהוא ייתן. לדוגמה, אפשר לבקש ממנו סיכום של מלחמות השושנים, שלא קשורות למטרה של אפליקציית הצבעים.

בשלב הבא, תיצרו הנחיית מערכת שתעזור ל-Gemini לפרש תיאורי צבע בצורה יעילה יותר. ההדגמה תראה איך להתאים אישית את ההתנהגות של מודל LLM לצרכים ספציפיים של אפליקציה, ולמקד את היכולות שלו בדומיין של האפליקציה.

פתרון בעיות

בעיות בהגדרות של Firebase

אם נתקלתם בשגיאות בהפעלת Firebase:

  • מוודאים שקובץ firebase_options.dart נוצר בצורה נכונה
  • מוודאים ששדרגתם לתוכנית Blaze כדי לקבל גישה ל-Firebase AI Logic

שגיאות בגישה ל-API

אם אתם מקבלים שגיאות בגישה ל-Gemini API:

  • בדיקה שהחיוב מוגדר בצורה תקינה בפרויקט Firebase
  • מוודאים שהתכונה 'לוגיקה מבוססת-AI ב-Firebase' וממשק Cloud AI API מופעלים בפרויקט ב-Firebase
  • בדיקת החיבור לרשת והגדרות חומת האש
  • מוודאים ששם המודל (gemini-2.0-flash) נכון וזמין

בעיות בהקשר של השיחה

אם שמתם לב ש-Gemini לא זוכר את ההקשר הקודם מהשיחה:

  • מוודאים שהפונקציה chatSession מסומנת בהערה @Riverpod(keepAlive: true)
  • מוודאים שאתם משתמשים באותה שיחת צ'אט לכל חילופי ההודעות
  • לפני ששולחים הודעות, צריך לוודא ששיחת הצ'אט אותחלה בצורה תקינה

בעיות שספציפיות לפלטפורמה

לבעיות שספציפיות לפלטפורמה:

  • ‫iOS/macOS: מוודאים שההרשאות המתאימות מוגדרות ושהגרסאות המינימליות מוגדרות
  • ‫Android: בדיקה שגרסת ה-SDK המינימלית מוגדרת בצורה נכונה
  • בדיקת הודעות שגיאה ספציפיות לפלטפורמה במסוף

מושגים מרכזיים שנלמדו

  • הגדרת Firebase באפליקציית Flutter
  • הגדרה של Firebase AI Logic לגישה ל-Gemini
  • יצירת ספקי Riverpod לשירותים אסינכרוניים
  • הטמעה של שירות צ'אט שמתקשר עם מודל שפה גדול (LLM)
  • טיפול במצבי API אסינכרוניים (טעינה, שגיאה, נתונים)
  • הסבר על זרימת התקשורת של מודלים גדולים של שפה ועל סשנים של צ'אט

4. הנחיות יעילות לתיאורי צבעים

בשלב הזה תיצרו ותטמיעו הנחיית מערכת שמנחה את Gemini בפירוש תיאורי צבעים. הנחיות למערכת הן דרך יעילה להתאים אישית את התנהגות ה-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 כך שיכלול את ספריית הנכסים הדיגיטליים:

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: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: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 כדי ליצור את קוד Riverpod הנדרש:

dart run build_runner build --delete-conflicting-outputs

הפעלה ובדיקה של האפליקציה

עכשיו מריצים את האפליקציה:

flutter run -d DEVICE

צילום מסך של אפליקציית Colorist שבו מודל Gemini LLM מגיב בתשובה שמתאימה לאפליקציה לבחירת צבעים

כדאי לנסות לבדוק אותו עם תיאורים שונים של צבעים:

  • ‫"I'd like a sky blue" ‏(אני רוצה צבע תכלת)
  • ‫"Give me a forest green" ‏(תן לי ירוק יער)
  • ‫"Make a vibrant sunset orange" ‏(צור כתום שקיעה עז)
  • ‫"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, אלא גם לקרוא לפונקציות באפליקציה כדי להגדיר את הצבע ישירות. הדוגמה הזו ממחישה איך מודלים של שפה גדולה יכולים לגשר על הפער בין שפה טבעית לבין תכונות קונקרטיות של אפליקציות.

פתרון בעיות

בעיות בטעינת נכסים

אם נתקלתם בשגיאות בטעינת הנחיית המערכת:

  • מוודאים שב-pubspec.yaml מופיעה נכון ספריית הנכסים
  • בודקים שהנתיב ב-rootBundle.loadString() תואם למיקום הקובץ
  • מריצים את הפקודה flutter clean ואחריה את הפקודה flutter pub get כדי לרענן את חבילת הנכסים.

תשובות לא עקביות

אם מודל ה-LLM לא פועל באופן עקבי לפי הוראות הפורמט:

  • כדאי לנסות להגדיר את דרישות הפורמט בצורה מפורשת יותר בהנחיית המערכת
  • הוספת דוגמאות נוספות כדי להדגים את התבנית הצפויה
  • מוודאים שהפורמט שמבקשים סביר למודל

הגבלת קצב של יצירת בקשות (API)

אם נתקלים בשגיאות שקשורות להגבלת קצב:

  • חשוב לדעת שיש מגבלות שימוש בשירות Firebase AI Logic
  • כדאי להטמיע לוגיקה לניסיון חוזר עם השהיה מעריכית לפני ניסיון חוזר (exponential backoff)
  • בדיקה במסוף Firebase אם יש בעיות שקשורות למכסות

מושגים מרכזיים שנלמדו

  • הסבר על התפקיד והחשיבות של הנחיות למערכת באפליקציות LLM
  • כתיבת הנחיות יעילות עם הוראות ברורות, דוגמאות ומגבלות
  • טעינה של הנחיות מערכת ושימוש בהן באפליקציית Flutter
  • הנחיית התנהגות של מודל שפה גדול למשימות ספציפיות לדומיין
  • שימוש בהנדסת הנחיות כדי לעצב תשובות של LLM

בשלב הזה נראה לכם איך אפשר להתאים אישית את ההתנהגות של מודל שפה גדול (LLM) בלי לשנות את הקוד – פשוט מספקים הוראות ברורות בהנחיית המערכת.

5. הצהרות על פונקציות בכלי LLM

בשלב הזה, תתחילו להטמיע הצהרות פונקציות כדי לאפשר ל-Gemini לבצע פעולות באפליקציה. התכונה המתקדמת הזו מאפשרת ל-LLM לא רק להציע ערכי RGB, אלא גם להגדיר אותם בפועל בממשק המשתמש של האפליקציה באמצעות קריאות מיוחדות לכלים. עם זאת, כדי לראות את בקשות ה-LLM שמופעלות באפליקציית Flutter, צריך לבצע את השלב הבא.

מה תלמדו בשלב הזה

  • הסבר על קריאה לפונקציות של מודלים גדולים של שפה (LLM) והיתרונות שלה באפליקציות Flutter
  • הגדרת הצהרות על פונקציות מבוססות סכימה ל-Gemini
  • שילוב של הצהרות פונקציה במודל Gemini
  • עדכון ההנחיה למערכת כדי להשתמש ביכולות של הכלי

הסבר על בקשות להפעלת פונקציות

לפני שמטמיעים הצהרות על פונקציות, כדאי להבין מה הן ולמה הן חשובות:

מהי קריאה לפונקציה?

קריאה לפונקציה (Function calling, לפעמים נקראת גם "שימוש בכלי") היא יכולת שמאפשרת למודל LLM:

  1. לזהות מתי בקשת משתמש תפיק תועלת מהפעלת פונקציה ספציפית
  2. יצירת אובייקט JSON מובנה עם הפרמטרים שנדרשים לפונקציה הזו
  3. מאפשרים לאפליקציה להפעיל את הפונקציה עם הפרמטרים האלה
  4. מקבל את התוצאה של הפונקציה ומשלב אותה בתשובה שלו

במקום שה-LLM רק יתאר מה צריך לעשות, קריאה לפונקציה מאפשרת ל-LLM להפעיל פעולות קונקרטיות באפליקציה.

למה חשוב להשתמש בקריאות לפונקציות באפליקציות Flutter

הפעלת פונקציות יוצרת גשר חזק בין שפה טבעית לבין תכונות של אפליקציות:

  1. פעולה ישירה: משתמשים יכולים לתאר מה הם רוצים בשפה טבעית, והאפליקציה מגיבה בפעולות קונקרטיות
  2. פלט מובנה: מודל ה-LLM יוצר נתונים מובנים ונקיים במקום טקסט שצריך לנתח
  3. פעולות מורכבות: מאפשרת ל-LLM לגשת לנתונים חיצוניים, לבצע חישובים או לשנות את מצב האפליקציה
  4. חוויית משתמש טובה יותר: שילוב חלק בין שיחה לפונקציונליות

באפליקציית Colorist, שימוש בפונקציות מאפשר למשתמשים להגיד "אני רוצה ירוק יער" וממשק המשתמש מתעדכן מיד בצבע הזה, בלי שצריך לנתח ערכי RGB מטקסט.

הגדרת הצהרות על פונקציות

יוצרים קובץ חדש lib/services/gemini_tools.dart כדי להגדיר את הצהרות הפונקציה:

lib/services/gemini_tools.dart

import 'package:firebase_ai/firebase_ai.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. תיאור הפונקציה: אתם מספקים תיאור ברור שעוזר למודל השפה הגדול להבין מתי להשתמש בה.
  3. הגדרות של פרמטרים: אתם מגדירים פרמטרים מובנים עם תיאורים משלהם:
    • red: הרכיב האדום של RGB, שמוגדר כמספר בין 0.0 ל-1.0
    • green: הרכיב הירוק של RGB, שמוגדר כמספר בין 0.0 ל-1.0
    • blue: הרכיב הכחול של RGB, שמוגדר כמספר בין 0.0 ל-1.0
  4. סוגי סכימה: משתמשים ב-Schema.number() כדי לציין שמדובר בערכים מספריים
  5. אוסף כלי עבודה: אתם יוצרים רשימה של כלים שמכילה את הצהרת הפונקציה

הגישה המובנית הזו עוזרת למודל השפה הגדול של Gemini להבין:

  • מתי הפונקציה הזו צריכה להיקרא
  • אילו פרמטרים צריך לספק
  • אילו אילוצים חלים על הפרמטרים האלה (כמו טווח הערכים)

עדכון ספק מודל 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: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. הסבר על הכלי: במקום לבקש ערכי RGB מעוצבים, עכשיו אתם יכולים לספר למודל שפה גדול על הכלי set_color
  2. תהליך שונה: שינוי שלב 3 מ'עיצוב הערכים בתגובה' ל'שימוש בכלי להגדרת ערכים'
  3. דוגמה מעודכנת: אתם מראים איך התגובה צריכה לכלול קריאה לכלי במקום טקסט מעוצב
  4. הסרנו את דרישת הפורמט: מכיוון שאתם משתמשים בקריאות פונקציה מובנות, אתם כבר לא צריכים פורמט טקסט ספציפי

ההנחיה המעודכנת הזו מכוונת את ה-LLM להשתמש בקריאה לפונקציה במקום לספק רק ערכי RGB בפורמט טקסט.

יצירת קוד Riverpod

מריצים את הפקודה של כלי ההרצה של ה-build כדי ליצור את קוד Riverpod הנדרש:

dart run build_runner build --delete-conflicting-outputs

הפעלת האפליקציה

בשלב הזה, Gemini ייצור תוכן שינסה להשתמש בקריאות לפונקציות, אבל עדיין לא הטמעתם את ה-handlers לקריאות לפונקציות. כשמריצים את האפליקציה ומתארים צבע, Gemini מגיב כאילו הוא הפעיל כלי, אבל לא רואים שינויים בצבע בממשק המשתמש עד לשלב הבא.

מפעילים את האפליקציה:

flutter run -d DEVICE

צילום מסך של אפליקציית Colorist שבו רואים את מודל Gemini LLM מגיב בתשובה חלקית

נסו לתאר צבע כמו "כחול עמוק של האוקיינוס" או "ירוק יער" ותראו את התשובות. מודל ה-LLM מנסה לקרוא לפונקציות שהוגדרו למעלה, אבל הקוד שלכם עדיין לא מזהה קריאות לפונקציות.

תהליך הפעלת הפונקציה

כדי להבין מה קורה כש-Gemini משתמש בקריאה לפונקציה:

  1. בחירת פונקציה: מודל ה-LLM מחליט אם קריאה לפונקציה תהיה מועילה על סמך הבקשה של המשתמש
  2. יצירת פרמטרים: מודל ה-LLM יוצר ערכי פרמטרים שמתאימים לסכימה של הפונקציה
  3. פורמט של בקשה להפעלת פונקציה: מודל ה-LLM שולח בתגובה אובייקט מובנה של בקשה להפעלת פונקציה
  4. טיפול באפליקציה: האפליקציה תקבל את הקריאה הזו ותבצע את הפונקציה הרלוונטית (שמיושמת בשלב הבא)
  5. שילוב של תשובות: בשיחות מרובות תורות, מודל ה-LLM מצפה שהתוצאה של הפונקציה תוחזר

במצב הנוכחי של האפליקציה, שלושת השלבים הראשונים מתרחשים, אבל עדיין לא הטמעתם את שלב 4 או 5 (טיפול בקריאות לפונקציות), שתעשו בשלב הבא.

פרטים טכניים: איך Gemini מחליט מתי להשתמש בפונקציות

‫Gemini מקבל החלטות חכמות לגבי מתי להשתמש בפונקציות על סמך:

  1. כוונת המשתמש: האם הפונקציה היא הדרך הכי טובה להשיב לבקשת המשתמש
  2. רלוונטיות הפונקציה: עד כמה הפונקציות הזמינות מתאימות למשימה
  3. זמינות הפרמטר: האם אפשר לקבוע את ערכי הפרמטרים בוודאות
  4. הוראות מערכת: הנחיות מההנחיה של המערכת לגבי השימוש בפונקציה

הגדרתם את Gemini לזהות בקשות לתיאור צבעים כהזדמנויות להפעיל את הפונקציה set_color על ידי מתן הצהרות ברורות על הפונקציה והוראות מערכת.

מה השלב הבא?

בשלב הבא, תטמיעו פונקציות לטיפול בבקשות להפעלת פונקציות שמגיעות מ-Gemini. כך נסגר המעגל, ותיאורים של משתמשים יכולים להפעיל שינויים בפועל בצבעים בממשק המשתמש באמצעות קריאות לפונקציות של מודל ה-LLM.

פתרון בעיות

בעיות בהצהרת פונקציה

אם נתקלים בשגיאות בהצהרות על פונקציות:

  • מוודאים שהשמות והסוגים של הפרמטרים תואמים למה שצפוי
  • מוודאים ששם הפונקציה ברור ומתאר את הפעולה שלה
  • מוודאים שהתיאור של הפונקציה מסביר באופן מדויק את המטרה שלה

בעיות בהנחיות מערכת

אם ה-LLM לא מנסה להשתמש בפונקציה:

  • מוודאים שההנחיה למערכת מורה בבירור למודל שפה גדול (LLM) להשתמש בכלי set_color
  • בודקים שהדוגמה בהנחיית המערכת מדגימה את השימוש בפונקציה
  • כדאי לנסות להסביר בצורה ברורה יותר איך להשתמש בכלי

בעיות כלליות

אם נתקלים בבעיות אחרות:

  • בדיקה אם יש שגיאות במסוף שקשורות להצהרות על פונקציות
  • מוודאים שהכלים מועברים למודל בצורה תקינה
  • מוודאים שכל הקוד שנוצר על ידי Riverpod מעודכן

מושגים מרכזיים שנלמדו

  • הגדרת הצהרות על פונקציות כדי להרחיב את היכולות של מודלים של LLM באפליקציות Flutter
  • יצירת סכימות של פרמטרים לאיסוף נתונים מובנים
  • שילוב של הצהרות פונקציות עם מודל Gemini
  • עדכון הנחיות המערכת כדי לעודד שימוש בפונקציות
  • איך מודלים גדולים של שפה (LLM) בוחרים פונקציות ומפעילים אותן

השלב הזה מדגים איך מודלים גדולים של שפה יכולים לגשר על הפער בין קלט בשפה טבעית לבין קריאות מובנות לפונקציות, ובכך ליצור בסיס לשילוב חלק בין שיחה לבין תכונות של אפליקציה.

6. הטמעה של טיפול בכלי

בשלב הזה, תטמיעו את הפונקציות לטיפול בקריאות לפונקציות שמגיעות מ-Gemini. כך נסגר מעגל התקשורת בין קלט בשפה טבעית לבין תכונות קונקרטיות של האפליקציה, והמודל LLM יכול לתפעל ישירות את ממשק המשתמש על סמך תיאורים של משתמשים.

מה תלמדו בשלב הזה

  • הסבר על צינור עיבוד הנתונים המלא של קריאות לפונקציות באפליקציות LLM
  • עיבוד קריאות לפונקציות מ-Gemini באפליקציית Flutter
  • הטמעה של פונקציות לטיפול בבקשות שמשנות את מצב האפליקציה
  • טיפול בתגובות של פונקציות והחזרת תוצאות ל-LLM
  • יצירת זרימת תקשורת מלאה בין LLM לממשק המשתמש
  • רישום ביומן של קריאות לפונקציות ותשובות לשם שקיפות

הסבר על צינור עיבוד הנתונים של קריאות לפונקציות

לפני שמתחילים בהטמעה, כדאי להבין את תהליך השימוש המלא בפונקציות:

התהליך מקצה לקצה

  1. קלט משתמש: המשתמש מתאר צבע בשפה טבעית (למשל, ‫"forest green")
  2. עיבוד LLM: Gemini מנתח את התיאור ומחליט להפעיל את הפונקציה set_color
  3. יצירת קריאה לפונקציה: Gemini יוצר JSON מובנה עם פרמטרים (ערכים של אדום, ירוק וכחול)
  4. קבלת קריאה לפונקציה: האפליקציה מקבלת את הנתונים המובְנים האלה מ-Gemini
  5. הפעלת הפונקציה: האפליקציה מפעילה את הפונקציה עם הפרמטרים שסופקו
  6. עדכון המצב: הפונקציה מעדכנת את המצב של האפליקציה (משנה את הצבע שמוצג)
  7. יצירת תשובה: הפונקציה מחזירה תוצאות למודל ה-LLM
  8. שילוב התשובה: מודל ה-LLM משלב את התוצאות האלה בתשובה הסופית שלו
  9. עדכון ממשק המשתמש: ממשק המשתמש מגיב לשינוי המצב ומציג את הצבע החדש

מחזור התקשורת המלא חיוני לשילוב נכון של מודלים גדולים של שפה (LLM). כשמודל LLM מבצע קריאה לפונקציה, הוא לא רק שולח את הבקשה וממשיך הלאה. במקום זאת, הוא ממתין שהאפליקציה תבצע את הפונקציה ותחזיר תוצאות. לאחר מכן, מודל ה-LLM משתמש בתוצאות האלה כדי לגבש את התשובה הסופית שלו, ויוצר רצף שיחה טבעי שכולל התייחסות לפעולות שבוצעו.

הטמעה של רכיבי handler של פונקציות

נעדכן את הקובץ lib/services/gemini_tools.dart כדי להוסיף רכיבי handler לקריאות לפונקציות:

lib/services/gemini_tools.dart

import 'package:colorist_ui/colorist_ui.dart';
import 'package:firebase_ai/firebase_ai.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(logStateProvider.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(colorStateProvider.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(logStateProvider.notifier);
    logStateNotifier.logFunctionResults(functionResults);
    return functionResults;
  }

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

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

הסבר על פונקציות לטיפול בבקשות

בואו נראה מה עושים ה-handlers של הפונקציות האלה:

  1. handleFunctionCall: רכיב מרכזי לניתוב בקשות ש:
    • הפונקציה call נרשמת ביומן לצורך שקיפות בחלונית היומן
    • הפונקציה מעבירה את הבקשה ל-handler המתאים על סמך שם הפונקציה
    • החזרת תגובה מובנית שתשלח חזרה ל-LLM
  2. handleSetColor: שם הפונקציה הספציפית של set_color ש:
    • מחזירה ערכי RGB ממפת הארגומנטים
    • המערכת ממירה אותם לסוגים הצפויים (מספרים כפולים)
    • מעדכן את מצב הצבע של האפליקציה באמצעות colorStateNotifier
    • יוצר תגובה מובנית עם סטטוס הצלחה ומידע על הצבע הנוכחי
    • הפונקציה מתעדת ביומן את התוצאות לצורך ניפוי באגים
  3. handleUnknownFunction: handler של חזרה למצב ראשוני לפונקציות לא ידועות, שכולל:
    • מתעד אזהרה לגבי הפונקציה שלא נתמכת
    • החזרת תגובת שגיאה ל-LLM

הפונקציה handleSetColor חשובה במיוחד כי היא מגשרת על הפער בין ההבנה של שפה טבעית של מודל ה-LLM לבין שינויים קונקרטיים בממשק המשתמש.

עדכון שירות 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_ai/firebase_ai.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(chatStateProvider.notifier);
    final logStateNotifier = ref.read(logStateProvider.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. לכל קריאה לפונקציה, מפעיל את ה-method‏ handleFunctionCall עם שם הפונקציה והארגומנטים
  3. איסוף התוצאות של כל קריאה לפונקציה
  4. התוצאות האלה נשלחות חזרה ל-LLM באמצעות Content.functionResponses
  5. עיבוד התשובה של ה-LLM לתוצאות הפונקציה
  6. עדכון ממשק המשתמש עם טקסט התשובה הסופי

כך נוצר תהליך הלוך ושוב:

  • משתמש ← LLM: בקשה לצבע
  • מודל שפה גדול (LLM) → אפליקציה: קריאות לפונקציות עם פרמטרים
  • אפליקציה ← משתמש: צבע חדש מוצג
  • אפליקציה ← LLM: תוצאות של פונקציה
  • מודל שפה גדול (LLM) ← משתמש: תשובה סופית שמשלבת תוצאות של פונקציות

יצירת קוד Riverpod

מריצים את הפקודה של כלי ההרצה של ה-build כדי ליצור את קוד Riverpod הנדרש:

dart run build_runner build --delete-conflicting-outputs

הרצה ובדיקה של התהליך המלא

עכשיו מריצים את האפליקציה:

flutter run -d DEVICE

צילום מסך של אפליקציית Colorist שבו רואים את מודל Gemini LLM מגיב באמצעות קריאה לפונקציה

נסו להזין תיאורים שונים של צבעים:

  • ‫"I'd like a deep crimson red" ‏(אני רוצה אדום עמוק)
  • ‫"Show me a calming sky blue"‏ (הצג לי תמונה של שמיים כחולים מרגיעים)
  • ‫"Give me the color of fresh mint leaves" ‏(תגיד לי מה הצבע של עלי נענע טריים)
  • ‫"I want to see a warm sunset orange" ‏(אני רוצה לראות צבע כתום חם של שקיעת השמש)
  • ‫"Make it a rich royal purple" ‏(תעשה את זה סגול מלכותי עשיר)

עכשיו אמורים להופיע:

  1. ההודעה שלכם מופיעה בממשק הצ'אט
  2. התשובה של Gemini מופיעה בצ'אט
  3. תיעוד של קריאות לפונקציות בחלונית היומן
  4. תוצאות הפונקציה מתועדות ביומן מיד אחרי
  5. מלבן הצבעים מתעדכן ומציג את הצבע המתואר
  6. ערכי ה-RGB מתעדכנים כדי להציג את הרכיבים של הצבע החדש
  7. התשובה הסופית של Gemini מופיעה, ולעתים קרובות כוללת הערה על הצבע שהוגדר

חלונית היומן מספקת תובנות לגבי מה שקורה מאחורי הקלעים. הפרטים שמוצגים הם:

  • הקריאות המדויקות לפונקציות ש-Gemini מבצע
  • הפרמטרים שנבחרו לכל ערך RGB
  • התוצאות שהפונקציה מחזירה
  • התשובות הנוספות מ-Gemini

התראות על מצב הצבע

ה-colorStateNotifier שבו אתם משתמשים כדי לעדכן את הצבעים הוא חלק מחבילת colorist_ui. הוא מנהל את:

  • הצבע הנוכחי שמוצג בממשק המשתמש
  • היסטוריית הצבעים (10 הצבעים האחרונים)
  • התראה על שינויים במצב של רכיבי ממשק משתמש

כשקוראים לפונקציה updateColor עם ערכי RGB חדשים, היא:

  1. יוצר אובייקט ColorData חדש עם הערכים שסופקו
  2. עדכון הצבע הנוכחי במצב האפליקציה
  3. הצבע יתווסף להיסטוריה
  4. מפעיל עדכונים בממשק המשתמש באמצעות ניהול המצב של Riverpod

רכיבי ממשק המשתמש בחבילת colorist_ui עוקבים אחרי הסטטוס הזה ומתעדכנים אוטומטית כשהוא משתנה, וכך נוצר חוויה ריאקטיבית.

הסבר על טיפול בשגיאות

ההטמעה שלך כוללת טיפול אמין בשגיאות:

  1. בלוק try-catch: עוטף את כל האינטראקציות עם LLM כדי לזהות חריגים
  2. רישום שגיאות ביומן: רישום שגיאות בחלונית היומן עם עקבות מחסנית
  3. משוב משתמשים: מספק הודעת שגיאה ידידותית בצ'אט
  4. ניקוי המצב: השלמת מצב ההודעה גם אם מתרחשת שגיאה

כך האפליקציה נשארת יציבה ומספקת משוב מתאים גם כשמתרחשות בעיות בשירות ה-LLM או בהפעלת הפונקציה.

היתרונות של שימוש בפונקציות לשיפור חוויית המשתמש

הפעולות שביצעתם כאן מדגימות איך מודלים גדולים של שפה (LLM) יכולים ליצור ממשקי משתמש טבעיים ועוצמתיים:

  1. ממשק בשפה טבעית: המשתמשים מביעים כוונות בשפה יומיומית
  2. פרשנות חכמה: מודל ה-LLM מתרגם תיאורים מעורפלים לערכים מדויקים
  3. מניפולציה ישירה: ממשק המשתמש מתעדכן בתגובה לשפה טבעית
  4. תשובות לפי ההקשר: מודל ה-LLM מספק הקשר לשיחה לגבי השינויים
  5. עומס קוגניטיבי נמוך: המשתמשים לא צריכים להבין ערכי RGB או תורת הצבעים

אפשר להרחיב את דפוס השימוש הזה בקריאות לפונקציות של LLM כדי לגשר בין שפה טבעית לפעולות בממשק המשתמש, וליישם אותו בתחומים רבים אחרים מעבר לבחירת צבעים.

מה השלב הבא?

בשלב הבא, נשפר את חוויית המשתמש באמצעות הטמעה של תגובות בסטרימינג. במקום לחכות לתשובה המלאה, המערכת תעבד את חלקי הטקסט ואת הקריאות לפונקציות כשהיא מקבלת אותם, וכך תיצור אפליקציה רספונסיבית ומושכת יותר.

פתרון בעיות

בעיות בבקשות להפעלת פונקציות

אם Gemini לא מפעיל את הפונקציות או שהפרמטרים שגויים:

  • מוודאים שהצהרת הפונקציה תואמת למה שמתואר בהנחיית המערכת
  • מוודאים ששמות הפרמטרים והסוגים שלהם עקביים
  • מוודאים שההנחיה למערכת מורה באופן מפורש למודל שפה גדול להשתמש בכלי
  • מוודאים ששם הפונקציה ב-handler זהה בדיוק למה שמופיע בהצהרה
  • בדיקת חלונית היומן לקבלת מידע מפורט על קריאות לפונקציות

בעיות בתגובה של פונקציה

אם התוצאות של הפונקציה לא מועברות בחזרה ל-LLM כמו שצריך:

  • בודקים שהפונקציה מחזירה מפה בפורמט תקין
  • מוודאים שהמערכת בונה את Content.functionResponses בצורה נכונה
  • מחפשים ביומן שגיאות שקשורות לתשובות של פונקציות
  • חשוב לוודא שאתם משתמשים באותה שיחה כדי לקבל את התשובה

בעיות בתצוגת הצבעים

אם הצבעים לא מוצגים כמו שצריך:

  • מוודאים שערכי ה-RGB מומרים כראוי למספרים ממשיים (יכול להיות שה-LLM ישלח אותם כמספרים שלמים)
  • מוודאים שהערכים נמצאים בטווח הצפוי (0.0 עד 1.0)
  • בודקים שהקריאה ל-color state notifier מתבצעת בצורה נכונה
  • בודקים ביומן את הערכים המדויקים שמועברים לפונקציה

בעיות כלליות

לבעיות כלליות:

  • בודקים אם יש שגיאות או אזהרות ביומנים
  • אימות הקישוריות של Firebase AI Logic
  • בודקים אם יש אי-התאמות בסוגים של פרמטרים של פונקציות
  • מוודאים שכל הקוד שנוצר על ידי Riverpod מעודכן

מושגים מרכזיים שנלמדו

  • הטמעה של צינור מלא לעיבוד נתונים של קריאות לפונקציות ב-Flutter
  • יצירת תקשורת מלאה בין מודל LLM לבין האפליקציה
  • עיבוד נתונים מובְנים מתשובות של LLM
  • שליחת תוצאות הפונקציה בחזרה למודל שפה גדול (LLM) כדי לשלב אותן בתשובות
  • שימוש בחלונית היומן כדי לקבל תובנות לגבי האינטראקציות בין מודלים גדולים של שפה (LLM) לבין אפליקציות
  • חיבור קלט בשפה טבעית לשינויים קונקרטיים בממשק המשתמש

אחרי השלמת השלב הזה, האפליקציה שלכם תציג את אחת מהתבניות החזקות ביותר לשילוב של LLM: תרגום של קלט בשפה טבעית לפעולות קונקרטיות בממשק המשתמש, תוך שמירה על שיחה עקבית שמתייחסת לפעולות האלה. כך נוצר ממשק שיחה אינטואיטיבי שגורם למשתמשים להרגיש קסם.

7. הצגת התשובות באופן שוטף לשיפור חוויית המשתמש

בשלב הזה, תשפרו את חוויית המשתמש על ידי הטמעה של תשובות בסטרימינג מ-Gemini. במקום לחכות ליצירת התשובה המלאה, תוכלו לעבד את חלקי הטקסט ואת הקריאות לפונקציות כשהם מתקבלים, וכך ליצור אפליקציה רספונסיבית ומושכת יותר.

מה כולל השלב הזה

  • החשיבות של סטרימינג באפליקציות מבוססות-LLM
  • הטמעה של תשובות LLM בסטרימינג באפליקציית Flutter
  • עיבוד של חלקי טקסט שמגיעים מה-API
  • ניהול מצב השיחה כדי למנוע התנגשויות בין הודעות
  • טיפול בהפעלות של פונקציות בתגובות סטרימינג
  • יצירת אינדיקטורים ויזואליים לתשובות בתהליך

למה סטרימינג חשוב לאפליקציות של מודלים גדולים של שפה

לפני שמטמיעים את התכונה, חשוב להבין למה סטרימינג של תשובות הוא חיוני ליצירת חוויות משתמש מצוינות באמצעות מודלים מסוג LLM:

חוויית משתמש משופרת

לתשובות בסטרימינג יש כמה יתרונות משמעותיים מבחינת חוויית המשתמש:

  1. השהיה מורגשת נמוכה יותר: המשתמשים רואים את הטקסט מתחיל להופיע מיד (בדרך כלל תוך 100-300 אלפיות השנייה), במקום לחכות כמה שניות לתשובה מלאה. התפיסה הזו של מיידיות משפרת באופן משמעותי את שביעות הרצון של המשתמשים.
  2. קצב טבעי של שיחה: הטקסט מופיע בהדרגה, כמו בשיחה בין בני אדם, וכך נוצרת חוויה טבעית יותר של דיאלוג.
  3. עיבוד מידע הדרגתי: המשתמשים יכולים להתחיל לעבד מידע כשהוא מגיע, במקום להיות מוצפים בבלוק גדול של טקסט בבת אחת.
  4. הזדמנות להפריע מוקדם: באפליקציה מלאה, המשתמשים יכולים להפריע ל-LLM או להפנות אותו מחדש אם הם רואים שהוא מתקדם לכיוון לא מועיל.
  5. אישור חזותי של הפעילות: הטקסט בסטרימינג מספק משוב מיידי שהמערכת פועלת, וכך מצמצם את אי הוודאות.

יתרונות טכניים

בנוסף לשיפורים בחוויית המשתמש, הסטרימינג מציע יתרונות טכניים:

  1. הפעלה מוקדמת של פונקציות: אפשר לזהות ולהפעיל קריאות לפונקציות ברגע שהן מופיעות בזרם, בלי לחכות לתגובה המלאה.
  2. עדכונים מצטברים בממשק המשתמש: אתם יכולים לעדכן את ממשק המשתמש באופן הדרגתי ככל שמתקבל מידע חדש, וכך ליצור חוויה דינמית יותר.
  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/legacy.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(chatStateProvider.notifier);
    final logStateNotifier = ref.read(logStateProvider.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(chatStateProvider.notifier);
    final logStateNotifier = ref.read(logStateProvider.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 מעבד כל נתח בזמן אמת כשהוא מגיע
    • הטקסט מתווסף לממשק המשתמש באופן מיידי, ויוצר את אפקט הסטרימינג
    • הפונקציות מופעלות ברגע שהן מזוהות
  4. טיפול בקריאות לפונקציות:
    • כשמזוהה קריאה לפונקציה בחלק של טקסט, היא מופעלת באופן מיידי
    • התוצאות נשלחות חזרה ל-LLM באמצעות שיחת סטרימינג נוספת
    • גם התשובה של ה-LLM לתוצאות האלה מעובדת בסטרימינג
  5. טיפול בשגיאות וניקוי:
    • try/catch מספק טיפול חזק בשגיאות
    • הבלוק finally block מוודא שמצב השיחה מאופס בצורה תקינה
    • ההודעה תמיד מסתיימת, גם אם מתרחשות שגיאות

ההטמעה הזו יוצרת חוויית סטרימינג מהירה ואמינה, תוך שמירה על מצב שיחה תקין.

עדכון המסך הראשי כדי לחבר את מצב השיחה

משנים את הקובץ 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 כדי ליצור את קוד Riverpod הנדרש:

dart run build_runner build --delete-conflicting-outputs

הפעלה ובדיקה של תשובות בסטרימינג

מריצים את האפליקציה:

flutter run -d DEVICE

צילום מסך של אפליקציית Colorist שבו רואים את מודל Gemini LLM מגיב בשידור חי

עכשיו נסו לבדוק את התנהגות הסטרימינג עם תיאורים שונים של צבעים. אפשר לנסות תיאורים כמו:

  • ‫"Show me the deep teal color of the ocean at twilight"
  • ‫"I'd like to see a vibrant coral that reminds me of tropical flowers" ‏(אני רוצה לראות אלמוג תוסס שמזכיר לי פרחים טרופיים)
  • ‫"Create a muted olive green like old army fatigues" (תיצור צבע ירוק זית עמום כמו מדי צבא ישנים)

פירוט התהליך הטכני של סטרימינג

בואו נבדוק בדיוק מה קורה כשמבצעים סטרימינג של תשובה:

יצירת חיבור

כשמתקשרים אל sendMessageStream(), קורים הדברים הבאים:

  1. האפליקציה יוצרת חיבור לשירות Firebase AI Logic
  2. בקשת המשתמש נשלחת לשירות
  3. השרת מתחיל לעבד את הבקשה
  4. החיבור לשידור נשאר פתוח ומוכן להעברת נתונים

העברת מקטעים

כש-Gemini יוצר תוכן, נתחים נשלחים דרך הזרם:

  1. השרת שולח נתחי טקסט כשהם נוצרים (בדרך כלל כמה מילים או משפטים)
  2. כש-Gemini מחליט לבצע קריאה לפונקציה, הוא שולח את פרטי הקריאה לפונקציה
  3. יכול להיות שיהיו עוד קטעי טקסט אחרי בקשות להפעלת פונקציות
  4. הזרם ממשיך עד שהיצירה מסתיימת

עיבוד הדרגתי

האפליקציה מעבדת כל נתח באופן מצטבר:

  1. כל מקטע טקסט מתווסף לתשובה הקיימת
  2. הפונקציות מופעלות ברגע שהן מזוהות
  3. ממשק המשתמש מתעדכן בזמן אמת עם תוצאות של טקסט ופונקציות
  4. הסטטוס נשמר כדי להראות שהתשובה עדיין בסטרימינג

השלמת הצפייה בשידור

כשהיצירה מסתיימת:

  1. השידור נסגר על ידי השרת
  2. הלולאה של await for יוצאת באופן טבעי
  3. ההודעה מסומנת כהודעה שהושלמה
  4. מצב השיחה חוזר למצב סרק
  5. ממשק המשתמש מתעדכן כדי לשקף את המצב שהושלם

השוואה בין סטרימינג לבין צפייה לא בסטרימינג

כדי להבין טוב יותר את היתרונות של סטרימינג, נשווה בין גישות של סטרימינג לבין גישות שאינן סטרימינג:

יחס

לא שייך לסטרימינג

סטרימינג

זמן אחזור נתפס

המשתמש לא רואה כלום עד שהתשובה מוכנה

המשתמש רואה את המילים הראשונות תוך אלפיות השנייה

חוויית משתמש

המתנה ארוכה ואז הטקסט מופיע פתאום

הופעה טבעית והדרגתית של הטקסט

ניהול מצב

פשוט יותר (ההודעות בהמתנה או שהן הושלמו)

מורכב יותר (ההודעות יכולות להיות במצב סטרימינג)

ביצוע הפונקציה

מתרחש רק אחרי שהתשובה מוכנה

מתרחש במהלך יצירת התשובה

מורכבות ההטמעה

קל יותר להטמיע

נדרש ניהול מצב נוסף

התאוששות משגיאה

תשובה שכוללת את כל האפשרויות

יכול להיות שקטעים מהתשובה עדיין יהיו שימושיים

מורכבות הקוד

פחות מורכב

מורכבות יותר בגלל הטיפול בנתונים בזמן אמת

באפליקציה כמו Colorist, היתרונות של חוויית המשתמש בסטרימינג עולים על המורכבות של ההטמעה, במיוחד כשמדובר בפרשנויות של צבעים שעשויות לקחת כמה שניות ליצירה.

שיטות מומלצות לשיפור חוויית המשתמש בסטרימינג

כשמטמיעים סטרימינג באפליקציות LLM משלכם, כדאי לפעול לפי השיטות המומלצות הבאות:

  1. אינדיקטורים ויזואליים ברורים: חשוב לספק תמיד רמזים ויזואליים ברורים שמבדילים בין הודעות בסטרימינג לבין הודעות מלאות
  2. חסימת קלט: השבתת קלט משתמשים במהלך סטרימינג כדי למנוע בקשות חופפות מרובות
  3. שחזור שגיאות: עיצוב ממשק המשתמש כך שיטפל בשחזור חלק אם הסטרימינג מופסק
  4. מעברים בין מצבים: מוודאים שהמעברים בין מצבי המתנה, סטרימינג והשלמה יהיו חלקים
  5. הדמיה של ההתקדמות: כדאי להשתמש באנימציות או באינדיקטורים עדינים שמציגים את העיבוד הפעיל
  6. אפשרויות ביטול: באפליקציה מלאה, צריך לספק למשתמשים דרכים לבטל יצירה שנמצאת בתהליך
  7. שילוב של תוצאות הפונקציה: עיצוב ממשק המשתמש כך שיטפל בתוצאות של פונקציות שמופיעות באמצע התהליך
  8. אופטימיזציה של הביצועים: צמצום הבנייה מחדש של ממשק המשתמש במהלך עדכונים מהירים של הסטרימינג

חבילת colorist_ui מיישמת בשבילכם הרבה מהשיטות המומלצות האלה, אבל חשוב לקחת אותן בחשבון בכל הטמעה של מודל שפה גדול (LLM) להזרמת נתונים.

מה השלב הבא?

בשלב הבא, תטמיעו סנכרון של LLM על ידי שליחת הודעה ל-Gemini כשמשתמשים בוחרים צבעים מההיסטוריה. כך נוצרת חוויה מגובשת יותר שבה מודל ה-LLM מודע לשינויים במצב האפליקציה שהמשתמש יזם.

פתרון בעיות

בעיות בעיבוד נתונים בזמן אמת

אם נתקלים בבעיות בעיבוד הנתונים בשידור חי:

  • תסמינים: תגובות חלקיות, טקסט חסר או סיום פתאומי של הסטרימינג
  • פתרון: בודקים את החיבור לרשת ומוודאים שיש דפוסי async/await תקינים בקוד
  • אבחון: בודקים בחלונית היומן אם יש הודעות שגיאה או אזהרות שקשורות לעיבוד הסטרימינג
  • תיקון: מוודאים שכל עיבוד הנתונים בסטרימינג משתמש בטיפול נכון בשגיאות עם בלוקים של try/catch

חסרות בקשות להפעלת פונקציות

אם לא מזוהות קריאות לפונקציות בזרם:

  • תסמינים: הטקסט מופיע אבל הצבעים לא מתעדכנים, או שביומן לא מופיעות קריאות לפונקציות
  • הפתרון: בודקים את ההנחיות בהנחיית המערכת לגבי השימוש בקריאות לפונקציות
  • אבחון: בודקים בחלונית היומן אם מתקבלות קריאות לפונקציות
  • התיקון: משנים את הנחיית המערכת כך שתהיה בה הוראה מפורשת יותר למודל שפה גדול להשתמש בכלי set_color

טיפול כללי בשגיאות

אם יש בעיות אחרות:

  • שלב 1: בדיקה אם הודעות שגיאה מוצגות בחלונית היומן
  • שלב 2: אימות הקישוריות של Firebase AI Logic
  • שלב 3: מוודאים שכל הקוד שנוצר על ידי Riverpod מעודכן
  • שלב 4: בודקים את ההטמעה של הסטרימינג כדי לוודא שאין הצהרות await חסרות

מושגים מרכזיים שנלמדו

  • Implementing streaming responses with the Gemini API for more responsive UX
  • ניהול מצב השיחה כדי לטפל באינטראקציות של סטרימינג בצורה נכונה
  • עיבוד של טקסט בזמן אמת ושל קריאות לפונקציות כשהם מגיעים
  • יצירת ממשקי משתמש רספונסיביים שמתעדכנים בהדרגה במהלך הסטרימינג
  • טיפול בשידורים בו-זמניים באמצעות דפוסי אסינכרון מתאימים
  • מתן משוב חזותי מתאים במהלך סטרימינג של תשובות

הטמעת סטרימינג שיפרה באופן משמעותי את חוויית המשתמש באפליקציית Colorist, ויצרה ממשק רספונסיבי ומושך יותר שמרגיש כמו שיחה אמיתית.

8. סנכרון ההקשר של מודל שפה גדול (LLM)

בשלב הבונוס הזה, תטמיעו סנכרון של הקשר LLM על ידי שליחת הודעה ל-Gemini כשמשתמשים בוחרים צבעים מההיסטוריה. כך נוצרת חוויה מגובשת יותר שבה מודל ה-LLM מודע לפעולות של המשתמש בממשק, ולא רק להודעות המפורשות שלו.

מה כולל השלב הזה

  • יצירת סנכרון של הקשר של מודל שפה גדול (LLM) בין ממשק המשתמש לבין ה-LLM
  • סריאליזציה של אירועים בממשק המשתמש להקשר שה-LLM יכול להבין
  • עדכון ההקשר של השיחה על סמך פעולות המשתמש
  • יצירת חוויה עקבית בשיטות אינטראקציה שונות
  • שיפור היכולת של מודלים מסוג LLM להבין את ההקשר מעבר להודעות צ'אט מפורשות

הסבר על סנכרון ההקשר של מודלים גדולים של שפה

צ'אטבוטים מסורתיים מגיבים רק להודעות מפורשות של משתמשים, ולכן נוצר ניתוק כשמשתמשים מבצעים אינטראקציה עם האפליקציה באמצעים אחרים. הסנכרון של ההקשר של מודל שפה גדול (LLM) פותר את המגבלה הזו:

למה חשוב לסנכרן את ההקשר של מודלים גדולים של שפה

כשמשתמשים מבצעים אינטראקציה עם האפליקציה באמצעות רכיבי ממשק משתמש (למשל, בחירת צבע מההיסטוריה), מודל ה-LLM לא יכול לדעת מה קרה אלא אם מציינים זאת במפורש. סנכרון ההקשר של מודל שפה גדול (LLM):

  1. שמירה על ההקשר: מודל ה-LLM מקבל מידע על כל הפעולות הרלוונטיות של המשתמשים
  2. יוצר עקביות: יוצר חוויה מגובשת שבה מודל ה-LLM מזהה אינטראקציות עם ממשק המשתמש
  3. שיפור האינטליגנציה: מאפשר למודל ה-LLM להגיב בצורה מתאימה לכל פעולות המשתמש
  4. שיפור חוויית המשתמש: האפליקציה מרגישה משולבת ומגיבה יותר
  5. מצמצם את המאמץ של המשתמשים: לא צריך להסביר באופן ידני את הפעולות בממשק המשתמש

באפליקציית 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/legacy.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(chatStateProvider.notifier);
    final logStateNotifier = ref.read(logStateProvider.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(chatStateProvider.notifier);
    final logStateNotifier = ref.read(logStateProvider.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, שמקשרת בין אירוע בממשק המשתמש (בחירת צבע מההיסטוריה) לבין מערכת ההתראות של ה-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. הסבר של המושג 'התראות על בחירת היסטוריה' למודל שפה גדול
  2. דוגמה לאופן שבו ההתראות האלה נראות
  3. דוגמה לתשובה מתאימה
  4. הגדרת ציפיות לגבי אישור הבחירה והוספת תגובה לגבי הצבע

כך מודל ה-LLM יוכל להבין איך להגיב בצורה מתאימה להודעות המיוחדות האלה.

יצירת קוד Riverpod

מריצים את הפקודה של כלי ההרצה של ה-build כדי ליצור את קוד Riverpod הנדרש:

dart run build_runner build --delete-conflicting-outputs

הרצה ובדיקה של סנכרון ההקשר של מודל שפה גדול (LLM)

מריצים את האפליקציה:

flutter run -d DEVICE

צילום מסך של אפליקציית Colorist שבו רואים את מודל Gemini LLM מגיב לבחירה מתוך היסטוריית הצבעים

בדיקת סנכרון ההקשר של מודל שפה גדול (LLM) כוללת:

  1. קודם, יוצרים כמה צבעים על ידי תיאור שלהם בצ'אט
    • ‫"Show me a vibrant purple" ‏(הצגת סגול עז)
    • ‫"I'd like a forest green" (אני רוצה ירוק יער)
    • ‫"Give me a bright red" ‏(תביא לי אדום בוהק)
  2. אחר כך לוחצים על אחת מתמונות המיניאטורה של הצבעים ברצועת ההיסטוריה.

כדאי לשים לב לנקודות הבאות:

  1. הצבע שנבחר מופיע בתצוגה הראשית
  2. הודעה למשתמש מופיעה בצ'אט ומציינת את בחירת הצבע
  3. ה-LLM מגיב באישור הבחירה ובהערה על הצבע
  4. האינטראקציה כולה מרגישה טבעית ועקבית

כך נוצרת חוויה חלקה שבה ה-LLM מודע להודעות ישירות ולפעולות בממשק המשתמש ומגיב להן בצורה מתאימה.

איך פועל סנכרון ההקשר של מודלים גדולים של שפה (LLM)

הנה הסבר על הפרטים הטכניים של הסנכרון:

זרימת נתונים

  1. פעולת משתמש: המשתמש לוחץ על צבע ברצועת ההיסטוריה
  2. אירוע בממשק המשתמש: הווידג'ט MainScreen מזהה את הבחירה הזו
  3. ביצוע של קריאה חוזרת (callback): הקריאה החוזרת notifyColorSelection מופעלת
  4. יצירת הודעה: נוצרת הודעה בפורמט מיוחד עם נתוני הצבע
  5. עיבוד LLM: ההודעה נשלחת אל Gemini, שמזהה את הפורמט
  6. תשובה בהתאם להקשר: Gemini מגיב בצורה הולמת על סמך הנחיית המערכת
  7. עדכון בממשק המשתמש: התשובה מופיעה בצ'אט, וכך נוצר חוויה מגובשת

סריאליזציה של נתונים

היבט מרכזי בגישה הזו הוא האופן שבו מבצעים סריאליזציה של נתוני הצבע:

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

השיטה toLLMContextMap() (שמסופקת על ידי חבילת colorist_ui) ממירה אובייקט ColorData למפה עם מאפייני מפתח שמודל שפה גדול יכול להבין. בדרך כלל צוות כזה כולל:

  • ערכי RGB (אדום, ירוק, כחול)
  • ייצוג קוד הקסדצימלי
  • כל שם או תיאור שמשויכים לצבע

אם תדאגו שהנתונים האלה יהיו בפורמט עקבי ותכללו אותם בהודעה, תבטיחו של-LLM יהיה את כל המידע שהוא צריך כדי להגיב בצורה מתאימה.

שימושים נרחבים יותר בסנכרון ההקשר של מודלים גדולים של שפה

לדפוס הזה של שליחת התראות למודל שפה גדול (LLM) לגבי אירועים בממשק המשתמש יש שימושים רבים מעבר לבחירת צבעים:

תרחישים אחרים לדוגמה

  1. שינויים במסננים: הודעה למודל ה-LLM כשמשתמשים מחילים מסננים על נתונים
  2. אירועי ניווט: מודיעים ל-LLM כשמשתמשים עוברים לקטעים שונים
  3. שינויים בבחירה: עדכון ה-LLM כשמשתמשים בוחרים פריטים מרשימות או מטבלאות
  4. עדכוני העדפות: עדכון מודל ה-LLM כשמשתמשים משנים הגדרות או העדפות
  5. מניפולציה של נתונים: שליחת התראה ל-LLM כשמשתמשים מוסיפים, עורכים או מוחקים נתונים

בכל מקרה, התבנית נשארת זהה:

  1. זיהוי אירוע בממשק המשתמש
  2. סריאליזציה של נתונים רלוונטיים
  3. שליחת התראה בפורמט מיוחד ל-LLM
  4. הנחיית ה-LLM להגיב בצורה הולמת באמצעות הנחיית המערכת

שיטות מומלצות לסנכרון הקשר של מודלים גדולים של שפה

בהתאם להטמעה שלכם, הנה כמה שיטות מומלצות לסנכרון יעיל של הקשר ב-LLM:

1. עיצוב עקבי

כדאי להשתמש בפורמט עקבי להתראות כדי שמודל ה-LLM יוכל לזהות אותן בקלות:

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

2. הקשר עשיר

כדאי לכלול בהתראות מספיק פרטים כדי שה-LLM יוכל להגיב בצורה חכמה. לגבי צבעים, הכוונה היא לערכי RGB, קודי Hex וכל מאפיין רלוונטי אחר.

‫3. מחיקת ההוראות

צריך לתת בהנחיה למערכת הוראות מפורשות לגבי אופן הטיפול בהתראות, רצוי עם דוגמאות.

4. שילוב טבעי

ההתראות צריכות להשתלב בשיחה בצורה טבעית, ולא להפריע לה.

5. התראה סלקטיבית

ההודעה על פעולות רלוונטית לשיחה רק אם היא קשורה אליה. לא צריך לדווח על כל אירוע בממשק המשתמש.

פתרון בעיות

בעיות של התראות

אם מודל ה-LLM לא מגיב כמו שצריך לבחירת הצבעים:

  • בודקים שפורמט הודעת ההתראה תואם למה שמתואר בהנחיה למערכת
  • מוודאים שנתוני הצבע עוברים סריאליזציה בצורה תקינה
  • מוודאים שההנחיה למערכת כוללת הוראות ברורות לטיפול בבחירות
  • חיפוש שגיאות בשירות הצ'אט כשנשלחות התראות

ניהול ההקשרים

אם נראה שה-LLM מאבד את ההקשר:

  • בודקים שהשיחה בצ'אט מתנהלת בצורה תקינה
  • איך מוודאים שהמעברים בין מצבי השיחה מתבצעים בצורה תקינה
  • חשוב לוודא שההתראות נשלחות דרך אותה שיחת צ'אט

בעיות כלליות

לבעיות כלליות:

  • בודקים אם יש שגיאות או אזהרות ביומנים
  • אימות הקישוריות של Firebase AI Logic
  • בודקים אם יש אי-התאמות בסוגים של פרמטרים של פונקציות
  • מוודאים שכל הקוד שנוצר על ידי Riverpod מעודכן

מושגים מרכזיים שנלמדו

  • יצירת סנכרון של הקשר של מודל שפה גדול (LLM) בין ממשק המשתמש לבין מודל השפה הגדול
  • המרת אירועים בממשק המשתמש להקשר שמתאים למודל שפה גדול
  • הנחיית התנהגות של LLM עבור דפוסי אינטראקציה שונים
  • יצירת חוויה עקבית באינטראקציות עם הודעות ובאינטראקציות אחרות
  • שיפור ההבנה של מודל שפה גדול (LLM) לגבי המצב הכללי של האפליקציה

הטמעת סנכרון ההקשר של LLM מאפשרת ליצור חוויה משולבת באמת, שבה ה-LLM מרגיש כמו עוזר מודע ומגיב ולא רק כמו מחולל טקסט. אפשר להשתמש בדפוס הזה בעוד הרבה אפליקציות כדי ליצור ממשקי AI אינטואיטיביים וטבעיים יותר.

9. מעולה!

סיימתם בהצלחה את ה-Codelab בנושא קולוריסט! 🎉

מה יצרתם

יצרתם אפליקציית Flutter שפועלת באופן מלא ומשלבת את Gemini API של Google כדי לפרש תיאורי צבע בשפה טבעית. האפליקציה יכולה עכשיו:

  • עיבוד תיאורים בשפה טבעית כמו "כתום שקיעה" או "כחול עמוק של האוקיינוס"
  • אפשר להשתמש ב-Gemini כדי לתרגם את התיאורים האלה בצורה חכמה לערכי RGB
  • הצגת הצבעים המתורגמים בזמן אמת באמצעות תשובות בהזרמה
  • טיפול באינטראקציות של משתמשים באמצעות צ'אט ורכיבי ממשק משתמש
  • שמירה על מודעות להקשר בשיטות אינטראקציה שונות

לאן כדאי ללכת מכאן

אחרי שסיימתם ללמוד את יסודות השילוב של Gemini עם Flutter, הנה כמה דרכים להמשיך את המסע:

שיפור אפליקציית Colorist

  • לוחות צבעים: הוספת פונקציונליות ליצירת ערכות צבעים משלימות או תואמות
  • קלט קולי: שילוב של זיהוי דיבור לתיאורי צבעים מילוליים
  • ניהול היסטוריה: הוספת אפשרויות למתן שם, לארגון ולייצוא של ערכות צבעים
  • הנחיות בהתאמה אישית: יצירת ממשק שמאפשר למשתמשים להתאים אישית את הנחיות המערכת
  • ניתוח מתקדם: מעקב אחרי התיאורים שמניבים את התוצאות הכי טובות או גורמים לקשיים

תכונות נוספות של Gemini

  • קלט רב-אופני: הוספת קלט של תמונות כדי לחלץ צבעים מתמונות
  • יצירת תוכן: אפשר להשתמש ב-Gemini כדי ליצור תוכן שקשור לצבעים, כמו תיאורים או סיפורים
  • שיפורים בשימוש בפונקציות: יצירת שילובים מורכבים יותר של כלים עם כמה פונקציות
  • הגדרות בטיחות: אפשר לבדוק הגדרות בטיחות שונות ואת ההשפעה שלהן על התשובות

החלת הדפוסים האלה על דומיינים אחרים

  • ניתוח מסמכים: יצירת אפליקציות שיכולות להבין ולנתח מסמכים
  • עזרה בכתיבה יוצרת: יצירת כלים לכתיבה עם הצעות מבוססות-LLM
  • אוטומציה של משימות: עיצוב אפליקציות שמתרגמות שפה טבעית למשימות אוטומטיות
  • אפליקציות מבוססות-ידע: יצירת מערכות מומחים בתחומים ספציפיים

משאבים

ריכזנו כאן כמה מקורות מידע שיעזרו לכם להמשיך ללמוד:

תיעוד רשמי

קורס ומדריך ליצירת הנחיות

קהילה

Observable Flutter Agentic series

בפרק 59, קרייג לאבנז ואנדרו ברוגדן בוחנים את ה-codelab הזה ומדגישים חלקים מעניינים בבניית האפליקציה.

בפרק 60, קרייג ואנדרו חוזרים להרחיב את אפליקציית ה-codelab עם יכולות חדשות, ונאבקים כדי לגרום למודלים מסוג LLM לעשות את מה שאומרים להם.

בפרק 61, קרייג מצטרף לכריס סלס כדי לנתח כותרות חדשותיות וליצור תמונות תואמות.

משוב

נשמח לשמוע על החוויה שלך עם ה-codelab הזה. נשמח לקבל ממך משוב באחת מהדרכים הבאות:

תודה שסיימתם את ה-Codelab הזה. אנחנו מקווים שתמשיכו לחקור את האפשרויות המעניינות שקיימות בשילוב בין Flutter ל-AI.