1. יצירת אפליקציית Flutter מבוססת-Gemini
מה תפַתחו
ב-Codelab הזה נלמד איך ליצור את Colorist – אפליקציית Flutter אינטראקטיבית שמבוססת על Gemini API ומאפשרת למשתמשים לשלוט באפליקציה באמצעות שפה טבעית. ב-Codelab הזה תלמדו איך עושים את זה.
אפליקציית Colorist מאפשרת למשתמשים לתאר צבעים בשפה טבעית (למשל, "הכתום של שקיעה" או "כחול עמוק של האוקיינוס"), והאפליקציה:
- מעבד את התיאורים האלה באמצעות Gemini API מבית Google
- מפרש את התיאורים לערכי צבע RGB מדויקים
- הצגת הצבע במסך בזמן אמת
- מספק פרטים טכניים על הצבע והקשר מעניין לגביו
- שומר היסטוריה של צבעים שנוצרו לאחרונה

ממשק האפליקציה כולל מסך מפוצל עם אזור תצוגה צבעוני ומערכת צ'אט אינטראקקטיבית בצד אחד, וחלונית יומן מפורטת שמציגה את האינטראקציות הגולמיות עם מודל ה-LLM בצד השני. היומן הזה מאפשר לכם להבין טוב יותר איך שילוב של LLM פועל בפועל.
למה זה חשוב למפתחי Flutter
מודלים גדולים של שפה (LLM) משנים את האופן שבו משתמשים מקיימים אינטראקציה עם אפליקציות, אבל שילוב יעיל שלהם באפליקציות לנייד ולמחשב מציב אתגרים ייחודיים. ב-Codelab הזה נלמד דפוסי שימוש מעשיים שהם מעבר להפעלות API גולמיות.
תהליך הלמידה שלכם
ב-Codelab הזה נסביר איך ליצור את Colorist שלב אחר שלב:
- הגדרת הפרויקט – מתחילים עם מבנה בסיסי של אפליקציית Flutter וחבילת
colorist_ui - שילוב בסיסי של Gemini – קישור האפליקציה ל-Firebase AI Logic והטמעה של תקשורת LLM
- יצירת הנחיות יעילות – יוצרים הנחיה למערכת שמנחה את ה-LLM להבין תיאורים של צבעים
- הצהרות על פונקציות – הגדרה של כלים שה-LLM יכול להשתמש בהם כדי להגדיר צבעים באפליקציה
- טיפול בכלי – עיבוד של קריאות לפונקציות מ-LLM וקישור שלהן למצב האפליקציה
- סטרימינג של תשובות – שיפור חוויית המשתמש באמצעות סטרימינג בזמן אמת של תשובות מ-LLM
- סנכרון הקשר של מודל שפה גדול (LLM) – יצירת חוויה עקבית על ידי עדכון מודל השפה הגדול בפעולות המשתמש
מה תלמדו
- הגדרת Firebase AI Logic לאפליקציות Flutter
- כתיבת הנחיות מערכת יעילות כדי להנחות את ההתנהגות של מודל LLM
- הטמעה של הצהרות פונקציה שמגשרות בין שפה טבעית לבין תכונות של האפליקציה
- עיבוד תשובות בסטרימינג כדי לשפר את חוויית המשתמש
- סנכרון מצב בין אירועים בממשק המשתמש לבין מודל שפה גדול (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 ו-streams
- חשבון 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 custom_lint
הפעולה הזו תוסיף את חבילות המפתח הבאות:
-
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
הגדרת אפשרויות ניתוח
מוסיפים את custom_lint לקובץ analysis_options.yaml ברמה הבסיסית של הפרויקט:
include: package:flutter_lints/flutter.yaml
analyzer:
plugins:
- custom_lint
ההגדרה הזו מאפשרת להשתמש ב-lints ספציפיים ל-Riverpod כדי לשמור על איכות הקוד.
הטמעה של הקובץ 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 מספקת רכיבי ממשק משתמש מוכנים מראש וכלים לניהול מצב:
- MainScreen: רכיב ממשק המשתמש הראשי שמציג:
- פריסת מסך מפוצל במחשב (אזור אינטראקציה וחלונית יומן)
- ממשק עם כרטיסיות בנייד
- תצוגה צבעונית, ממשק צ'אט ותמונות ממוזערות של ההיסטוריה
- ניהול מצב: האפליקציה משתמשת בכמה התראות על מצב:
- ChatStateNotifier: ניהול הודעות הצ'אט
- ColorStateNotifier: ניהול הצבע הנוכחי וההיסטוריה
- LogStateNotifier: מנהל את הרשומות ביומן לצורך ניפוי באגים
- טיפול בהודעות: האפליקציה משתמשת במודל הודעות עם מצבים שונים:
- הודעות למשתמש: הודעות שהוזנו על ידי המשתמש
- הודעות LLM: נוצרות על ידי מודל שפה גדול (או שירות האקו שלכם בשלב הזה)
- MessageState: מעקב אחרי השלמת הודעות של מודל שפה גדול (LLM) או אחרי סטרימינג של הודעות
ארכיטקטורה של אפליקציות
הארכיטקטורה של האפליקציה היא כזו:
- שכבת ממשק המשתמש: מסופקת על ידי חבילת
colorist_ui - ניהול מצב: שימוש ב-Riverpod לניהול מצב תגובתי
- שכבת השירות: כרגע מכילה את שירות האקו הפשוט שלכם, והיא תוחלף בשירות Gemini Chat
- שילוב LLM: יתווסף בשלבים מאוחרים יותר
ההפרדה הזו מאפשרת לכם להתמקד בהטמעה של שילוב ה-LLM, בזמן שרכיבי ממשק המשתמש כבר מטופלים.
הפעלת האפליקציה
מריצים את האפליקציה באמצעות הפקודה הבאה:
flutter run -d DEVICE
מחליפים את DEVICE במכשיר היעד, כמו macos, windows, chrome או מזהה מכשיר.

עכשיו אמורה להופיע אפליקציית Colorist עם:
- אזור תצוגה של צבע עם צבע ברירת מחדל
- ממשק צ'אט שבו אפשר להקליד הודעות
- חלונית יומן שבה מוצגות האינטראקציות בצ'אט
מנסים להקליד הודעה כמו "אני רוצה צבע כחול עמוק" ולוחצים על סמל השליחה. שירות האקו פשוט יחזור על ההודעה שלכם. בשלבים הבאים, תחליפו את זה בפרשנות צבעים בפועל באמצעות Firebase AI Logic.
מה השלב הבא?
בשלב הבא, תגדירו את Firebase ותטמיעו שילוב בסיסי של Gemini API כדי להחליף את שירות האקו בשירות הצ'אט של Gemini. כך האפליקציה תוכל לפרש תיאורים של צבעים ולספק תשובות חכמות.
פתרון בעיות
בעיות בחבילת ממשק המשתמש
אם נתקלים בבעיות בחבילה colorist_ui:
- מוודאים שמשתמשים בגרסה העדכנית ביותר
- מוודאים שהוספתם את התלות בצורה נכונה
- בדיקה אם יש גרסאות חבילה שמתנגשות
שגיאות בנייה
אם מופיעות שגיאות בבנייה:
- מוודאים שמותקנת הגרסה האחרונה של Flutter SDK בערוץ היציב
- מריצים את
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
- עוברים אל מסוף Firebase ונכנסים באמצעות חשבון Google.
- לוחצים על יצירת פרויקט Firebase או בוחרים פרויקט קיים.
- פועלים לפי ההוראות באשף ההגדרה כדי ליצור את הפרויקט.
הגדרת Firebase AI Logic בפרויקט Firebase
- במסוף Firebase, עוברים לפרויקט.
- בסרגל הצד שמימין, לוחצים על AI.
- בתפריט הנפתח של ה-AI, בוחרים באפשרות לוגיקת AI.
- בכרטיס 'Firebase AI Logic', לוחצים על תחילת העבודה.
- פועלים לפי ההנחיות כדי להפעיל את Gemini Developer API בפרויקט.
התקנת FlutterFire CLI
ה-CLI של FlutterFire מפשט את ההגדרה של Firebase באפליקציות Flutter:
dart pub global activate flutterfire_cli
הוספת Firebase לאפליקציית Flutter
- מוסיפים את חבילות Firebase core ו-Firebase AI Logic לפרויקט:
flutter pub add firebase_core firebase_ai
- מריצים את פקודת ההגדרה של FlutterFire:
flutterfire configure
הפקודה הזו:
- מבקשים לבחור את הפרויקט ב-Firebase שיצרתם
- רישום אפליקציות Flutter ב-Firebase
- יצירת קובץ
firebase_options.dartעם הגדרות הפרויקט
הפקודה תזהה באופן אוטומטי את הפלטפורמות שבחרתם (iOS, Android, macOS, Windows, web) ותגדיר אותן בהתאם.
הגדרה ספציפית לפלטפורמה
ב-Firebase נדרשות גרסאות מינימליות גבוהות יותר מאלה שמוגדרות כברירת מחדל ב-Flutter. נדרשת גם גישה לרשת כדי לתקשר עם שרתי Firebase AI Logic.
הגדרת הרשאות ב-macOS
ב-macOS, צריך להפעיל גישה לרשת בהרשאות של האפליקציה:
- פותחים את
macos/Runner/DebugProfile.entitlementsומוסיפים:
macos/Runner/DebugProfile.entitlements
<key>com.apple.security.network.client</key>
<true/>
- פותחים גם את
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(keepAlive: true)
Future<FirebaseApp> firebaseApp(Ref ref) =>
Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
@Riverpod(keepAlive: true)
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. הקוד הזה משתמש בגישה מבוססת-אנוטציות של Riverpod 3 עם דפוסי ספק מעודכנים.
-
firebaseAppProvider: מאתחל את Firebase עם הגדרות הפרויקט -
geminiModelProvider: יצירת מופע של מודל גנרטיבי של Gemini -
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(keepAlive: true)
GeminiChatService geminiChatService(Ref ref) => GeminiChatService(ref);
השירות הזה:
- מקבל הודעות מהמשתמש ושולח אותן ל-Gemini API
- עדכון ממשק הצ'אט עם תשובות מהמודל
- רישום כל התקשורת כדי להבין בקלות את התהליך האמיתי של מודל שפה גדול
- מטפל בשגיאות באמצעות משוב מתאים למשתמש
הערה: בשלב הזה, חלון היומן ייראה כמעט זהה לחלון הצ'אט. היומן יהיה מעניין יותר אחרי שתציגו קריאות לפונקציות ואז תגובות בסטרימינג.
יצירת קוד 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),
),
);
}
}
השינויים העיקריים בעדכון הזה הם:
- החלפת שירות האקו בשירות צ'אט מבוסס Gemini API
- הוספת מסכי טעינה ושגיאה באמצעות התבנית
AsyncValueשל Riverpod עם השיטהwhen - חיבור ממשק המשתמש לשירות הצ'אט החדש באמצעות
sendMessageקריאה חוזרת (callback)
הפעלת האפליקציה
מריצים את האפליקציה באמצעות הפקודה הבאה:
flutter run -d DEVICE
מחליפים את DEVICE במכשיר היעד, כמו macos, windows, chrome או מזהה מכשיר.

עכשיו, כשמקלידים הודעה, היא נשלחת אל Gemini API, ומקבלים תשובה מ-LLM ולא הד. בחלונית היומן יוצגו האינטראקציות עם ה-API.
הסבר על תקשורת עם מודלים גדולים של שפה
כדאי להבין מה קורה כשמתקשרים עם Gemini API:
תהליך התקשורת
- קלט של משתמשים: המשתמש מזין טקסט בממשק הצ'אט
- עיצוב הבקשה: האפליקציה מעצבת את הטקסט כאובייקט
Contentעבור Gemini API - תקשורת עם API: הטקסט נשלח אל Gemini API דרך Firebase AI Logic
- עיבוד LLM: מודל Gemini מעבד את הטקסט ומפיק תשובה
- טיפול בתגובה: האפליקציה מקבלת את התגובה ומעדכנת את ממשק המשתמש
- רישום ביומן: כל התקשורת נרשמת ביומן לצורך שקיפות
סשנים של צ'אט והקשר של השיחה
במהלך שיחת הצ'אט עם Gemini, ההקשר נשמר בין ההודעות, כך שאפשר לנהל אינטראקציות כמו בשיחה רגילה. המשמעות היא שמודל ה-LLM 'זוכר' את ההחלפות הקודמות בסשן הנוכחי, וכך מאפשר שיחות עקביות יותר.
האנוטציה keepAlive: true בספק של שיחת הצ'אט מבטיחה שההקשר הזה יישמר לאורך מחזור החיים של האפליקציה. ההקשר הקבוע הזה חיוני לשמירה על רצף שיחה טבעי עם מודל ה-LLM.
מה השלב הבא?
בשלב הזה, אפשר לשאול את Gemini API כל דבר, כי אין הגבלות על מה שהוא יכול לענות. לדוגמה, אפשר לבקש ממנו סיכום של מלחמות השושנים, שלא קשורות למטרה של אפליקציית הצבעים.
בשלב הבא, תיצרו הנחיית מערכת שתעזור ל-Gemini לפרש תיאורי צבע בצורה יעילה יותר. ההדגמה תראה איך להתאים אישית את ההתנהגות של מודל LLM לצרכים ספציפיים של אפליקציה, ולמקד את היכולות שלו בדומיין של האפליקציה.
פתרון בעיות
בעיות בהגדרות של Firebase
אם נתקלתם בשגיאות בהפעלת Firebase:
- מוודאים שקובץ
firebase_options.dartנוצר בצורה נכונה - איך מוודאים ששדרגתם לתוכנית Blaze כדי לקבל גישה ל-Firebase AI Logic
שגיאות בגישה ל-API
אם אתם מקבלים שגיאות בגישה ל-Gemini API:
- מוודאים שהחיוב מוגדר בצורה תקינה בפרויקט Firebase
- מוודאים שהפעלתם את Firebase AI Logic ואת 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, כי הן:
- שמירה על עקביות: הנחיית המודל לספק תשובות בפורמט עקבי
- שיפור הרלוונטיות: מיקוד המודל בדומיין הספציפי שלכם (במקרה שלכם, צבעים)
- הגדרת גבולות: הגדרת הפעולות שהמודל צריך לבצע והפעולות שהוא לא צריך לבצע
- שיפור חוויית המשתמש: יצירת דפוס אינטראקציה טבעי ומועיל יותר
- צמצום העיבוד שלאחר העיבוד: קבלת תשובות בפורמטים שקל יותר לנתח או להציג
באפליקציית 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
הסבר על מבנה הנחיית המערכת
בואו ננתח את ההנחיה הזו:
- הגדרת התפקיד: הגדרת ה-LLM כ "עוזר מומחה לצבעים"
- הסבר על המשימה: הגדרת המשימה העיקרית כתרגום תיאורי צבעים לערכי RGB
- פורמט התגובה: הגדרה מדויקת של פורמט ערכי ה-RGB כדי לשמור על עקביות
- דוגמה לשיחה: דוגמה קונקרטית של דפוס האינטראקציה הצפוי
- טיפול במקרי קצה: הוראות לטיפול בתיאורים לא ברורים
- מגבלות והנחיות: הגדרת גבולות כמו שמירה על ערכי 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(keepAlive: true)
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(keepAlive: true)
Future<FirebaseApp> firebaseApp(Ref ref) =>
Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
@Riverpod(keepAlive: true)
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

כדאי לנסות לבדוק אותו עם תיאורים שונים של צבעים:
- "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) ויכולות להשפיע באופן משמעותי על מידת התועלת של המודל עבור האפליקציה הספציפית שלכם. מה שעשיתם כאן הוא סוג של הנדסת הנחיות – התאמת ההוראות כדי שהמודל יתנהג בצורה שתתאים לצרכים של האפליקציה.
הנדסת הנחיות יעילה כוללת:
- הגדרה ברורה של התפקיד: הגדרה של מטרת ה-LLM
- הוראות מפורשות: פירוט מדויק של אופן התגובה של ה-LLM
- דוגמאות קונקרטיות: במקום רק להסביר איך נראות תשובות טובות, אנחנו מראים לכם דוגמאות.
- טיפול במקרי קצה: הנחיית ה-LLM לגבי אופן הטיפול בתרחישים מעורפלים
- מפרטים של פורמטים: כדי לוודא שהתשובות בנויות בצורה עקבית ושימושית
ההנחיה למערכת שיצרתם הופכת את היכולות הכלליות של 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
- עדכון ההנחיה למערכת כדי להשתמש ביכולות של הכלי
הסבר על בקשה להפעלת פונקציה
לפני שמטמיעים הצהרות על פונקציות, כדאי להבין מה הן ולמה הן חשובות:
מהי בקשה להפעלת פונקציה?
בקשה להפעלת פונקציה (שנקראת לפעמים "שימוש בכלי") היא יכולת שמאפשרת למודל LLM:
- לזהות מתי כדאי להפעיל פונקציה ספציפית כדי לענות על בקשת משתמש
- יצירת אובייקט JSON מובנה עם הפרמטרים שנדרשים לפונקציה הזו
- מאפשרים לאפליקציה להפעיל את הפונקציה עם הפרמטרים האלה
- מקבל את התוצאה של הפונקציה ומשלב אותה בתשובה שלו
במקום שה-LLM רק יתאר מה צריך לעשות, קריאה לפונקציה מאפשרת ל-LLM להפעיל פעולות קונקרטיות באפליקציה.
למה בקשות להפעלת פונקציות חשובות לאפליקציות Flutter
הפעלת פונקציות יוצרת גשר חזק בין שפה טבעית לבין תכונות של אפליקציות:
- פעולה ישירה: משתמשים יכולים לתאר מה הם רוצים בשפה טבעית, והאפליקציה מגיבה בפעולות קונקרטיות
- פלט מובנה: מודל ה-LLM יוצר נתונים מובנים ונקיים במקום טקסט שצריך לנתח
- פעולות מורכבות: מאפשרות ל-LLM לגשת לנתונים חיצוניים, לבצע חישובים או לשנות את מצב האפליקציה
- חוויית משתמש טובה יותר: שילוב חלק בין שיחה לפונקציונליות
באפליקציית 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(keepAlive: true)
GeminiTools geminiTools(Ref ref) => GeminiTools(ref);
הסבר על הצהרות פונקציות
ננסה להסביר מה הקוד הזה עושה:
- שם הפונקציה: נותנים לפונקציה שם
set_colorכדי לציין בבירור את המטרה שלה - תיאור הפונקציה: אתם מספקים תיאור ברור שעוזר למודל ה-LLM להבין מתי להשתמש בה
- הגדרות של פרמטרים: אתם מגדירים פרמטרים מובנים עם תיאורים משלהם:
-
red: הרכיב האדום של RGB, שמוגדר כמספר בין 0.0 ל-1.0 -
green: הרכיב הירוק של RGB, שמוגדר כמספר בין 0.0 ל-1.0 -
blue: הרכיב הכחול של RGB, שמוגדר כמספר בין 0.0 ל-1.0
-
- סוגי סכימה: משתמשים ב-
Schema.number()כדי לציין שמדובר בערכים מספריים - אוסף כלי עבודה: אתם יוצרים רשימה של כלים שמכילה את הצהרת הפונקציה
הגישה המובנית הזו עוזרת למודל השפה הגדול של 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(keepAlive: true)
Future<FirebaseApp> firebaseApp(Ref ref) =>
Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
@Riverpod(keepAlive: true)
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
השינויים העיקריים בהנחיית המערכת הם:
- הסבר על הכלי: במקום לבקש ערכי RGB מעוצבים, עכשיו אפשר לספר למודל שפה גדול על הכלי
set_color - תהליך שונה: שינוי שלב 3 מ'עיצוב הערכים בתגובה' ל'שימוש בכלי להגדרת ערכים'
- דוגמה מעודכנת: אתם מראים איך התגובה צריכה לכלול קריאה לכלי במקום טקסט מעוצב
- הסרנו את דרישת הפורמט: מכיוון שאתם משתמשים בקריאות פונקציה מובנות, אתם כבר לא צריכים פורמט טקסט ספציפי
ההנחיה המעודכנת הזו מכוונת את מודל ה-LLM להשתמש בקריאה לפונקציה במקום לספק רק ערכי RGB בפורמט טקסט.
יצירת קוד Riverpod
מריצים את הפקודה של כלי ההרצה של ה-build כדי ליצור את קוד Riverpod הנדרש:
dart run build_runner build --delete-conflicting-outputs
הפעלת האפליקציה
בשלב הזה, Gemini ייצור תוכן שינסה להשתמש בקריאות לפונקציות, אבל עדיין לא הטמעתם את ה-handlers לקריאות לפונקציות. כשמפעילים את האפליקציה ומתארים צבע, רואים את Gemini מגיב כאילו הוא הפעיל כלי, אבל לא רואים שינויים בצבע בממשק המשתמש עד לשלב הבא.
מפעילים את האפליקציה:
flutter run -d DEVICE

נסו לתאר צבע כמו "כחול עמוק של האוקיינוס" או "ירוק יער" ותראו את התשובות. מודל ה-LLM מנסה לקרוא לפונקציות שהוגדרו למעלה, אבל הקוד שלכם עדיין לא מזהה קריאות לפונקציות.
תהליך הבקשה להפעלת פונקציה
כדי להבין מה קורה כש-Gemini משתמש בבקשה להפעלת פונקציה:
- בחירת פונקציה: מודל ה-LLM מחליט אם בקשה להפעלת פונקציה תהיה מועילה על סמך הבקשה של המשתמש
- יצירת פרמטרים: מודל ה-LLM יוצר ערכי פרמטרים שמתאימים לסכימה של הפונקציה
- פורמט של בקשה להפעלת פונקציה: מודל ה-LLM שולח בתגובה אובייקט מובנה של בקשה להפעלת פונקציה
- טיפול באפליקציה: האפליקציה תקבל את הקריאה הזו ותבצע את הפונקציה הרלוונטית (שמיושמת בשלב הבא)
- שילוב תשובות: בשיחות מרובות תפניות, מודל ה-LLM מצפה שהתוצאה של הפונקציה תוחזר
במצב הנוכחי של האפליקציה, שלושת השלבים הראשונים מתרחשים, אבל עדיין לא הטמעתם את שלב 4 או 5 (טיפול בקריאות לפונקציות), שתעשו בשלב הבא.
פרטים טכניים: איך Gemini מחליט מתי להשתמש בפונקציות
Gemini מקבל החלטות חכמות לגבי מתי להשתמש בפונקציות על סמך:
- כוונת המשתמש: האם הפונקציה היא הדרך הכי טובה להשיב לבקשת המשתמש
- רלוונטיות הפונקציה: עד כמה הפונקציות הזמינות מתאימות למשימה
- זמינות הפרמטר: האם אפשר לקבוע את ערכי הפרמטרים בוודאות
- הוראות מערכת: הנחיות מההנחיה של המערכת לגבי השימוש בפונקציות
הגדרתם את Gemini לזהות בקשות לתיאור צבעים כהזדמנויות להפעיל את הפונקציה set_color על ידי מתן הצהרות ברורות על הפונקציה והוראות למערכת.
מה השלב הבא?
בשלב הבא, תטמיעו את ה-handlers של הבקשות להפעלת פונקציות שמגיעות מ-Gemini. כך נסגר המעגל, והתיאורים של המשתמשים יכולים להפעיל שינויים בפועל בצבעים בממשק המשתמש באמצעות קריאות לפונקציות של מודל ה-LLM.
פתרון בעיות
בעיות בהצהרת פונקציה
אם נתקלים בשגיאות בהצהרות על פונקציות:
- מוודאים שהשמות והסוגים של הפרמטרים תואמים למה שצפוי
- מוודאים ששם הפונקציה ברור ומתאר את הפעולה שלה
- צריך לוודא שתיאור הפונקציה מסביר באופן מדויק את המטרה שלה
בעיות בהנחיות למערכת
אם ה-LLM לא מנסה להשתמש בפונקציה:
- מוודאים שההנחיה למערכת מורה בבירור למודל שפה גדול (LLM) להשתמש בכלי
set_color - בודקים שהדוגמה בהנחיית המערכת מדגימה את השימוש בפונקציה
- כדאי לנסות להסביר בצורה ברורה יותר איך להשתמש בכלי
בעיות כלליות
אם נתקלים בבעיות אחרות:
- בדיקה אם יש שגיאות במסוף שקשורות להצהרות על פונקציות
- מוודאים שהכלים מועברים למודל בצורה תקינה
- מוודאים שכל הקוד שנוצר על ידי Riverpod מעודכן
מושגים מרכזיים שנלמדו
- הגדרת הצהרות על פונקציות כדי להרחיב את היכולות של מודלים גדולים של שפה באפליקציות Flutter
- יצירת סכימות של פרמטרים לאיסוף נתונים מובנים
- שילוב הצהרות פונקציות עם מודל Gemini
- עדכון הנחיות המערכת כדי לעודד שימוש בפונקציות
- איך מודלים גדולים של שפה (LLM) בוחרים פונקציות ומפעילים אותן
השלב הזה מדגים איך מודלים גדולים של שפה (LLM) יכולים לגשר על הפער בין קלט בשפה טבעית לבין קריאות פונקציה מובנות, ובכך ליצור בסיס לשילוב חלק בין שיחה לבין תכונות של אפליקציה.
6. הטמעה של טיפול בכלי
בשלב הזה, מטמיעים את ה-handlers של הקריאות לפונקציות שמגיעות מ-Gemini. כך נסגר מעגל התקשורת בין קלט בשפה טבעית לבין תכונות קונקרטיות של האפליקציה, והמודל LLM יכול לתפעל ישירות את ממשק המשתמש על סמך תיאורים של המשתמש.
מה תלמדו בשלב הזה
- הסבר על פייפליין מלא של בקשות להפעלת פונקציות באפליקציות LLM
- עיבוד בקשות להפעלת פונקציות מ-Gemini באפליקציית Flutter
- הטמעה של פונקציות לטיפול בבקשות שמשנות את מצב האפליקציה
- טיפול בתגובות של פונקציות והחזרת תוצאות ל-LLM
- יצירת זרימת תקשורת מלאה בין LLM לממשק המשתמש
- רישום של קריאות לפונקציות ותשובות לשקיפות
הסבר על צינור עיבוד הנתונים של קריאות לפונקציות
לפני שמתחילים בהטמעה, כדאי להבין את תהליך השימוש המלא בפונקציות:
התהליך מקצה לקצה
- קלט של משתמשים: המשתמש מתאר צבע בשפה טבעית (למשל, "ירוק יער")
- עיבוד LLM: Gemini מנתח את התיאור ומחליט להפעיל את הפונקציה
set_color - יצירת בקשה להפעלת פונקציה: Gemini יוצר JSON מובנה עם פרמטרים (ערכים של אדום, ירוק וכחול)
- קבלת בקשה להפעלת פונקציה: האפליקציה מקבלת את הנתונים המובנים האלה מ-Gemini
- הפעלת הפונקציה: האפליקציה מפעילה את הפונקציה עם הפרמטרים שסופקו
- עדכון מצב: הפונקציה מעדכנת את המצב של האפליקציה (שינוי הצבע שמוצג)
- יצירת תשובה: הפונקציה מחזירה תוצאות למודל ה-LLM
- שילוב התשובה: מודל ה-LLM משלב את התוצאות האלה בתשובה הסופית
- עדכון ממשק המשתמש: ממשק המשתמש מגיב לשינוי המצב ומציג את הצבע החדש
מחזור התקשורת המלא חיוני לשילוב נכון של 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(keepAlive: true)
GeminiTools geminiTools(Ref ref) => GeminiTools(ref);
הסבר על פונקציות לטיפול באירועים
בואו נראה מה עושים ה-handlers של הפונקציות האלה:
-
handleFunctionCall: רכיב מרכזי לניתוב בקשות ש:- תיעוד של בקשה להפעלת פונקציה לצורך שקיפות בחלונית היומן
- הפונקציה מכוונת ל-handler המתאים על סמך שם הפונקציה
- החזרת תגובה מובנית שתשלח חזרה למודל שפה גדול
-
handleSetColor: שם הפונקציה הספציפית שלset_colorש:- חילוץ ערכי RGB ממפת הארגומנטים
- ממירה אותם לסוגים הצפויים (מספרים ממשיים)
- עדכון מצב הצבע של האפליקציה באמצעות
colorStateNotifier - יצירת תגובה מובנית עם סטטוס הצלחה ומידע על הצבע הנוכחי
- הפונקציה מתעדת ביומן את התוצאות לצורך ניפוי באגים
-
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(keepAlive: true)
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);
}
}
הקוד הזה:
- בודקת אם התשובה של ה-LLM מכילה קריאות לפונקציות
- לכל בקשה להפעלת פונקציה, מפעיל את השיטה
handleFunctionCallעם שם הפונקציה והארגומנטים - איסוף התוצאות של כל בקשה להפעלת פונקציה
- התוצאות האלה נשלחות חזרה למודל שפה גדול (LLM) באמצעות
Content.functionResponses - עיבוד התשובה של ה-LLM לתוצאות הפונקציה
- עדכון ממשק המשתמש עם הטקסט הסופי של התשובה
כך נוצר תהליך הלוך ושוב:
- משתמש ← LLM: בקשה לצבע
- מודל LLM → אפליקציה: בקשות להפעלת פונקציות עם פרמטרים
- אפליקציה ← משתמש: צבע חדש מוצג
- אפליקציה ← LLM: תוצאות של פונקציות
- מודל שפה גדול (LLM) ← משתמש: תשובה סופית שכוללת תוצאות של פונקציות
יצירת קוד Riverpod
מריצים את הפקודה של כלי ההרצה של ה-build כדי ליצור את קוד Riverpod הנדרש:
dart run build_runner build --delete-conflicting-outputs
הפעלה ובדיקה של התהליך המלא
עכשיו מריצים את האפליקציה:
flutter run -d DEVICE

נסו להזין תיאורים שונים של צבעים:
- "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" (תעשה את זה סגול מלכותי עשיר)
עכשיו אמורים להופיע:
- ההודעה שלכם מופיעה בממשק הצ'אט
- התשובה של Gemini מופיעה בצ'אט
- בקשות להפעלת פונקציות שמתועדות בחלונית היומן
- תוצאות הפונקציה מתועדות ביומן מיד אחרי
- מלבן הצבעים מתעדכן ומציג את הצבע שמתואר
- ערכי ה-RGB מתעדכנים כדי להציג את הרכיבים של הצבע החדש
- התשובה הסופית של Gemini מופיעה, ולרוב כוללת הערה על הצבע שהוגדר
חלונית היומן מספקת תובנות לגבי מה שקורה מאחורי הקלעים. הפרטים שמוצגים הם:
- הבקשות המדויקות להפעלת פונקציות ש-Gemini מבצע
- הפרמטרים שנבחרו לכל ערך RGB
- התוצאות שהפונקציה מחזירה
- התשובות הנוספות מ-Gemini
התראות על מצב הצבע
ה-colorStateNotifier שבו אתם משתמשים כדי לעדכן את הצבעים הוא חלק מחבילת colorist_ui. הוא מנהל:
- הצבע הנוכחי שמוצג בממשק המשתמש
- היסטוריית הצבעים (10 הצבעים האחרונים)
- התראה על שינויים במצב של רכיבי ממשק משתמש
כשקוראים לפונקציה updateColor עם ערכי RGB חדשים, היא:
- יוצרת אובייקט
ColorDataחדש עם הערכים שסופקו - עדכון הצבע הנוכחי במצב האפליקציה
- הצבע יתווסף להיסטוריה
- מפעיל עדכונים בממשק המשתמש באמצעות ניהול המצב של Riverpod
רכיבי ממשק המשתמש בחבילה colorist_ui עוקבים אחרי המצב הזה ומתעדכנים אוטומטית כשהוא משתנה, וכך יוצרים חוויה דינמית.
הסבר על טיפול בשגיאות
ההטמעה שלך כוללת טיפול אמין בשגיאות:
- בלוק try-catch: עוטף את כל האינטראקציות עם LLM כדי לזהות חריגים
- רישום שגיאות ביומן: רישום שגיאות בחלונית היומן עם עקבות מחסנית
- משוב משתמשים: מספק הודעת שגיאה ידידותית בצ'אט
- ניקוי המצב: השלמת מצב ההודעה גם אם מתרחשת שגיאה
כך האפליקציה נשארת יציבה ומספקת משוב מתאים גם כשמתרחשות בעיות בשירות LLM או בהפעלת הפונקציה.
היתרונות של שימוש בפונקציות לשיפור חוויית המשתמש
ההישגים שלכם כאן מדגימים איך מודלים גדולים של שפה יכולים ליצור ממשקי משתמש טבעיים ועוצמתיים:
- ממשק בשפה טבעית: המשתמשים מביעים כוונות בשפה יומיומית
- פרשנות חכמה: מודל ה-LLM מתרגם תיאורים מעורפלים לערכים מדויקים
- מניפולציה ישירה: ממשק המשתמש מתעדכן בתגובה לשפה טבעית
- תשובות לפי ההקשר: מודל ה-LLM מספק הקשר לשיחה לגבי השינויים
- עומס קוגניטיבי נמוך: המשתמשים לא צריכים להבין ערכי 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): תרגום של קלט בשפה טבעית לפעולות קונקרטיות בממשק המשתמש, תוך שמירה על שיחה עקבית שמתייחסת לפעולות האלה. כך נוצר ממשק שיחה אינטואיטיבי שמרגיש למשתמשים כמו קסם.
7. סטרימינג של תשובות לשיפור חוויית המשתמש
בשלב הזה, תשפרו את חוויית המשתמש על ידי הטמעה של תשובות בסטרימינג מ-Gemini. במקום לחכות ליצירת התשובה המלאה, תוכלו לעבד את חלקי הטקסט ואת הקריאות לפונקציות כשהם מתקבלים, וכך ליצור אפליקציה רספונסיבית ומושכת יותר.
מה כולל השלב הזה
- החשיבות של סטרימינג באפליקציות מבוססות-LLM
- הטמעה של תשובות LLM בסטרימינג באפליקציית Flutter
- עיבוד של חלקי טקסט שמגיעים מה-API
- ניהול מצב השיחה כדי למנוע התנגשויות בין הודעות
- טיפול בהפעלות של פונקציות בתגובות סטרימינג
- יצירת אינדיקטורים חזותיים לתשובות שנמצאות בתהליך
למה סטרימינג חשוב לאפליקציות של מודלים גדולים של שפה
לפני שמטמיעים את התכונה, חשוב להבין למה סטרימינג של תשובות הוא חיוני ליצירת חוויות משתמש מצוינות באמצעות מודלים מסוג LLM:
חוויית משתמש משופרת
לתשובות שמוצגות בסטרימינג יש כמה יתרונות משמעותיים מבחינת חוויית המשתמש:
- השהיה מורגשת נמוכה יותר: המשתמשים רואים את הטקסט מתחיל להופיע מיד (בדרך כלל תוך 100-300 אלפיות השנייה), במקום לחכות כמה שניות לתשובה מלאה. התחושה הזו של מיידיות משפרת באופן משמעותי את שביעות רצון המשתמשים.
- קצב שיחה טבעי: הטקסט מופיע בהדרגה, כמו בשיחה בין בני אדם, וכך נוצרת חוויה טבעית יותר של דיאלוג.
- עיבוד מידע הדרגתי: המשתמשים יכולים להתחיל לעבד מידע כשהוא מגיע, במקום להיות מוצפים בבלוק טקסט גדול בבת אחת.
- הזדמנות להפריע מוקדם: באפליקציה מלאה, המשתמשים יכולים להפריע ל-LLM או להפנות אותו מחדש אם הם רואים שהוא מתקדם בכיוון לא מועיל.
- אישור חזותי של הפעילות: הטקסט בסטרימינג מספק משוב מיידי שהמערכת פועלת, וכך מצמצם את אי הוודאות.
יתרונות טכניים
בנוסף לשיפורים בחוויית המשתמש, הסטרימינג מציע יתרונות טכניים:
- הפעלה מוקדמת של פונקציות: אפשר לזהות ולהפעיל קריאות לפונקציות ברגע שהן מופיעות בזרם, בלי לחכות לתשובה המלאה.
- עדכונים מצטברים בממשק המשתמש: אתם יכולים לעדכן את ממשק המשתמש באופן הדרגתי ככל שמתקבל מידע חדש, וכך ליצור חוויה דינמית יותר.
- ניהול מצב השיחה: הסטרימינג מספק אותות ברורים לגבי השלמת התשובות לעומת תשובות שנמצאות עדיין בתהליך, וכך מאפשר ניהול טוב יותר של המצב.
- צמצום הסיכונים שקשורים לפסק זמן: בתגובות שאינן סטרימינג, יש סיכון לפסק זמן בחיבור כשמייצרים תוכן לאורך זמן. הסטרימינג יוצר את החיבור מוקדם יותר ושומר עליו.
באפליקציית 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';
class ConversationStateNotifier extends Notifier<ConversationState> { // Add from here...
@override
ConversationState build() => ConversationState.idle;
void busy() {
state = ConversationState.busy;
}
void idle() {
state = ConversationState.idle;
}
}
final conversationStateProvider =
NotifierProvider<ConversationStateNotifier, ConversationState>(
ConversationStateNotifier.new,
); // 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.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.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(keepAlive: true)
GeminiChatService geminiChatService(Ref ref) => GeminiChatService(ref);
הסבר על הטמעת הסטרימינג
ננסה להסביר מה הקוד הזה עושה:
- מעקב אחרי מצב השיחה:
-
conversationStateProviderעוקב אחרי העיבוד של התשובה באפליקציה - המעבר בין המצבים הוא
idle→busyבמהלך העיבוד, ואז חזרה ל-idle - כך נמנעות כמה בקשות בו-זמניות שעלולות להתנגש
-
- הפעלת הסטרימינג:
-
sendMessageStream()מחזירה זרם של חלקי תשובה במקוםFutureעם התשובה המלאה - כל מקטע יכול להכיל טקסט, קריאות לפונקציות או את שניהם
-
- עיבוד הדרגתי:
-
await forמעבד כל חלק בזמן אמת כשהוא מגיע - הטקסט מתווסף לממשק המשתמש באופן מיידי, ויוצר את אפקט הסטרימינג
- הפעלת פונקציות מתבצעת ברגע שהן מזוהות
-
- טיפול בבקשות להפעלת פונקציות:
- כשמזוהה בקשה להפעלת פונקציה בחלק של טקסט, היא מופעלת באופן מיידי
- התוצאות נשלחות חזרה ל-LLM באמצעות שיחת סטרימינג נוספת
- גם התשובה של ה-LLM לתוצאות האלה מעובדת בסטרימינג
- טיפול בשגיאות וניקוי:
-
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 כדי ליצור את קוד Riverpod הנדרש:
dart run build_runner build --delete-conflicting-outputs
הפעלה ובדיקה של תשובות בסטרימינג
מריצים את האפליקציה:
flutter run -d DEVICE

עכשיו נסו לבדוק את התנהגות הסטרימינג עם תיאורים שונים של צבעים. אפשר לנסות תיאורים כמו:
- "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(), קורים הדברים הבאים:
- האפליקציה יוצרת חיבור לשירות Firebase AI Logic
- בקשת המשתמש נשלחת לשירות
- השרת מתחיל לעבד את הבקשה
- החיבור לשידור נשאר פתוח ומוכן להעברת נתונים
הפצת מקטעים
במהלך יצירת התוכן על ידי Gemini, נשלחים נתחים דרך הזרם:
- השרת שולח נתחי טקסט כשהם נוצרים (בדרך כלל כמה מילים או משפטים)
- כש-Gemini מחליט לבצע בקשה להפעלת פונקציה, הוא שולח את פרטי הבקשה להפעלת פונקציה
- יכול להיות שיהיו עוד נתחי טקסט אחרי בקשות להפעלת פונקציות
- הזרם ממשיך עד שהיצירה מסתיימת
עיבוד הדרגתי
האפליקציה מעבדת כל נתח באופן מצטבר:
- כל מקטע טקסט מתווסף לתשובה הקיימת
- הפעלת פונקציות מתבצעת ברגע שהן מזוהות
- ממשק המשתמש מתעדכן בזמן אמת עם תוצאות הטקסט והפונקציות
- הסטטוס נשמר כדי להראות שהתשובה עדיין מוזרמת
השלמת הצפייה בסטרימינג
כשהיצירה מסתיימת:
- השידור נסגר על ידי השרת
- הלולאה
await forמסתיימת באופן טבעי - ההודעה מסומנת כהודעה שהושלמה
- מצב השיחה חוזר למצב סרק
- ממשק המשתמש מתעדכן כדי לשקף את המצב שהושלם
השוואה בין סטרימינג לבין צפייה לא בסטרימינג
כדי להבין טוב יותר את היתרונות של סטרימינג, נשווה בין גישות של סטרימינג לבין גישות שאינן סטרימינג:
יחס | לא שייך לסטרימינג | סטרימינג |
זמן אחזור נתפס | המשתמש לא רואה כלום עד שהתשובה מוכנה | המשתמש רואה את המילים הראשונות תוך אלפיות שנייה |
חוויית משתמש | המתנה ארוכה ואז הטקסט מופיע פתאום | מראה טבעי ומתקדם של הטקסט |
ניהול מצב | פשוט יותר (ההודעות בהמתנה או שהן הושלמו) | מורכבת יותר (ההודעות יכולות להיות במצב סטרימינג) |
ביצוע הפונקציה | מתרחש רק אחרי שהתשובה מוכנה | מתרחש במהלך יצירת התשובה |
מורכבות ההטמעה | קל יותר להטמיע | נדרש ניהול מצב נוסף |
התאוששות משגיאה | תשובה של הכול או כלום | יכול להיות שעדיין יהיה לכם שימוש בתשובות חלקיות |
מורכבות הקוד | פחות מורכב | מורכבות גבוהה יותר בגלל הטיפול בנתונים בזמן אמת |
באפליקציה כמו Colorist, היתרונות של חוויית המשתמש בסטרימינג עולים על מורכבות ההטמעה, במיוחד כשמדובר בפרשנויות של צבעים שעשויות לקחת כמה שניות ליצירה.
שיטות מומלצות לשיפור חוויית המשתמש בסטרימינג
כשמטמיעים סטרימינג באפליקציות LLM משלכם, כדאי לפעול לפי השיטות המומלצות הבאות:
- אינדיקטורים חזותיים ברורים: חשוב לספק תמיד רמזים חזותיים ברורים שמבדילים בין הודעות בסטרימינג לבין הודעות מלאות
- חסימת קלט של משתמשים: השבתת קלט של משתמשים במהלך סטרימינג כדי למנוע בקשות חופפות מרובות
- שחזור שגיאות: עיצוב ממשק המשתמש כך שיטפל בשחזור חלק אם הסטרימינג מופסק
- מעברים בין מצבים: חשוב לוודא שהמעברים בין מצבי המתנה, סטרימינג והשלמה יהיו חלקים
- הדמיה של ההתקדמות: כדאי להשתמש באנימציות או באינדיקטורים עדינים שמציגים את העיבוד הפעיל
- אפשרויות ביטול: באפליקציה מלאה, צריך לספק למשתמשים דרכים לבטל יצירה שנמצאת בתהליך
- שילוב של תוצאות הפונקציה: עיצוב ממשק המשתמש כך שיטפל בתוצאות של פונקציות שמופיעות באמצע התהליך
- אופטימיזציה של הביצועים: צמצום הבנייה מחדש של ממשק המשתמש במהלך עדכונים מהירים של הסטרימינג
חבילת colorist_ui מיישמת בשבילכם הרבה מהשיטות המומלצות האלה, אבל חשוב לקחת אותן בחשבון בכל הטמעה של מודל שפה גדול (LLM) שמעביר נתונים בסטרימינג.
מה השלב הבא?
בשלב הבא, תטמיעו סנכרון של LLM על ידי שליחת הודעה ל-Gemini כשמשתמשים בוחרים צבעים מההיסטוריה. כך נוצרת חוויה מגובשת יותר שבה מודל ה-LLM מודע לשינויים במצב האפליקציה שהמשתמש יזם.
פתרון בעיות
בעיות בעיבוד זרמי נתונים (stream processing)
אם נתקלים בבעיות בעיבוד זרמי נתונים (stream processing):
- תסמינים: תגובות חלקיות, טקסט חסר או סיום פתאומי של הסטרימינג
- פתרון: בודקים את החיבור לרשת ומוודאים שיש דפוסי async/await תקינים בקוד
- אבחון: בודקים בחלונית היומן אם יש הודעות שגיאה או אזהרות שקשורות לעיבוד זרמי נתונים (stream processing)
- תיקון: מוודאים שכל עיבוד זרמי נתונים (stream processing) משתמש בטיפול בשגיאות נכון עם בלוקים של
try/catch
בקשות חסרות להפעלת פונקציה
אם לא מזוהות קריאות לפונקציות בזרם:
- תסמינים: הטקסט מופיע אבל הצבעים לא מתעדכנים, או שביומן לא מופיעות קריאות לפונקציות
- הפתרון: בודקים את ההנחיות בהנחיית המערכת לגבי השימוש בקריאות לפונקציות
- אבחון: בודקים בחלונית היומן אם מתקבלות קריאות לפונקציות
- פתרון: משנים את הנחיית המערכת כך שתורה למודל שפה גדול (LLM) להשתמש בכלי
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):
- שומר על ההקשר: מודל ה-LLM מקבל מידע על כל הפעולות הרלוונטיות של המשתמשים
- יצירת עקביות: יצירת חוויה מגובשת שבה מודל ה-LLM מזהה אינטראקציות עם ממשק המשתמש
- משפר את האינטליגנציה: מאפשר ל-LLM להגיב בצורה מתאימה לכל פעולות המשתמש
- שיפור חוויית המשתמש: האפליקציה מרגישה יותר משולבת ומגיבה
- מפחית את המאמץ של המשתמשים: אין צורך שהמשתמשים יסבירו באופן ידני את הפעולות שלהם בממשק המשתמש
באפליקציית 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';
class ConversationStateNotifier extends Notifier<ConversationState> {
@override
ConversationState build() => ConversationState.idle;
void busy() {
state = ConversationState.busy;
}
void idle() {
state = ConversationState.idle;
}
}
final conversationStateProvider =
NotifierProvider<ConversationStateNotifier, ConversationState>(
ConversationStateNotifier.new,
);
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.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.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(keepAlive: true)
GeminiChatService geminiChatService(Ref ref) => GeminiChatService(ref);
השינוי העיקרי הוא הוספת השיטה notifyColorSelection, שכוללת:
- מקבל אובייקט
ColorDataשמייצג את הצבע שנבחר - מקודד אותו לפורמט JSON שאפשר לכלול בהודעה
- שליחת הודעה בפורמט מיוחד למודל שפה גדולה (LLM) שמציינת בחירה של משתמש
- נעשה שימוש חוזר בשיטת
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
התוספת העיקרית היא הקטע 'כשמשתמשים בוחרים צבעים היסטוריים', שבו:
- הסבר של המושג 'התראות על בחירת היסטוריה' למודל שפה גדול
- דוגמה לאופן שבו ההתראות האלה נראות
- דוגמה לתשובה מתאימה
- הגדרת ציפיות לגבי אישור הבחירה והוספת תגובה על הצבע
כך מודל ה-LLM יוכל להבין איך להגיב בצורה מתאימה להודעות המיוחדות האלה.
יצירת קוד Riverpod
מריצים את הפקודה של כלי ההרצה של ה-build כדי ליצור את קוד Riverpod הנדרש:
dart run build_runner build --delete-conflicting-outputs
הרצה ובדיקה של סנכרון ההקשר של מודל שפה גדול (LLM)
מריצים את האפליקציה:
flutter run -d DEVICE

בדיקת הסנכרון של ההקשר של מודל שפה גדול (LLM) כוללת:
- קודם, יוצרים כמה צבעים על ידי תיאור שלהם בצ'אט
- "Show me a vibrant purple" (הצגת סגול עז)
- "I'd like a forest green"
- "Give me a bright red" (תביא לי אדום בוהק)
- לאחר מכן, לוחצים על אחת מתמונות המיניאטורה של הצבעים בפס ההיסטוריה.
כדאי לשים לב לנקודות הבאות:
- הצבע שנבחר מופיע בתצוגה הראשית
- הודעה למשתמש מופיעה בצ'אט ומציינת את בחירת הצבע
- מודל ה-LLM מגיב באישור הבחירה ובהערה על הצבע
- האינטראקציה כולה מרגישה טבעית ועקבית
כך נוצרת חוויה חלקה שבה ה-LLM מודע להודעות ישירות ולפעולות בממשק המשתמש ומגיב להן בצורה מתאימה.
איך פועל סנכרון ההקשר של מודלים גדולים של שפה (LLM)
הנה הסבר על הפרטים הטכניים של הסנכרון:
זרימת נתונים
- פעולת משתמש: המשתמש לוחץ על צבע ברצועת ההיסטוריה
- אירוע בממשק המשתמש: הווידג'ט
MainScreenמזהה את הבחירה הזו - ביצוע של קריאה חוזרת (callback): הקריאה החוזרת
notifyColorSelectionמופעלת - יצירת הודעה: נוצרת הודעה בפורמט מיוחד עם נתוני הצבע
- עיבוד LLM: ההודעה נשלחת אל Gemini, שמזהה את הפורמט
- תשובה בהתאם להקשר: Gemini מגיב בצורה מתאימה על סמך הנחיית המערכת
- עדכון בממשק המשתמש: התשובה מופיעה בצ'אט, וכך נוצרת חוויה מגובשת
סריאליזציה של נתונים
היבט מרכזי בגישה הזו הוא האופן שבו מבצעים סריאליזציה של נתוני הצבע:
'User selected color from history: ${json.encode(color.toLLMContextMap())}'
השיטה toLLMContextMap() (שמסופקת על ידי חבילת colorist_ui) ממירה אובייקט ColorData למפה עם מאפייני מפתח ש-LLM יכול להבין. בדרך כלל צוות כזה כולל:
- ערכי RGB (אדום, ירוק, כחול)
- ייצוג של קוד הקסדצימלי
- כל שם או תיאור שמשויכים לצבע
אם תעצבו את הנתונים האלה באופן עקבי ותכללו אותם בהודעה, תבטיחו של-LLM יהיה כל המידע שהוא צריך כדי להגיב בצורה מתאימה.
שימושים נרחבים יותר בסנכרון הקשר של מודלים גדולים של שפה
לדפוס הזה של שליחת התראות למודל שפה גדול (LLM) לגבי אירועים בממשק המשתמש יש שימושים רבים מעבר לבחירת צבעים:
תרחישים אחרים לדוגמה
- שינויים במסננים: הודעה למודל ה-LLM כשמשתמשים מחילים מסננים על נתונים
- אירועי ניווט: מודיעים ל-LLM כשמשתמשים עוברים לקטעים שונים
- שינויים בבחירה: עדכון ה-LLM כשמשתמשים בוחרים פריטים מרשימות או מטבלאות
- עדכוני העדפות: עדכון מודל ה-LLM כשמשתמשים משנים הגדרות או העדפות
- מניפולציה של נתונים: הודעה ל-LLM כשמשתמשים מוסיפים, עורכים או מוחקים נתונים
בכל מקרה, התבנית נשארת זהה:
- זיהוי אירוע בממשק המשתמש
- סריאליזציה של נתונים רלוונטיים
- שליחת התראה בפורמט מיוחד ל-LLM
- הנחיית ה-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, ויצרתם חוויה משולבת באמת שבה ה-LLM מרגיש כמו עוזר מודע ומגיב ולא רק כמו כלי ליצירת טקסט. אפשר להשתמש בדפוס הזה בעוד הרבה אפליקציות כדי ליצור ממשקים טבעיים ואינטואיטיביים יותר שמבוססים-AI.
9. מעולה!
סיימתם בהצלחה את ה-Codelab בנושא קולוריסט! 🎉
מה יצרתם
יצרתם אפליקציית Flutter שפועלת באופן מלא ומשלבת את Google Gemini API כדי לפרש תיאורים של צבעים בשפה טבעית. האפליקציה יכולה עכשיו:
- עיבוד תיאורים בשפה טבעית כמו "כתום שקיעה" או "כחול עמוק של האוקיינוס"
- אפשר להיעזר ב-Gemini כדי לתרגם את התיאורים האלה בצורה חכמה לערכי RGB
- הצגת הצבעים המתורגמים בזמן אמת באמצעות תשובות בסטרימינג
- טיפול באינטראקציות של משתמשים באמצעות צ'אט ורכיבי ממשק משתמש
- שמירה על מודעות להקשר בשיטות אינטראקציה שונות
לאן כדאי ללכת מכאן
אחרי שסיימתם את השלב הבסיסי של שילוב Gemini עם Flutter, הנה כמה דרכים להמשיך את התהליך:
שיפור אפליקציית Colorist
- פלטות צבעים: הוספת פונקציונליות ליצירת ערכות צבעים משלימות או תואמות
- קלט קולי: שילוב של זיהוי דיבור לתיאורי צבעים מילוליים
- ניהול היסטוריה: הוספנו אפשרויות לתת שם לערכות צבעים, לארגן אותן ולייצא אותן
- הנחיות בהתאמה אישית: יצירת ממשק שמאפשר למשתמשים להתאים אישית את ההנחיות למערכת
- ניתוח נתונים מתקדם: מעקב אחרי התיאורים שהכי עוזרים או גורמים לקשיים
עולם תוכן מורחב של Gemini
- קלט רב-אופני: הוספת קלט של תמונות כדי לחלץ צבעים מתמונות
- יצירת תוכן: אפשר להשתמש ב-Gemini כדי ליצור תוכן שקשור לצבעים, כמו תיאורים או סיפורים
- שיפורים בקריאה לפונקציות: יצירת שילובים מורכבים יותר של כלים עם מספר פונקציות
- הגדרות בטיחות: אפשר לבדוק הגדרות בטיחות שונות ואת ההשפעה שלהן על התשובות
החלת הדפוסים האלה על דומיינים אחרים
- ניתוח מסמכים: יצירת אפליקציות שיכולות להבין ולנתח מסמכים
- עזרה בכתיבה יוצרת: יצירת כלים לניסוח טקסט עם הצעות מבוססות-LLM
- אוטומציה של משימות: עיצוב אפליקציות שמתרגמות שפה טבעית למשימות אוטומטיות
- אפליקציות מבוססות-ידע: יצירת מערכות מומחים בתחומים ספציפיים
משאבים
ריכזנו כאן כמה מקורות מידע שיעזרו לכם להמשיך ללמוד:
תיעוד רשמי
קורס ומדריך ליצירת הנחיות
קהילה
Observable Flutter Agentic series
בפרק 59, קרייג לאבנז ואנדרו ברוגדן בוחנים את שיעור ה-Codelab הזה ומדגישים חלקים מעניינים בבניית האפליקציה.
בפרק 60, הצטרפו שוב אל קרייג ואנדרו כשהם מרחיבים את אפליקציית ה-codelab עם יכולות חדשות ונאבקים כדי לגרום למודלים מסוג LLM לעשות את מה שאומרים להם.
בפרק 61, קרייג מצטרף לכריס סלס כדי לנתח כותרות חדשותיות וליצור תמונות תואמות.
משוב
נשמח לשמוע על החוויה שלך עם ה-codelab הזה. נשמח לקבל משוב באמצעות:
תודה שסיימתם את ה-Codelab הזה. אנחנו מקווים שתמשיכו לחקור את האפשרויות המעניינות שקיימות בשילוב בין Flutter ל-AI.