1. สร้างแอป Flutter ที่ขับเคลื่อนด้วย Gemini
สิ่งที่คุณจะสร้าง
ในโค้ดแล็บนี้ คุณจะได้สร้าง Colorist ซึ่งเป็นแอปพลิเคชัน Flutter แบบอินเทอร์แอกทีฟที่นำความสามารถของ Gemini API มาไว้ในแอป Flutter โดยตรง คุณเคยต้องการให้ผู้ใช้ควบคุมแอปผ่านภาษาที่เป็นธรรมชาติแต่ไม่รู้ว่าจะเริ่มต้นจากตรงไหนใช่ไหม ซึ่ง Codelab นี้จะแสดงวิธีให้คุณ
Colorist ช่วยให้ผู้ใช้อธิบายสีด้วยภาษาที่เป็นธรรมชาติ (เช่น "สีส้มของพระอาทิตย์ตก" หรือ "สีน้ำเงินเข้มของมหาสมุทร") และแอปจะทำสิ่งต่อไปนี้
- ประมวลผลคำอธิบายเหล่านี้โดยใช้ Gemini API ของ Google
- แปลงคำอธิบายเป็นค่าสี RGB ที่แม่นยำ
- แสดงสีบนหน้าจอแบบเรียลไทม์
- ให้รายละเอียดทางเทคนิคเกี่ยวกับสีและบริบทที่น่าสนใจเกี่ยวกับสี
- เก็บประวัติสีที่สร้างขึ้นล่าสุด

แอปมีอินเทอร์เฟซแบบแยกหน้าจอพร้อมพื้นที่แสดงสีและระบบแชทแบบอินเทอร์แอกทีฟที่ด้านหนึ่ง และแผงบันทึกแบบละเอียดที่แสดงการโต้ตอบ LLM ดิบที่อีกด้านหนึ่ง บันทึกนี้ช่วยให้คุณเข้าใจวิธีการทำงานของการผสานรวม LLM ได้ดียิ่งขึ้น
เหตุผลที่เรื่องนี้สำคัญสำหรับนักพัฒนาแอป Flutter
LLM กำลังปฏิวัติวิธีที่ผู้ใช้โต้ตอบกับแอปพลิเคชัน แต่การผสานรวม LLM เข้ากับแอปบนอุปกรณ์เคลื่อนที่และเดสก์ท็อปอย่างมีประสิทธิภาพก็เป็นความท้าทายที่ไม่เหมือนใคร Codelab นี้จะสอนรูปแบบการใช้งานจริงที่มากกว่าแค่การเรียก API แบบดิบๆ
เส้นทางการเรียนรู้ของคุณ
Codelab นี้จะแนะนำขั้นตอนการสร้าง Colorist ทีละขั้นตอน
- การตั้งค่าโปรเจ็กต์ - คุณจะเริ่มต้นด้วยโครงสร้างแอป Flutter พื้นฐานและแพ็กเกจ
colorist_ui - การผสานรวม Gemini ขั้นพื้นฐาน - เชื่อมต่อแอปกับตรรกะ AI ของ Firebase และใช้การสื่อสาร LLM
- การเขียนพรอมต์ที่มีประสิทธิภาพ - สร้างพรอมต์ของระบบที่จะช่วยให้ LLM เข้าใจคำอธิบายสี
- การประกาศฟังก์ชัน - กำหนดเครื่องมือที่ LLM ใช้เพื่อตั้งค่าสีในแอปพลิเคชันได้
- การจัดการเครื่องมือ - ประมวลผลการเรียกใช้ฟังก์ชันจาก LLM และเชื่อมต่อกับการทำงานของแอป
- การสตรีมคำตอบ - ปรับปรุงประสบการณ์ของผู้ใช้ด้วยคำตอบจาก LLM แบบสตรีมมิงแบบเรียลไทม์
- การซิงค์บริบท LLM - สร้างประสบการณ์การใช้งานที่สอดคล้องกันโดยแจ้งให้ LLM ทราบถึงการดำเนินการของผู้ใช้
สิ่งที่คุณจะได้เรียนรู้
- กำหนดค่า Firebase AI Logic สำหรับแอปพลิเคชัน Flutter
- สร้างพรอมต์ของระบบที่มีประสิทธิภาพเพื่อกำหนดลักษณะการทำงานของ LLM
- ใช้การประกาศฟังก์ชันที่เชื่อมโยงภาษามนุษย์กับฟีเจอร์ของแอป
- ประมวลผลการตอบกลับแบบสตรีมเพื่อประสบการณ์ของผู้ใช้ที่ตอบสนองได้ดี
- ซิงค์สถานะระหว่างเหตุการณ์ UI กับ LLM
- จัดการสถานะการสนทนาของ LLM โดยใช้ Riverpod
- จัดการข้อผิดพลาดอย่างเหมาะสมในแอปพลิเคชันที่ทำงานด้วย LLM
ตัวอย่างโค้ด: ตัวอย่างสิ่งที่คุณจะนำไปใช้
ต่อไปนี้คือตัวอย่างการประกาศฟังก์ชันที่คุณจะสร้างขึ้นเพื่อให้ LLM ตั้งค่าสีในแอป
FunctionDeclaration get setColorFuncDecl => FunctionDeclaration(
'set_color',
'Set the color of the display square based on red, green, and blue values.',
parameters: {
'red': Schema.number(description: 'Red component value (0.0 - 1.0)'),
'green': Schema.number(description: 'Green component value (0.0 - 1.0)'),
'blue': Schema.number(description: 'Blue component value (0.0 - 1.0)'),
},
);
ภาพรวมของวิดีโอ Codelab นี้
ดู Craig Labenz และ Andrew Brogdon พูดคุยเกี่ยวกับ Codelab นี้ใน Observable Flutter ตอนที่ 59
ข้อกำหนดเบื้องต้น
คุณควรมีสิ่งต่อไปนี้เพื่อใช้ประโยชน์จาก Codelab นี้ให้ได้มากที่สุด
- ประสบการณ์การพัฒนา Flutter - ความคุ้นเคยกับพื้นฐานของ Flutter และไวยากรณ์ Dart
- ความรู้ด้านการเขียนโปรแกรมแบบอะซิงโครนัส - ความเข้าใจเกี่ยวกับ Futures, async/await และสตรีม
- บัญชี Firebase - คุณจะต้องมีบัญชี Google เพื่อตั้งค่า Firebase
มาเริ่มสร้างแอป Flutter ตัวแรกที่ทำงานด้วย LLM กันเลย
2. การตั้งค่าโปรเจ็กต์และบริการ Echo
ในขั้นตอนแรกนี้ คุณจะต้องตั้งค่าโครงสร้างโปรเจ็กต์และใช้บริการ Echo ซึ่งจะแทนที่ด้วยการผสานรวม Gemini API ในภายหลัง ซึ่งจะสร้างสถาปัตยกรรมของแอปพลิเคชันและช่วยให้มั่นใจว่า UI ทำงานได้อย่างถูกต้องก่อนที่จะเพิ่มความซับซ้อนของการเรียก LLM
สิ่งที่คุณจะได้เรียนรู้ในขั้นตอนนี้
- การตั้งค่าโปรเจ็กต์ Flutter ด้วยทรัพยากร Dependency ที่จำเป็น
- การทำงานกับแพ็กเกจ
colorist_uiสำหรับคอมโพเนนต์ UI - การติดตั้งใช้งานบริการข้อความก้องและการเชื่อมต่อกับ UI
สร้างโปรเจ็กต์ Flutter ใหม่
เริ่มต้นด้วยการสร้างโปรเจ็กต์ Flutter ใหม่ด้วยคำสั่งต่อไปนี้
flutter create -e colorist --platforms=android,ios,macos,web,windows
แฟล็ก -e ระบุว่าคุณต้องการโปรเจ็กต์ที่ว่างเปล่าโดยไม่มีแอป counter เริ่มต้น แอปนี้ออกแบบมาให้ทำงานบนเดสก์ท็อป อุปกรณ์เคลื่อนที่ และเว็บ แต่ขณะนี้ flutterfire ไม่รองรับ Linux
เพิ่มทรัพยากร Dependency
ไปที่ไดเรกทอรีโปรเจ็กต์และเพิ่มทรัพยากร Dependency ที่จำเป็น โดยทำดังนี้
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: แพ็กเกจที่กำหนดเองซึ่งมีคอมโพเนนต์ UI สำหรับแอป Coloristflutter_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
การกำหนดค่านี้ช่วยให้ Linter ของ 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 แพ็กเกจมีคอมโพเนนต์ UI ที่สร้างไว้ล่วงหน้าและเครื่องมือการจัดการสถานะ ดังนี้
- MainScreen: คอมโพเนนต์ UI หลักที่แสดง
- เลย์เอาต์แบบแยกหน้าจอบนเดสก์ท็อป (พื้นที่การโต้ตอบและแผงบันทึก)
- อินเทอร์เฟซแบบแท็บบนอุปกรณ์เคลื่อนที่
- การแสดงสี อินเทอร์เฟซแชท และภาพขนาดย่อของประวัติ
- การจัดการสถานะ: แอปใช้ตัวแจ้งสถานะหลายตัว
- ChatStateNotifier: จัดการข้อความแชท
- ColorStateNotifier: จัดการสีปัจจุบันและประวัติ
- LogStateNotifier: จัดการรายการบันทึกสำหรับการแก้ไขข้อบกพร่อง
- การจัดการข้อความ: แอปใช้รูปแบบข้อความที่มีสถานะต่างๆ ดังนี้
- ข้อความสำหรับผู้ใช้: ป้อนโดยผู้ใช้
- ข้อความ LLM: สร้างโดย LLM (หรือบริการ Echo ของคุณในตอนนี้)
- MessageState: ติดตามว่าข้อความ LLM เสร็จสมบูรณ์แล้วหรือยังคงสตรีมอยู่
สถาปัตยกรรมแอปพลิเคชัน
แอปมีสถาปัตยกรรมดังนี้
- เลเยอร์ UI: ให้บริการโดยแพ็กเกจ
colorist_ui - การจัดการสถานะ: ใช้ Riverpod สำหรับการจัดการสถานะแบบรีแอ็กทีฟ
- เลเยอร์บริการ: ปัจจุบันมีบริการ Echo อย่างง่ายของคุณ ซึ่งจะแทนที่ด้วยบริการ Gemini Chat
- การผสานรวม LLM: จะเพิ่มในขั้นตอนต่อๆ ไป
การแยกนี้ช่วยให้คุณมุ่งเน้นไปที่การติดตั้งใช้งานการผสานรวม LLM ได้ในขณะที่คอมโพเนนต์ UI ได้รับการดูแลอยู่แล้ว
เรียกใช้แอป
เรียกใช้แอปด้วยคำสั่งต่อไปนี้
flutter run -d DEVICE
แทนที่ DEVICE ด้วยอุปกรณ์เป้าหมาย เช่น macos, windows, chrome หรือรหัสอุปกรณ์

ตอนนี้คุณควรเห็นแอป Colorist พร้อมข้อมูลต่อไปนี้
- พื้นที่แสดงสีที่มีสีเริ่มต้น
- อินเทอร์เฟซแชทที่คุณพิมพ์ข้อความได้
- แผงบันทึกที่แสดงการโต้ตอบในแชท
ลองพิมพ์ข้อความ เช่น "ฉันอยากได้สีน้ำเงินเข้ม" แล้วกดส่ง บริการก้องจะพูดข้อความของคุณซ้ำ ในขั้นตอนต่อๆ ไป คุณจะแทนที่ค่านี้ด้วยการตีความสีจริงโดยใช้ตรรกะ AI ของ Firebase
ขั้นตอนต่อไปคืออะไร
ในขั้นตอนถัดไป คุณจะกำหนดค่า Firebase และใช้การผสานรวม Gemini API ขั้นพื้นฐานเพื่อแทนที่บริการ Echo ด้วยบริการแชทของ Gemini ซึ่งจะช่วยให้แอปตีความคำอธิบายสีและให้คำตอบที่ชาญฉลาดได้
การแก้ปัญหา
ปัญหาเกี่ยวกับแพ็กเกจ UI
หากพบปัญหาเกี่ยวกับแพ็กเกจ colorist_ui ให้ทำดังนี้
- ตรวจสอบว่าคุณใช้เวอร์ชันล่าสุด
- ตรวจสอบว่าคุณเพิ่มการอ้างอิงอย่างถูกต้อง
- ตรวจสอบเวอร์ชันแพ็กเกจที่ขัดแย้งกัน
ข้อผิดพลาดในการสร้าง
หากพบข้อผิดพลาดในการสร้าง ให้ทำดังนี้
- ตรวจสอบว่าคุณได้ติดตั้ง Flutter SDK เวอร์ชันเสถียรล่าสุดแล้ว
- เรียกใช้
flutter cleanตามด้วยflutter pub get - ตรวจสอบเอาต์พุตของคอนโซลเพื่อดูข้อความแสดงข้อผิดพลาดที่เฉพาะเจาะจง
แนวคิดหลักที่ได้เรียนรู้
- การตั้งค่าโปรเจ็กต์ Flutter ด้วยทรัพยากร Dependency ที่จำเป็น
- ทำความเข้าใจสถาปัตยกรรมของแอปพลิเคชันและความรับผิดชอบของคอมโพเนนต์
- การติดตั้งใช้งานบริการอย่างง่ายที่เลียนแบบลักษณะการทำงานของ LLM
- การเชื่อมต่อบริการกับคอมโพเนนต์ UI
- การใช้ Riverpod สำหรับการจัดการสถานะ
3. การผสานรวม Gemini Chat ขั้นพื้นฐาน
ในขั้นตอนนี้ คุณจะแทนที่บริการ Echo จากขั้นตอนก่อนหน้าด้วยการผสานรวม Gemini API โดยใช้ตรรกะ AI ของ Firebase คุณจะกำหนดค่า Firebase, ตั้งค่าผู้ให้บริการที่จำเป็น และใช้บริการแชทพื้นฐานที่สื่อสารกับ Gemini API
สิ่งที่คุณจะได้เรียนรู้ในขั้นตอนนี้
- การตั้งค่า Firebase ในแอปพลิเคชัน Flutter
- การกำหนดค่า Firebase AI Logic สำหรับสิทธิ์เข้าถึง Gemini
- การสร้างผู้ให้บริการ Riverpod สำหรับบริการ Firebase และ Gemini
- การติดตั้งใช้งานบริการแชทพื้นฐานด้วย Gemini API
- การจัดการการตอบกลับจาก API แบบไม่พร้อมกันและสถานะข้อผิดพลาด
ตั้งค่า Firebase
ก่อนอื่น คุณต้องตั้งค่า Firebase สำหรับโปรเจ็กต์ Flutter ซึ่งรวมถึงการสร้างโปรเจ็กต์ Firebase, การเพิ่มแอปของคุณลงในโปรเจ็กต์ และการกําหนดค่าการตั้งค่าตรรกะ AI ของ Firebase ที่จําเป็น
สร้างโปรเจ็กต์ Firebase
- ไปที่คอนโซล Firebase แล้วลงชื่อเข้าใช้ด้วยบัญชี Google
- คลิกสร้างโปรเจ็กต์ Firebase หรือเลือกโปรเจ็กต์ที่มีอยู่
- ทำตามวิซาร์ดการตั้งค่าเพื่อสร้างโปรเจ็กต์
ตั้งค่าตรรกะ AI ของ Firebase ในโปรเจ็กต์ Firebase
- ในคอนโซล Firebase ให้ไปที่โปรเจ็กต์
- เลือก AI ในแถบด้านข้างทางซ้าย
- เลือกตรรกะของ AI ในเมนูแบบเลื่อนลงของ AI
- ในการ์ดตรรกะ AI ของ Firebase ให้เลือกเริ่มต้นใช้งาน
- ทำตามข้อความแจ้งเพื่อเปิดใช้ Gemini Developer API สำหรับโปรเจ็กต์ของคุณ
ติดตั้ง FlutterFire CLI
FlutterFire CLI ช่วยให้การตั้งค่า Firebase ในแอป Flutter ง่ายขึ้น
dart pub global activate flutterfire_cli
เพิ่ม Firebase ไปยังแอป Flutter
- เพิ่มแพ็กเกจหลักของ Firebase และตรรกะ AI ของ Firebase ลงในโปรเจ็กต์
flutter pub add firebase_core firebase_ai
- เรียกใช้คำสั่งกำหนดค่า FlutterFire
flutterfire configure
คำสั่งนี้จะทำสิ่งต่อไปนี้
- ขอให้คุณเลือกโปรเจ็กต์ Firebase ที่เพิ่งสร้าง
- ลงทะเบียนแอป Flutter กับ Firebase
- สร้างไฟล์
firebase_options.dartที่มีการกำหนดค่าโปรเจ็กต์
คำสั่งจะตรวจหาแพลตฟอร์มที่คุณเลือก (iOS, Android, macOS, Windows, เว็บ) โดยอัตโนมัติและกำหนดค่าอย่างเหมาะสม
การกำหนดค่าเฉพาะแพลตฟอร์ม
Firebase กำหนดให้ใช้เวอร์ชันขั้นต่ำที่สูงกว่าค่าเริ่มต้นสำหรับ Flutter นอกจากนี้ ยังต้องมีสิทธิ์เข้าถึงเครือข่ายเพื่อสื่อสารกับเซิร์ฟเวอร์ตรรกะ AI ของ Firebase
กำหนดค่าสิทธิ์ 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();
}
ไฟล์นี้กำหนดพื้นฐานสำหรับผู้ให้บริการหลัก 3 ราย ระบบจะสร้างผู้ให้บริการเหล่านี้เมื่อคุณเรียกใช้ dart run build_runner โดยเครื่องมือสร้างโค้ด Riverpod โค้ดนี้ใช้วิธีการตามคำอธิบายประกอบของ Riverpod 3 พร้อมรูปแบบ Provider ที่อัปเดต
firebaseAppProvider: เริ่มต้น Firebase ด้วยการกำหนดค่าโปรเจ็กต์geminiModelProvider: สร้างอินสแตนซ์โมเดล Generative ของ GeminichatSessionProvider: สร้างและรักษาเซสชันแชทกับโมเดล 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
- อัปเดตอินเทอร์เฟซแชทด้วยคำตอบจากโมเดล
- บันทึกการสื่อสารทั้งหมดเพื่อให้เข้าใจโฟลว์ LLM จริงได้ง่าย
- จัดการข้อผิดพลาดด้วยความคิดเห็นของผู้ใช้ที่เหมาะสม
หมายเหตุ: หน้าต่างบันทึกจะดูเหมือนหน้าต่างแชทเกือบทุกประการในตอนนี้ บันทึกจะน่าสนใจยิ่งขึ้นเมื่อคุณใช้การเรียกฟังก์ชันและส่งคำตอบแบบสตรีม
สร้างโค้ด Riverpod
เรียกใช้คำสั่ง Build Runner เพื่อสร้างโค้ด Riverpod ที่จำเป็น
dart run build_runner build --delete-conflicting-outputs
ซึ่งจะสร้างไฟล์ .g.dart ที่ Riverpod ต้องใช้ในการทำงาน
อัปเดตไฟล์ main.dart
อัปเดตไฟล์ lib/main.dart เพื่อใช้บริการแชท Gemini ใหม่ โดยทำดังนี้
lib/main.dart
import 'package:colorist_ui/colorist_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'providers/gemini.dart';
import 'services/gemini_chat_service.dart';
void main() async {
runApp(ProviderScope(child: MainApp()));
}
class MainApp extends ConsumerWidget {
const MainApp({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final model = ref.watch(geminiModelProvider);
return MaterialApp(
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
),
home: model.when(
data: (data) => MainScreen(
sendMessage: (text) {
ref.read(geminiChatServiceProvider).sendMessage(text);
},
),
loading: () => LoadingScreen(message: 'Initializing Gemini Model'),
error: (err, st) => ErrorScreen(error: err),
),
);
}
}
การเปลี่ยนแปลงที่สำคัญในการอัปเดตนี้มีดังนี้
- การแทนที่บริการ Echo ด้วยบริการแชทที่ใช้ Gemini API
- การเพิ่มหน้าจอการโหลดและข้อผิดพลาดโดยใช้รูปแบบ
AsyncValueของ Riverpod กับเมธอดwhen - เชื่อมต่อ UI กับบริการแชทใหม่ผ่าน
sendMessageการเรียกกลับ
เรียกใช้แอป
เรียกใช้แอปด้วยคำสั่งต่อไปนี้
flutter run -d DEVICE
แทนที่ DEVICE ด้วยอุปกรณ์เป้าหมาย เช่น macos, windows, chrome หรือรหัสอุปกรณ์

ตอนนี้เมื่อคุณพิมพ์ข้อความ ระบบจะส่งข้อความนั้นไปยัง Gemini API และคุณจะได้รับการตอบกลับจาก LLM แทนที่จะเป็นเสียงก้อง แผงบันทึกจะแสดงการโต้ตอบกับ API
ทำความเข้าใจการสื่อสารของ LLM
มาทำความเข้าใจสิ่งที่เกิดขึ้นเมื่อคุณสื่อสารกับ Gemini API กัน
ขั้นตอนการสื่อสาร
- ข้อมูลจากผู้ใช้: ผู้ใช้ป้อนข้อความในอินเทอร์เฟซแชท
- การจัดรูปแบบคำขอ: แอปจะจัดรูปแบบข้อความเป็นออบเจ็กต์
Contentสำหรับ Gemini API - การสื่อสารผ่าน API: ระบบจะส่งข้อความไปยัง Gemini API ผ่าน Firebase AI Logic
- การประมวลผล LLM: โมเดล Gemini ประมวลผลข้อความและสร้างคำตอบ
- การจัดการการตอบกลับ: แอปได้รับการตอบกลับและอัปเดต UI
- การบันทึก: ระบบจะบันทึกการสื่อสารทั้งหมดเพื่อความโปร่งใส
เซสชันแชทและบริบทการสนทนา
เซสชันแชทกับ Gemini จะคงบริบทระหว่างข้อความไว้ ทำให้สามารถโต้ตอบแบบสนทนาได้ ซึ่งหมายความว่า LLM จะ "จดจำ" การแลกเปลี่ยนก่อนหน้าในเซสชันปัจจุบันได้ ทำให้การสนทนาเป็นไปอย่างสอดคล้องกันมากขึ้น
คำอธิบายประกอบ keepAlive: true ในผู้ให้บริการเซสชันแชทจะช่วยให้บริบทนี้คงอยู่ตลอดวงจรของแอป บริบทที่คงอยู่นี้มีความสำคัญอย่างยิ่งต่อการรักษาการสนทนากับ LLM ให้เป็นไปอย่างเป็นธรรมชาติ
ขั้นตอนต่อไปคืออะไร
ในตอนนี้ คุณสามารถถาม Gemini API ได้ทุกอย่าง เนื่องจากไม่มีข้อจำกัดเกี่ยวกับสิ่งที่ API จะตอบ เช่น คุณอาจขอสรุปสงครามดอกกุหลาบ ซึ่งไม่เกี่ยวข้องกับวัตถุประสงค์ของแอปสี
ในขั้นตอนถัดไป คุณจะสร้างพรอมต์ของระบบเพื่อเป็นแนวทางให้ Gemini ตีความคำอธิบายสีได้อย่างมีประสิทธิภาพมากขึ้น ซึ่งจะแสดงวิธีปรับแต่งลักษณะการทำงานของ LLM ให้ตรงกับความต้องการเฉพาะของแอปพลิเคชัน และมุ่งเน้นความสามารถของ 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 แบบไม่พร้อมกัน (กำลังโหลด ข้อผิดพลาด ข้อมูล)
- ทำความเข้าใจขั้นตอนการสื่อสารและเซสชันแชทของ LLM
4. การใช้พรอมต์อย่างมีประสิทธิภาพสำหรับคำอธิบายสี
ในขั้นตอนนี้ คุณจะได้สร้างและใช้พรอมต์ของระบบที่จะแนะนำ Gemini ในการตีความคำอธิบายสี พรอมต์ของระบบเป็นวิธีที่มีประสิทธิภาพในการปรับแต่งลักษณะการทำงานของ LLM สำหรับงานที่เฉพาะเจาะจงโดยไม่ต้องเปลี่ยนโค้ด
สิ่งที่คุณจะได้เรียนรู้ในขั้นตอนนี้
- ทำความเข้าใจพรอมต์ของระบบและความสำคัญของพรอมต์ในแอปพลิเคชัน LLM
- การสร้างพรอมต์ที่มีประสิทธิภาพสำหรับงานเฉพาะโดเมน
- การโหลดและการใช้พรอมต์ของระบบในแอป Flutter
- การแนะนำ LLM ให้แสดงคำตอบในรูปแบบที่สอดคล้องกัน
- การทดสอบว่าพรอมต์ของระบบส่งผลต่อลักษณะการทำงานของ 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 อย่างแน่นอนเพื่อให้สอดคล้องกัน
- ตัวอย่าง Exchange: แสดงตัวอย่างรูปแบบการโต้ตอบที่คาดไว้
- การจัดการกรณีสุดโต่ง: อธิบายวิธีจัดการคำอธิบายที่ไม่ชัดเจน
- ข้อจำกัดและหลักเกณฑ์: กำหนดขอบเขต เช่น การเก็บค่า 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) เมื่อสร้างโมเดล Generative ซึ่งจะบอกให้ Gemini ใช้คำสั่งของคุณเป็นพรอมต์ของระบบสำหรับการโต้ตอบทั้งหมดในเซสชันแชทนี้
สร้างโค้ด Riverpod
เรียกใช้คำสั่ง Build Runner เพื่อสร้างโค้ด Riverpod ที่จำเป็น
dart run build_runner build --delete-conflicting-outputs
เรียกใช้และทดสอบแอปพลิเคชัน
ตอนนี้ให้เรียกใช้แอปพลิเคชันโดยใช้คำสั่งต่อไปนี้
flutter run -d DEVICE

ลองทดสอบด้วยคำอธิบายสีต่างๆ ดังนี้
- "ฉันอยากได้สีฟ้า"
- "ขอสีเขียวป่าไม้"
- "สร้างสีส้มสดใสของพระอาทิตย์ตก"
- "ฉันอยากได้สีลาเวนเดอร์สด"
- "ขอดูสีที่คล้ายกับสีน้ำเงินเข้มของมหาสมุทรหน่อย"
คุณจะเห็นว่าตอนนี้ Gemini ตอบกลับด้วยคำอธิบายแบบสนทนาเกี่ยวกับสีพร้อมค่า RGB ที่จัดรูปแบบอย่างสม่ำเสมอ พรอมต์ของระบบได้แนะนำ LLM อย่างมีประสิทธิภาพเพื่อให้คำตอบประเภทที่คุณต้องการ
นอกจากนี้ ลองขอเนื้อหาที่อยู่นอกบริบทของสี เช่น สาเหตุหลักของสงครามดอกกุหลาบ คุณควรสังเกตเห็นความแตกต่างจากขั้นตอนก่อนหน้า
ความสำคัญของวิศวกรรมพรอมต์สำหรับงานเฉพาะทาง
พรอมต์ของระบบเป็นทั้งศาสตร์และศิลป์ ซึ่งเป็นส่วนสำคัญของการผสานรวม LLM ที่อาจส่งผลอย่างมากต่อความมีประโยชน์ของโมเดลสำหรับการใช้งานเฉพาะของคุณ สิ่งที่คุณทำที่นี่คือรูปแบบหนึ่งของการออกแบบพรอมต์ ซึ่งเป็นการปรับแต่งคำสั่งเพื่อให้โมเดลทำงานในลักษณะที่เหมาะกับความต้องการของแอปพลิเคชันของคุณ
วิศวกรรมพรอมต์ที่มีประสิทธิภาพประกอบด้วย
- การกำหนดบทบาทที่ชัดเจน: การกำหนดวัตถุประสงค์ของ LLM
- คำสั่งที่ชัดเจน: ระบุรายละเอียดวิธีที่ LLM ควรตอบ
- ตัวอย่างที่เป็นรูปธรรม: แสดงให้เห็นแทนที่จะบอกเพียงว่าคำตอบที่ดีควรมีลักษณะอย่างไร
- การจัดการกรณีที่สุ่มเสี่ยงจะละเมิดนโยบาย: การสั่งให้ LLM จัดการกับสถานการณ์ที่ไม่ชัดเจน
- ข้อกำหนดการจัดรูปแบบ: การตรวจสอบว่าคำตอบมีโครงสร้างในลักษณะที่สอดคล้องกันและนำไปใช้ได้
พรอมต์ของระบบที่คุณสร้างขึ้นจะเปลี่ยนความสามารถทั่วไปของ Gemini ให้เป็นผู้ช่วยตีความสีเฉพาะทาง ซึ่งจะให้คำตอบที่จัดรูปแบบมาโดยเฉพาะสำหรับความต้องการของแอปพลิเคชันของคุณ นี่เป็นรูปแบบที่มีประสิทธิภาพซึ่งคุณนำไปใช้กับโดเมนและงานต่างๆ ได้มากมาย
ขั้นตอนต่อไปคืออะไร
ในขั้นตอนถัดไป คุณจะสร้างต่อจากพื้นฐานนี้โดยการเพิ่มการประกาศฟังก์ชัน ซึ่งจะช่วยให้ LLM ไม่เพียงแค่แนะนำค่า RGB แต่ยังเรียกใช้ฟังก์ชันในแอปเพื่อตั้งค่าสีโดยตรงได้อีกด้วย ซึ่งแสดงให้เห็นว่า LLM สามารถเชื่อมช่องว่างระหว่างภาษาธรรมชาติกับฟีเจอร์แอปพลิเคชันที่เป็นรูปธรรมได้อย่างไร
การแก้ปัญหา
ปัญหาในการโหลดชิ้นงาน
หากพบข้อผิดพลาดในการโหลดพรอมต์ของระบบ ให้ทำดังนี้
- ตรวจสอบว่า
pubspec.yamlแสดงรายการไดเรกทอรีชิ้นงานอย่างถูกต้อง - ตรวจสอบว่าเส้นทางใน
rootBundle.loadString()ตรงกับตำแหน่งไฟล์ - เรียกใช้
flutter cleanตามด้วยflutter pub getเพื่อรีเฟรชชุดเนื้อหา
คำตอบที่ไม่สอดคล้องกัน
หาก LLM ไม่ทำตามวิธีการจัดรูปแบบของคุณอย่างสม่ำเสมอ ให้ทำดังนี้
- ลองระบุข้อกำหนดของรูปแบบให้ชัดเจนยิ่งขึ้นในพรอมต์ของระบบ
- เพิ่มตัวอย่างเพื่อแสดงรูปแบบที่คาดไว้
- ตรวจสอบว่ารูปแบบที่คุณขอเหมาะสมกับโมเดล
การจำกัดอัตรา API
หากพบข้อผิดพลาดที่เกี่ยวข้องกับการจำกัดอัตรา ให้ทำดังนี้
- โปรดทราบว่าบริการ Firebase AI Logic มีโควต้าการใช้งาน
- พิจารณานำตรรกะการลองใหม่ไปใช้กับ Exponential Backoff
- ตรวจสอบปัญหาเกี่ยวกับโควต้าในคอนโซล Firebase
แนวคิดหลักที่ได้เรียนรู้
- ทำความเข้าใจบทบาทและความสำคัญของพรอมต์ของระบบในแอปพลิเคชัน LLM
- การสร้างพรอมต์ที่มีประสิทธิภาพด้วยคำสั่ง ตัวอย่าง และข้อจำกัดที่ชัดเจน
- การโหลดและการใช้พรอมต์ของระบบในแอปพลิเคชัน Flutter
- การกำหนดแนวทางลักษณะการทำงานของ LLM สำหรับงานที่เจาะจงโดเมน
- การใช้พรอมต์เอ็นจิเนียริงเพื่อกำหนดคำตอบของ LLM
ขั้นตอนนี้แสดงให้เห็นว่าคุณปรับแต่งลักษณะการทำงานของ LLM ได้อย่างมากโดยไม่ต้องเปลี่ยนโค้ด เพียงแค่ระบุวิธีการที่ชัดเจนในพรอมต์ของระบบ
5. การประกาศฟังก์ชันสำหรับเครื่องมือ LLM
ในขั้นตอนนี้ คุณจะเริ่มการทำงานเพื่อเปิดให้ Gemini ดำเนินการในแอปได้โดยการติดตั้งใช้งานการประกาศฟังก์ชัน ฟีเจอร์อันทรงพลังนี้ช่วยให้ LLM ไม่เพียงแค่แนะนำค่า RGB แต่ยังตั้งค่าเหล่านั้นใน UI ของแอปผ่านการเรียกใช้เครื่องมือเฉพาะได้อีกด้วย อย่างไรก็ตาม คุณจะต้องทำตามขั้นตอนถัดไปเพื่อดูคำขอ LLM ที่ดำเนินการในแอป Flutter
สิ่งที่คุณจะได้เรียนรู้ในขั้นตอนนี้
- ทำความเข้าใจการเรียกใช้ฟังก์ชัน LLM และประโยชน์ของฟังก์ชันนี้สำหรับแอปพลิเคชัน Flutter
- การกำหนดประกาศฟังก์ชันที่อิงตามสคีมาสำหรับ Gemini
- การผสานรวมการประกาศฟังก์ชันกับโมเดล Gemini
- การอัปเดตพรอมต์ของระบบเพื่อใช้ความสามารถของเครื่องมือ
ทำความเข้าใจการเรียกใช้ฟังก์ชัน
ก่อนที่จะใช้การประกาศฟังก์ชัน มาทำความเข้าใจกันก่อนว่าการประกาศฟังก์ชันคืออะไรและเหตุใดจึงมีประโยชน์
การเรียกใช้ฟังก์ชันคืออะไร
การเรียกใช้ฟังก์ชัน (บางครั้งเรียกว่า "การใช้เครื่องมือ") เป็นความสามารถที่ช่วยให้ LLM ทำสิ่งต่อไปนี้ได้
- รับรู้เมื่อคำขอของผู้ใช้จะได้รับประโยชน์จากการเรียกใช้ฟังก์ชันที่เฉพาะเจาะจง
- สร้างออบเจ็กต์ JSON ที่มีโครงสร้างพร้อมพารามิเตอร์ที่จำเป็นสำหรับฟังก์ชันนั้น
- อนุญาตให้แอปพลิเคชันของคุณเรียกใช้ฟังก์ชันด้วยพารามิเตอร์เหล่านั้น
- รับผลลัพธ์ของฟังก์ชันและรวมไว้ในการตอบกลับ
การเรียกใช้ฟังก์ชันช่วยให้ LLM สามารถทริกเกอร์การดำเนินการที่เฉพาะเจาะจงในแอปพลิเคชันได้ แทนที่จะให้ LLM อธิบายสิ่งที่ต้องทำเท่านั้น
เหตุใดการเรียกใช้ฟังก์ชันจึงมีความสำคัญต่อแอป Flutter
การเรียกใช้ฟังก์ชันเป็นสะพานที่แข็งแกร่งระหว่างภาษาธรรมชาติกับฟีเจอร์ของแอปพลิเคชัน
- การดำเนินการโดยตรง: ผู้ใช้สามารถอธิบายสิ่งที่ต้องการด้วยภาษาที่เป็นธรรมชาติได้ และแอปจะตอบกลับด้วยการดำเนินการที่ชัดเจน
- เอาต์พุตที่มีโครงสร้าง: LLM สร้างข้อมูลที่มีโครงสร้างที่สะอาดแทนที่จะเป็นข้อความที่ต้องแยกวิเคราะห์
- การดำเนินการที่ซับซ้อน: ช่วยให้ LLM เข้าถึงข้อมูลภายนอก ทำการคำนวณ หรือแก้ไขสถานะแอปพลิเคชันได้
- ประสบการณ์ของผู้ใช้ที่ดีขึ้น: สร้างการผสานรวมที่ราบรื่นระหว่างการสนทนาและฟังก์ชันการทำงาน
ในแอป Colorist การเรียกใช้ฟังก์ชันช่วยให้ผู้ใช้พูดว่า "ฉันต้องการสีเขียวป่า" และให้ UI อัปเดตเป็นสีนั้นทันทีโดยไม่ต้องแยกวิเคราะห์ค่า 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.0green: องค์ประกอบสีเขียวของ RGB ซึ่งระบุเป็นตัวเลขระหว่าง 0.0 ถึง 1.0blue: องค์ประกอบสีน้ำเงินของ RGB ซึ่งระบุเป็นตัวเลขระหว่าง 0.0 ถึง 1.0
- ประเภทสคีมา: คุณใช้
Schema.number()เพื่อระบุว่าค่าเหล่านี้เป็นค่าตัวเลข - คอลเล็กชันเครื่องมือ: คุณสร้างรายการเครื่องมือที่มีการประกาศฟังก์ชัน
แนวทางที่มีโครงสร้างนี้ช่วยให้ LLM ของ 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 เมื่อสร้างโมเดล Generative ซึ่งจะทำให้ 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
การเปลี่ยนแปลงที่สำคัญในพรอมต์ของระบบมีดังนี้
- การแนะนำเครื่องมือ: ตอนนี้คุณสามารถบอก LLM เกี่ยวกับ
set_colorเครื่องมือแทนที่จะขอค่า RGB ที่จัดรูปแบบแล้ว - กระบวนการที่แก้ไขแล้ว: คุณเปลี่ยนขั้นตอนที่ 3 จาก "จัดรูปแบบค่าในการตอบกลับ" เป็น "ใช้เครื่องมือเพื่อตั้งค่า"
- ตัวอย่างที่อัปเดตแล้ว: คุณแสดงให้เห็นว่าคำตอบควรมีการเรียกใช้เครื่องมือแทนที่จะเป็นข้อความที่จัดรูปแบบ
- ข้อกำหนดการจัดรูปแบบที่นำออก: เนื่องจากคุณใช้การเรียกใช้ฟังก์ชันที่มีโครงสร้าง จึงไม่จำเป็นต้องใช้รูปแบบข้อความที่เฉพาะเจาะจงอีกต่อไป
พรอมต์ที่อัปเดตนี้จะสั่งให้ LLM ใช้การเรียกใช้ฟังก์ชันแทนที่จะให้ค่า RGB ในรูปแบบข้อความเพียงอย่างเดียว
สร้างโค้ด Riverpod
เรียกใช้คำสั่ง Build Runner เพื่อสร้างโค้ด Riverpod ที่จำเป็น
dart run build_runner build --delete-conflicting-outputs
เรียกใช้แอปพลิเคชัน
ในตอนนี้ Gemini จะสร้างเนื้อหาที่พยายามใช้การเรียกใช้ฟังก์ชัน แต่คุณยังไม่ได้ใช้ตัวแฮนเดิลสำหรับการเรียกใช้ฟังก์ชัน เมื่อเรียกใช้แอปและอธิบายสี คุณจะเห็น Gemini ตอบกลับราวกับว่าได้เรียกใช้เครื่องมือ แต่จะไม่เห็นการเปลี่ยนแปลงสีใดๆ ใน UI จนกว่าจะถึงขั้นตอนถัดไป
เรียกใช้แอปโดยทำดังนี้
flutter run -d DEVICE

ลองอธิบายสี เช่น "น้ำเงินเข้ม" หรือ "เขียวป่า" แล้วสังเกตคำตอบ LLM พยายามเรียกฟังก์ชันที่กำหนดไว้ข้างต้น แต่โค้ดของคุณยังไม่ตรวจพบการเรียกฟังก์ชัน
กระบวนการเรียกใช้ฟังก์ชัน
มาดูสิ่งที่เกิดขึ้นเมื่อ Gemini ใช้การเรียกใช้ฟังก์ชันกัน
- การเลือกฟังก์ชัน: LLM จะตัดสินว่าการเรียกใช้ฟังก์ชันจะเป็นประโยชน์หรือไม่โดยอิงตามคำขอของผู้ใช้
- การสร้างพารามิเตอร์: LLM จะสร้างค่าพารามิเตอร์ที่ตรงกับสคีมาของฟังก์ชัน
- รูปแบบการเรียกใช้ฟังก์ชัน: LLM จะส่งออบเจ็กต์การเรียกใช้ฟังก์ชันที่มีโครงสร้างในการตอบกลับ
- การจัดการแอปพลิเคชัน: แอปของคุณจะรับการเรียกนี้และเรียกใช้ฟังก์ชันที่เกี่ยวข้อง (ติดตั้งใช้งานในขั้นตอนถัดไป)
- การผสานรวมการตอบกลับ: ในการสนทนาแบบหลายรอบ LLM คาดหวังว่าจะได้รับผลลัพธ์ของฟังก์ชัน
ในสถานะปัจจุบันของแอป ขั้นตอนที่ 1-3 เกิดขึ้นแล้ว แต่คุณยังไม่ได้ใช้ขั้นตอนที่ 4 หรือ 5 (การจัดการการเรียกฟังก์ชัน) ซึ่งคุณจะทำในขั้นตอนถัดไป
รายละเอียดทางเทคนิค: วิธีที่ Gemini ตัดสินใจว่าจะใช้ฟังก์ชันเมื่อใด
Gemini จะตัดสินใจอย่างชาญฉลาดว่าจะใช้ฟังก์ชันเมื่อใดโดยอิงตามข้อมูลต่อไปนี้
- เจตนาของผู้ใช้: ฟังก์ชันจะตอบสนองคำขอของผู้ใช้ได้ดีที่สุดหรือไม่
- ความเกี่ยวข้องของฟังก์ชัน: ฟังก์ชันที่ใช้ได้ตรงกับงานมากน้อยแค่ไหน
- ความพร้อมใช้งานของพารามิเตอร์: ระบุว่าสามารถกำหนดค่าพารามิเตอร์ได้อย่างมั่นใจหรือไม่
- วิธีการของระบบ: คำแนะนำจากพรอมต์ของระบบเกี่ยวกับการใช้งานฟังก์ชัน
การประกาศฟังก์ชันและคำสั่งของระบบที่ชัดเจนจะช่วยให้ Gemini รู้ว่าคำขอคำอธิบายสีเป็นโอกาสในการเรียกใช้ฟังก์ชัน set_color
ขั้นตอนต่อไปคืออะไร
ในขั้นตอนถัดไป คุณจะใช้ตัวแฮนเดิลสำหรับการเรียกใช้ฟังก์ชันที่มาจาก Gemini ซึ่งจะทำให้วงจรนี้สมบูรณ์ ทำให้คำอธิบายของผู้ใช้ทริกเกอร์การเปลี่ยนสีจริงใน UI ผ่านการเรียกใช้ฟังก์ชันของ LLM
การแก้ปัญหา
ปัญหาเกี่ยวกับการประกาศฟังก์ชัน
หากพบข้อผิดพลาดเกี่ยวกับการประกาศฟังก์ชัน ให้ทำดังนี้
- ตรวจสอบว่าชื่อและประเภทพารามิเตอร์ตรงกับที่คาดไว้
- ตรวจสอบว่าชื่อฟังก์ชันชัดเจนและสื่อความหมาย
- ตรวจสอบว่าคำอธิบายฟังก์ชันอธิบายวัตถุประสงค์ได้อย่างถูกต้อง
ปัญหาเกี่ยวกับพรอมต์ของระบบ
หาก LLM ไม่พยายามใช้ฟังก์ชัน ให้ทำดังนี้
- ตรวจสอบว่าพรอมต์ของระบบสั่งให้ LLM ใช้เครื่องมือ
set_colorอย่างชัดเจน - ตรวจสอบว่าตัวอย่างในพรอมต์ของระบบแสดงการใช้งานฟังก์ชัน
- ลองระบุวิธีการใช้เครื่องมือให้ชัดเจนยิ่งขึ้น
ปัญหาทั่วไป
หากพบปัญหาอื่นๆ ให้ทำดังนี้
- ตรวจสอบข้อผิดพลาดที่เกี่ยวข้องกับการประกาศฟังก์ชันในคอนโซล
- ตรวจสอบว่ามีการส่งเครื่องมือไปยังโมเดลอย่างถูกต้อง
- ตรวจสอบว่าโค้ดที่ Riverpod สร้างขึ้นทั้งหมดเป็นเวอร์ชันล่าสุด
แนวคิดหลักที่ได้เรียนรู้
- การกำหนดการประกาศฟังก์ชันเพื่อขยายความสามารถของ LLM ในแอป Flutter
- การสร้างสคีมาพารามิเตอร์สำหรับการเก็บรวบรวม Structured Data
- การผสานรวมการประกาศฟังก์ชันกับโมเดล Gemini
- การอัปเดตพรอมต์ของระบบเพื่อกระตุ้นการใช้ฟังก์ชัน
- ทำความเข้าใจวิธีที่ LLM เลือกและเรียกใช้ฟังก์ชัน
ขั้นตอนนี้แสดงให้เห็นว่า LLM สามารถเชื่อมช่องว่างระหว่างอินพุตภาษาธรรมชาติและการเรียกใช้ฟังก์ชันที่มีโครงสร้างได้อย่างไร ซึ่งเป็นการวางรากฐานสำหรับการผสานรวมระหว่างฟีเจอร์การสนทนาและฟีเจอร์แอปพลิเคชันได้อย่างราบรื่น
6. การจัดการเครื่องมือ
ในขั้นตอนนี้ คุณจะติดตั้งใช้งานตัวแฮนเดิลสำหรับการเรียกใช้ฟังก์ชันที่มาจาก Gemini ซึ่งจะช่วยให้การสื่อสารระหว่างอินพุตภาษาธรรมชาติกับฟีเจอร์แอปพลิเคชันที่เป็นรูปธรรมสมบูรณ์ยิ่งขึ้น ทำให้ LLM สามารถจัดการ UI ได้โดยตรงตามคำอธิบายของผู้ใช้
สิ่งที่คุณจะได้เรียนรู้ในขั้นตอนนี้
- ทําความเข้าใจไปป์ไลน์การเรียกใช้ฟังก์ชันที่สมบูรณ์ในแอปพลิเคชัน LLM
- การประมวลผลการเรียกฟังก์ชันจาก Gemini ในแอปพลิเคชัน Flutter
- การใช้ตัวแฮนเดิลฟังก์ชันที่แก้ไขสถานะของแอปพลิเคชัน
- การจัดการการตอบกลับของฟังก์ชันและการแสดงผลลัพธ์ไปยัง LLM
- สร้างโฟลว์การสื่อสารที่สมบูรณ์ระหว่าง LLM กับ UI
- การบันทึกการเรียกฟังก์ชันและการตอบกลับเพื่อความโปร่งใส
ทําความเข้าใจไปป์ไลน์การเรียกใช้ฟังก์ชัน
ก่อนที่จะไปดูการใช้งาน เรามาทำความเข้าใจไปป์ไลน์การเรียกใช้ฟังก์ชันทั้งหมดกันก่อน
ขั้นตอนตั้งแต่ต้นจนจบ
- อินพุตของผู้ใช้: ผู้ใช้อธิบายสีด้วยภาษาที่เป็นธรรมชาติ (เช่น "เขียวเข้ม")
- การประมวลผล LLM: Gemini วิเคราะห์คำอธิบายและตัดสินใจเรียกใช้ฟังก์ชัน
set_color - การสร้างการเรียกใช้ฟังก์ชัน: Gemini สร้าง JSON ที่มีโครงสร้างพร้อมพารามิเตอร์ (ค่าสีแดง เขียว น้ำเงิน)
- การรับการเรียกใช้ฟังก์ชัน: แอปของคุณจะได้รับข้อมูลที่มีโครงสร้างนี้จาก Gemini
- การเรียกใช้ฟังก์ชัน: แอปของคุณเรียกใช้ฟังก์ชันด้วยพารามิเตอร์ที่ระบุ
- การอัปเดตสถานะ: ฟังก์ชันจะอัปเดตสถานะของแอป (เปลี่ยนสีที่แสดง)
- การสร้างคำตอบ: ฟังก์ชันจะส่งผลลัพธ์กลับไปยัง LLM
- การรวมคำตอบ: LLM จะรวมผลลัพธ์เหล่านี้ไว้ในคำตอบสุดท้าย
- การอัปเดต UI: UI จะตอบสนองต่อการเปลี่ยนสถานะโดยแสดงสีใหม่
วงจรการสื่อสารที่สมบูรณ์เป็นสิ่งจำเป็นสำหรับการผสานรวม LLM อย่างเหมาะสม เมื่อ LLM เรียกใช้ฟังก์ชัน ระบบไม่ได้เพียงแค่ส่งคำขอแล้วดำเนินการต่อ แต่จะรอให้แอปพลิเคชันของคุณเรียกใช้ฟังก์ชันและแสดงผลลัพธ์ จากนั้น LLM จะใช้ผลลัพธ์เหล่านี้เพื่อสร้างคำตอบสุดท้าย ซึ่งจะทำให้เกิดการสนทนาที่เป็นธรรมชาติซึ่งรับทราบการดำเนินการที่ทำ
ใช้ตัวแฮนเดิลฟังก์ชัน
มาอัปเดตไฟล์ lib/services/gemini_tools.dart เพื่อเพิ่มตัวแฮนเดิลสำหรับการเรียกใช้ฟังก์ชันกัน
lib/services/gemini_tools.dart
import 'package:colorist_ui/colorist_ui.dart';
import 'package:firebase_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);
ทำความเข้าใจตัวแฮนเดิลฟังก์ชัน
มาดูรายละเอียดว่าตัวแฮนเดิลฟังก์ชันเหล่านี้ทำอะไรได้บ้าง
handleFunctionCall: ผู้จัดส่งส่วนกลางที่มีคุณสมบัติดังนี้- บันทึกการเรียกฟังก์ชันเพื่อความโปร่งใสในแผงบันทึก
- กำหนดเส้นทางไปยังแฮนเดิลที่เหมาะสมตามชื่อฟังก์ชัน
- แสดงผลการตอบกลับที่มีโครงสร้างซึ่งจะส่งกลับไปยัง LLM
handleSetColor: ตัวแฮนเดิลเฉพาะสำหรับฟังก์ชันset_colorของคุณซึ่งมีลักษณะดังนี้- แยกค่า RGB จากแผนที่อาร์กิวเมนต์
- แปลงเป็นประเภทที่คาดไว้ (ดับเบิล)
- อัปเดตสถานะสีของแอปพลิเคชันโดยใช้
colorStateNotifier - สร้างการตอบกลับที่มีโครงสร้างพร้อมสถานะสำเร็จและข้อมูลสีปัจจุบัน
- บันทึกผลลัพธ์ของฟังก์ชันเพื่อการแก้ไขข้อบกพร่อง
handleUnknownFunction: ตัวแฮนเดิลสำรองสำหรับฟังก์ชันที่ไม่รู้จักซึ่งมีลักษณะดังนี้- บันทึกคำเตือนเกี่ยวกับฟังก์ชันที่ไม่รองรับ
- ส่งการตอบกลับข้อผิดพลาดไปยัง LLM
ฟังก์ชัน handleSetColor มีความสำคัญอย่างยิ่งเนื่องจากช่วยเชื่อมช่องว่างระหว่างความเข้าใจภาษาธรรมชาติของ LLM กับการเปลี่ยนแปลง UI ที่เป็นรูปธรรม
อัปเดตบริการแชท Gemini เพื่อประมวลผลการเรียกใช้ฟังก์ชันและการตอบกลับ
ตอนนี้มาอัปเดตไฟล์ 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 ต่อผลลัพธ์ของฟังก์ชัน
- อัปเดต UI ด้วยข้อความตอบกลับสุดท้าย
ซึ่งจะสร้างโฟลว์การเดินทางไปกลับ ดังนี้
- ผู้ใช้ → LLM: ขอสี
- LLM → แอป: การเรียกใช้ฟังก์ชันที่มีพารามิเตอร์
- แอป → ผู้ใช้: แสดงสีใหม่
- แอป → LLM: ผลลัพธ์ของฟังก์ชัน
- LLM → ผู้ใช้: คำตอบสุดท้ายที่รวมผลลัพธ์ของฟังก์ชัน
สร้างโค้ด Riverpod
เรียกใช้คำสั่ง Build Runner เพื่อสร้างโค้ด Riverpod ที่จำเป็น
dart run build_runner build --delete-conflicting-outputs
เรียกใช้และทดสอบโฟลว์ทั้งหมด
ตอนนี้ให้เรียกใช้แอปพลิเคชันโดยใช้คำสั่งต่อไปนี้
flutter run -d DEVICE

ลองป้อนคำอธิบายสีต่างๆ
- "ฉันอยากได้สีแดงเข้ม"
- "แสดงสีฟ้าครามที่สงบ"
- "บอกสีของใบสะระแหน่สดให้หน่อย"
- "ฉันอยากเห็นสีส้มของพระอาทิตย์ตกที่อบอุ่น"
- "ทำให้เป็นสีม่วงเข้มหรูหรา"
ตอนนี้คุณควรเห็นข้อมูลต่อไปนี้
- ข้อความของคุณจะปรากฏในอินเทอร์เฟซแชท
- คำตอบของ Gemini ที่ปรากฏในแชท
- การเรียกใช้ฟังก์ชันที่บันทึกไว้ในแผงบันทึก
- ระบบจะบันทึกผลลัพธ์ของฟังก์ชันทันทีหลังจากนั้น
- สี่เหลี่ยมผืนผ้าสีที่อัปเดตเพื่อแสดงสีที่อธิบาย
- ค่า RGB อัปเดตเพื่อแสดงคอมโพเนนต์ของสีใหม่
- คำตอบสุดท้ายของ Gemini ปรากฏขึ้น โดยมักจะแสดงความคิดเห็นเกี่ยวกับสีที่ตั้งไว้
แผงบันทึกจะให้ข้อมูลเชิงลึกเกี่ยวกับสิ่งที่เกิดขึ้นเบื้องหลัง คุณจะเห็นข้อมูลดังนี้
- การเรียกใช้ฟังก์ชันที่ Gemini ทำ
- พารามิเตอร์ที่เลือกสำหรับค่า RGB แต่ละค่า
- ผลลัพธ์ที่ฟังก์ชันแสดง
- คำตอบติดตามผลจาก Gemini
ตัวแจ้งสถานะสี
colorStateNotifierที่คุณใช้เพื่ออัปเดตสีเป็นส่วนหนึ่งของแพ็กเกจ colorist_ui โดยจะจัดการสิ่งต่อไปนี้
- สีปัจจุบันที่แสดงใน UI
- ประวัติสี (10 สีล่าสุด)
- การแจ้งเตือนการเปลี่ยนแปลงสถานะไปยังคอมโพเนนต์ UI
เมื่อเรียกใช้ updateColor ด้วยค่า RGB ใหม่ ระบบจะดำเนินการต่อไปนี้
- สร้างออบเจ็กต์
ColorDataใหม่ด้วยค่าที่ระบุ - อัปเดตสีปัจจุบันในสถานะของแอป
- เพิ่มสีลงในประวัติ
- ทริกเกอร์การอัปเดต UI ผ่านการจัดการสถานะของ Riverpod
คอมโพเนนต์ UI ในแพ็กเกจ colorist_ui จะสังเกตสถานะนี้และอัปเดตโดยอัตโนมัติเมื่อมีการเปลี่ยนแปลง ซึ่งจะสร้างประสบการณ์การใช้งานแบบโต้ตอบ
ทำความเข้าใจการจัดการข้อผิดพลาด
การติดตั้งใช้งานของคุณมีการจัดการข้อผิดพลาดที่มีประสิทธิภาพ ดังนี้
- บล็อก Try-catch: ห่อหุ้มการโต้ตอบกับ LLM ทั้งหมดเพื่อดักจับข้อยกเว้น
- การบันทึกข้อผิดพลาด: บันทึกข้อผิดพลาดในแผงบันทึกพร้อมด้วยการติดตามสแต็ก
- ความคิดเห็นของผู้ใช้: แสดงข้อความแสดงข้อผิดพลาดที่เป็นมิตรในแชท
- การล้างสถานะ: ทำให้สถานะข้อความเสร็จสมบูรณ์แม้ว่าจะเกิดข้อผิดพลาดก็ตาม
ซึ่งจะช่วยให้แอปยังคงเสถียรและให้ความคิดเห็นที่เหมาะสมได้แม้ว่าจะเกิดปัญหาเกี่ยวกับบริการ LLM หรือการดำเนินการฟังก์ชันก็ตาม
พลังของการเรียกใช้ฟังก์ชันเพื่อประสบการณ์ของผู้ใช้
สิ่งที่คุณทำสำเร็จที่นี่แสดงให้เห็นว่า LLM สร้างอินเทอร์เฟซที่เป็นธรรมชาติและทรงพลังได้อย่างไร
- อินเทอร์เฟซภาษาธรรมชาติ: ผู้ใช้แสดงเจตนาในภาษาที่ใช้ในชีวิตประจำวัน
- การตีความอัจฉริยะ: LLM จะแปลคำอธิบายที่คลุมเครือเป็นค่าที่แม่นยำ
- การจัดการโดยตรง: UI จะอัปเดตเพื่อตอบสนองต่อภาษาธรรมชาติ
- คำตอบตามบริบท: LLM จะให้บริบทการสนทนาเกี่ยวกับการเปลี่ยนแปลง
- ภาระทางปัญญาต่ำ: ผู้ใช้ไม่จำเป็นต้องเข้าใจค่า RGB หรือทฤษฎีสี
รูปแบบการใช้การเรียกใช้ฟังก์ชัน LLM เพื่อเชื่อมโยงภาษาที่เป็นธรรมชาติกับการดำเนินการใน UI นี้สามารถขยายไปยังโดเมนอื่นๆ ได้อีกมากมายนอกเหนือจากการเลือกสี
ขั้นตอนต่อไปคืออะไร
ในขั้นตอนถัดไป คุณจะปรับปรุงประสบการณ์ของผู้ใช้ด้วยการใช้การตอบกลับแบบสตรีม คุณจะประมวลผลกลุ่มข้อความและการเรียกใช้ฟังก์ชันเมื่อได้รับแทนที่จะรอคำตอบที่สมบูรณ์ ซึ่งจะทำให้แอปพลิเคชันตอบสนองและน่าสนใจยิ่งขึ้น
การแก้ปัญหา
ปัญหาเกี่ยวกับการเรียกใช้ฟังก์ชัน
หาก Gemini ไม่เรียกใช้ฟังก์ชันหรือพารามิเตอร์ไม่ถูกต้อง ให้ทำดังนี้
- ตรวจสอบว่าการประกาศฟังก์ชันตรงกับที่อธิบายไว้ในพรอมต์ของระบบ
- ตรวจสอบว่าชื่อและประเภทพารามิเตอร์สอดคล้องกัน
- ตรวจสอบว่าพรอมต์ของระบบสั่งให้ LLM ใช้เครื่องมืออย่างชัดเจน
- ตรวจสอบว่าชื่อฟังก์ชันในตัวแฮนเดิลตรงกับชื่อในประกาศทุกประการ
- ตรวจสอบแผงบันทึกเพื่อดูข้อมูลโดยละเอียดเกี่ยวกับการเรียกฟังก์ชัน
ปัญหาเกี่ยวกับคำตอบของฟังก์ชัน
หากระบบไม่ส่งผลลัพธ์ของฟังก์ชันกลับไปยัง LLM อย่างถูกต้อง ให้ทำดังนี้
- ตรวจสอบว่าฟังก์ชันแสดงผลแผนที่ที่มีรูปแบบถูกต้อง
- ตรวจสอบว่ามีการสร้าง Content.functionResponses อย่างถูกต้อง
- มองหาข้อผิดพลาดในบันทึกที่เกี่ยวข้องกับการตอบกลับของฟังก์ชัน
- ตรวจสอบว่าคุณใช้เซสชันแชทเดียวกันในการตอบกลับ
ปัญหาเกี่ยวกับการแสดงสี
หากสีแสดงไม่ถูกต้อง ให้ทำดังนี้
- ตรวจสอบว่าค่า RGB แปลงเป็นค่าทศนิยมอย่างถูกต้อง (LLM อาจส่งค่าเป็นจำนวนเต็ม)
- ตรวจสอบว่าค่าอยู่ในช่วงที่คาดไว้ (0.0 ถึง 1.0)
- ตรวจสอบว่ามีการเรียกใช้ตัวแจ้งสถานะสีอย่างถูกต้อง
- ตรวจสอบบันทึกเพื่อดูค่าที่แน่นอนซึ่งส่งไปยังฟังก์ชัน
ปัญหาทั่วไป
สำหรับปัญหาทั่วไป
- ตรวจสอบบันทึกเพื่อหาข้อผิดพลาดหรือคำเตือน
- ยืนยันการเชื่อมต่อ Firebase AI Logic
- ตรวจสอบพารามิเตอร์ฟังก์ชันว่ามีประเภทไม่ตรงกันหรือไม่
- ตรวจสอบว่าโค้ดที่ Riverpod สร้างขึ้นทั้งหมดเป็นเวอร์ชันล่าสุด
แนวคิดหลักที่ได้เรียนรู้
- การใช้ไปป์ไลน์การเรียกใช้ฟังก์ชันที่สมบูรณ์ใน Flutter
- สร้างการสื่อสารแบบเต็มระหว่าง LLM กับแอปพลิเคชัน
- การประมวลผล Structured Data จากการตอบกลับของ LLM
- ส่งผลลัพธ์ของฟังก์ชันกลับไปยัง LLM เพื่อรวมไว้ในการตอบกลับ
- การใช้แผงบันทึกเพื่อเพิ่มระดับการเข้าถึงการโต้ตอบของแอปพลิเคชัน LLM
- เชื่อมต่ออินพุตภาษาธรรมชาติกับการเปลี่ยนแปลง UI ที่ชัดเจน
เมื่อทำขั้นตอนนี้เสร็จแล้ว ตอนนี้แอปของคุณจะแสดงรูปแบบที่มีประสิทธิภาพมากที่สุดรูปแบบหนึ่งสำหรับการผสานรวม LLM นั่นคือการแปลอินพุตภาษาธรรมชาติเป็นการดำเนินการ UI ที่ชัดเจน ในขณะที่ยังคงการสนทนาที่สอดคล้องกันซึ่งรับทราบการดำเนินการเหล่านี้ ซึ่งจะสร้างอินเทอร์เฟซการสนทนาที่ใช้งานง่ายและน่าทึ่งสำหรับผู้ใช้
7. การสตรีมคำตอบเพื่อประสบการณ์ของผู้ใช้ที่ดียิ่งขึ้น
ในขั้นตอนนี้ คุณจะปรับปรุงประสบการณ์ของผู้ใช้โดยการใช้การตอบกลับแบบสตรีมจาก Gemini คุณจะประมวลผลกลุ่มข้อความและการเรียกใช้ฟังก์ชันเมื่อได้รับแทนที่จะรอให้ระบบสร้างคำตอบทั้งหมด ซึ่งจะช่วยสร้างแอปพลิเคชันที่ตอบสนองและน่าสนใจยิ่งขึ้น
สิ่งที่คุณจะทำในขั้นตอนนี้
- ความสำคัญของการสตรีมสำหรับแอปพลิเคชันที่ทำงานด้วย LLM
- การติดตั้งใช้งานการตอบกลับของ LLM แบบสตรีมมิงในแอปพลิเคชัน Flutter
- ประมวลผลข้อความบางส่วนเมื่อได้รับจาก API
- การจัดการสถานะการสนทนาเพื่อป้องกันข้อความที่ขัดแย้งกัน
- การจัดการการเรียกใช้ฟังก์ชันในการตอบกลับแบบสตรีม
- การสร้างตัวบ่งชี้ภาพสำหรับคำตอบที่อยู่ระหว่างดำเนินการ
เหตุใดการสตรีมจึงมีความสำคัญต่อแอปพลิเคชัน LLM
ก่อนที่จะนำไปใช้ เรามาดูเหตุผลที่การสตรีมคำตอบมีความสำคัญต่อการสร้างประสบการณ์การใช้งานที่ยอดเยี่ยมด้วย LLM กัน
ประสบการณ์ของผู้ใช้ที่ดีขึ้น
การสตรีมคำตอบมีประโยชน์ต่อประสบการณ์ของผู้ใช้หลายประการ ดังนี้
- ลดเวลาในการตอบสนองที่รับรู้ได้: ผู้ใช้จะเห็นข้อความเริ่มปรากฏขึ้นทันที (โดยปกติภายใน 100-300 มิลลิวินาที) แทนที่จะต้องรอหลายวินาทีเพื่อให้ได้คำตอบที่สมบูรณ์ การรับรู้ถึงความรวดเร็วนี้ช่วยเพิ่มความพึงพอใจของผู้ใช้อย่างมาก
- จังหวะการสนทนาที่เป็นธรรมชาติ: การปรากฏของข้อความทีละน้อยจะเลียนแบบวิธีที่มนุษย์สื่อสาร ซึ่งสร้างประสบการณ์การสนทนาที่เป็นธรรมชาติมากขึ้น
- การประมวลผลข้อมูลแบบค่อยเป็นค่อยไป: ผู้ใช้สามารถเริ่มประมวลผลข้อมูลได้ทันทีที่ได้รับ แทนที่จะต้องรับมือกับข้อความจำนวนมากในคราวเดียว
- โอกาสในการหยุดการทำงานก่อนเวลาอันควร: ในแอปพลิเคชันแบบเต็ม ผู้ใช้อาจหยุดหรือเปลี่ยนเส้นทาง LLM หากเห็นว่า LLM กำลังไปในทิศทางที่ไม่เป็นประโยชน์
- การยืนยันกิจกรรมด้วยภาพ: ข้อความสตรีมจะให้ความคิดเห็นทันทีว่าระบบทำงานอยู่ ซึ่งช่วยลดความไม่แน่นอน
ข้อดีทางเทคนิค
นอกเหนือจากการปรับปรุง UX แล้ว การสตรีมยังมีประโยชน์ด้านเทคนิคดังนี้
- การเรียกใช้ฟังก์ชันก่อนเวลา: ระบบจะตรวจหาและเรียกใช้การเรียกใช้ฟังก์ชันได้ทันทีที่ปรากฏในสตรีมโดยไม่ต้องรอการตอบกลับที่สมบูรณ์
- การอัปเดต UI แบบเพิ่มขึ้น: คุณสามารถอัปเดต UI ได้ทีละขั้นเมื่อได้รับข้อมูลใหม่ ซึ่งจะช่วยสร้างประสบการณ์การใช้งานที่ไดนามิกมากขึ้น
- การจัดการสถานะการสนทนา: การสตรีมจะให้สัญญาณที่ชัดเจนเกี่ยวกับเวลาที่คำตอบเสร็จสมบูรณ์เทียบกับเวลาที่ยังดำเนินการอยู่ ซึ่งช่วยให้จัดการสถานะได้ดียิ่งขึ้น
- ลดความเสี่ยงของการหมดเวลา: คำตอบที่ไม่ใช่แบบสตรีมอาจทำให้การสร้างที่ใช้เวลานานเสี่ยงต่อการหมดเวลาการเชื่อมต่อ การสตรีมจะสร้างการเชื่อมต่อตั้งแต่เนิ่นๆ และคงการเชื่อมต่อไว้
สําหรับแอป 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ที่มีการตอบกลับที่สมบูรณ์- แต่ละก้อนอาจมีข้อความ การเรียกใช้ฟังก์ชัน หรือทั้ง 2 อย่าง
- การประมวลผลแบบก้าวหน้า:
await forประมวลผลแต่ละส่วนเมื่อได้รับแบบเรียลไทม์- ข้อความจะต่อท้าย UI ทันที ทำให้เกิดเอฟเฟกต์การสตรีม
- ระบบจะเรียกใช้การเรียกฟังก์ชันทันทีที่ตรวจพบ
- การจัดการการเรียกฟังก์ชัน:
- เมื่อตรวจพบการเรียกใช้ฟังก์ชันในก้อนข้อมูล ระบบจะดำเนินการทันที
- ระบบจะส่งผลลัพธ์กลับไปยัง 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) จะใช้สถานะนี้เพื่อปิดใช้การป้อนข้อความขณะที่ระบบกำลังประมวลผลคำตอบ
ซึ่งจะสร้างประสบการณ์ของผู้ใช้ที่สอดคล้องกัน โดยที่ UI จะแสดงสถานะปัจจุบันของการสนทนา
สร้างโค้ด Riverpod
เรียกใช้คำสั่ง Build Runner เพื่อสร้างโค้ด Riverpod ที่จำเป็น
dart run build_runner build --delete-conflicting-outputs
เรียกใช้และทดสอบการตอบกลับแบบสตรีม
เรียกใช้แอปพลิเคชัน
flutter run -d DEVICE

ตอนนี้ลองทดสอบลักษณะการทำงานของการสตรีมด้วยคำอธิบายสีต่างๆ ลองใช้คำอธิบายต่อไปนี้
- "แสดงสีน้ำเงินเข้มของมหาสมุทรตอนพลบค่ำให้ดูหน่อย"
- "ฉันอยากเห็นปะการังสีสดใสที่ทำให้ฉันนึกถึงดอกไม้เขตร้อน"
- "สร้างสีเขียวมะกอกแบบหม่นๆ เหมือนชุดทหารเก่า"
ขั้นตอนทางเทคนิคของการสตรีมโดยละเอียด
มาดูสิ่งที่เกิดขึ้นเมื่อสตรีมคำตอบกัน
การสร้างการเชื่อมต่อ
เมื่อคุณโทรหา sendMessageStream() จะเกิดสิ่งต่อไปนี้
- แอปสร้างการเชื่อมต่อกับบริการตรรกะ AI ของ Firebase
- ส่งคำขอของผู้ใช้ไปยังบริการ
- เซิร์ฟเวอร์เริ่มประมวลผลคำขอ
- การเชื่อมต่อสตรีมจะยังคงเปิดอยู่และพร้อมที่จะส่งข้อมูลเป็นกลุ่ม
การส่งแบบเป็นกลุ่ม
เมื่อ Gemini สร้างเนื้อหา ระบบจะส่งเนื้อหาเป็นกลุ่มๆ ผ่านสตรีม
- เซิร์ฟเวอร์จะส่งกลุ่มข้อความเมื่อสร้างเสร็จ (โดยปกติจะเป็นคำหรือประโยค 2-3 คำ)
- เมื่อ Gemini ตัดสินใจที่จะเรียกใช้ฟังก์ชัน ระบบจะส่งข้อมูลการเรียกใช้ฟังก์ชัน
- อาจมีข้อความเพิ่มเติมตามหลังการเรียกใช้ฟังก์ชัน
- สตรีมจะดำเนินต่อไปจนกว่าการสร้างจะเสร็จสมบูรณ์
การประมวลผลแบบก้าวหน้า
แอปจะประมวลผลแต่ละก้อนทีละรายการ
- ระบบจะต่อข้อความแต่ละส่วนเข้ากับคำตอบที่มีอยู่
- ระบบจะเรียกใช้การเรียกฟังก์ชันทันทีที่ตรวจพบ
- UI จะอัปเดตแบบเรียลไทม์พร้อมทั้งข้อความและผลลัพธ์ของฟังก์ชัน
- ระบบจะติดตามสถานะเพื่อแสดงว่าการตอบกลับยังคงสตรีมอยู่
การไลฟ์สดจบลง
เมื่อสร้างเสร็จแล้ว
- เซิร์ฟเวอร์ปิดสตรีม
await forจะสิ้นสุดโดยอัตโนมัติ- ข้อความถูกทำเครื่องหมายว่าเสร็จสมบูรณ์แล้ว
- ระบบจะตั้งค่าสถานะการสนทนากลับเป็นไม่ได้ใช้งาน
- UI จะอัปเดตเพื่อแสดงสถานะเสร็จสมบูรณ์
การเปรียบเทียบการสตรีมกับที่ไม่ใช่การสตรีม
มาเปรียบเทียบแนวทางการสตรีมกับแนวทางที่ไม่ใช่การสตรีมเพื่อทำความเข้าใจประโยชน์ของการสตรีมให้ดียิ่งขึ้น
ทิศด้านลาด | ไม่ใช่การสตรีม | สตรีมมิง |
เวลาในการตอบสนองที่รับรู้ | ผู้ใช้จะไม่เห็นอะไรจนกว่าคำตอบที่สมบูรณ์จะพร้อม | ผู้ใช้เห็นคำแรกภายในมิลลิวินาที |
ประสบการณ์ของผู้ใช้ | รอเป็นเวลานานแล้วข้อความก็ปรากฏขึ้นอย่างกะทันหัน | ข้อความจะปรากฏอย่างเป็นธรรมชาติและค่อยๆ ชัดขึ้น |
การจัดการสถานะ | ง่ายขึ้น (ข้อความจะรอดำเนินการหรือเสร็จสมบูรณ์) | ซับซ้อนกว่า (ข้อความอาจอยู่ในสถานะการสตรีม) |
การเรียกใช้ฟังก์ชัน | เกิดขึ้นหลังจากได้รับคำตอบที่สมบูรณ์เท่านั้น | เกิดขึ้นระหว่างการสร้างคำตอบ |
ความซับซ้อนของการติดตั้งใช้งาน | ใช้งานง่ายกว่า | ต้องมีการจัดการสถานะเพิ่มเติม |
การแก้ไขข้อผิดพลาด | การตอบสนองแบบทั้งหมดหรือไม่มีเลย | คำตอบบางส่วนอาจยังมีประโยชน์ |
ความซับซ้อนของโค้ด | ซับซ้อนน้อยลง | ซับซ้อนมากขึ้นเนื่องจากการจัดการสตรีม |
สำหรับแอปพลิเคชันอย่าง Colorist ประโยชน์ด้าน UX ของการสตรีมมีมากกว่าความซับซ้อนในการติดตั้งใช้งาน โดยเฉพาะอย่างยิ่งสำหรับการตีความสีที่อาจใช้เวลาหลายวินาทีในการสร้าง
แนวทางปฏิบัติแนะนำสำหรับ UX การสตรีม
เมื่อติดตั้งใช้งานการสตรีมในแอปพลิเคชัน LLM ของคุณเอง ให้พิจารณาแนวทางปฏิบัติแนะนำต่อไปนี้
- ตัวบ่งชี้ภาพที่ชัดเจน: ระบุคิวภาพที่ชัดเจนเสมอเพื่อแยกความแตกต่างระหว่างข้อความที่สตรีมกับข้อความที่สมบูรณ์
- การบล็อกอินพุต: ปิดใช้การป้อนข้อมูลของผู้ใช้ระหว่างการสตรีมเพื่อป้องกันไม่ให้คำขอหลายรายการทับซ้อนกัน
- การกู้คืนจากข้อผิดพลาด: ออกแบบ UI เพื่อจัดการการกู้คืนอย่างราบรื่นหากการสตรีมถูกขัดจังหวะ
- การเปลี่ยนสถานะ: ตรวจสอบว่าการเปลี่ยนสถานะระหว่างสถานะว่าง สตรีมมิง และเสร็จสมบูรณ์เป็นไปอย่างราบรื่น
- การแสดงภาพความคืบหน้า: ลองใช้ภาพเคลื่อนไหวหรือตัวบ่งชี้ที่ละเอียดอ่อนซึ่งแสดงการประมวลผลที่ใช้งานอยู่
- ตัวเลือกการยกเลิก: ในแอปที่สมบูรณ์ ให้ระบุวิธีให้ผู้ใช้ยกเลิกการสร้างที่กำลังดำเนินการ
- การผสานรวมผลลัพธ์ของฟังก์ชัน: ออกแบบ UI เพื่อจัดการผลลัพธ์ของฟังก์ชันที่ปรากฏกลางสตรีม
- การเพิ่มประสิทธิภาพ: ลดการสร้าง UI ใหม่ระหว่างการอัปเดตสตรีมอย่างรวดเร็ว
colorist_ui แพ็กเกจจะใช้แนวทางปฏิบัติแนะนำเหล่านี้หลายอย่างให้คุณ แต่ก็เป็นข้อควรพิจารณาที่สำคัญสำหรับการติดตั้งใช้งาน LLM แบบสตรีมมิง
ขั้นตอนต่อไปคืออะไร
ในขั้นตอนถัดไป คุณจะใช้การซิงค์ LLM โดยการแจ้งให้ Gemini ทราบเมื่อผู้ใช้เลือกสีจากประวัติ ซึ่งจะสร้างประสบการณ์การใช้งานที่สอดคล้องกันมากขึ้น โดย LLM จะรับรู้ถึงการเปลี่ยนแปลงที่ผู้ใช้เริ่มต้นในสถานะของแอปพลิเคชัน
การแก้ปัญหา
ปัญหาเกี่ยวกับการประมวลผลสตรีม
หากพบปัญหาเกี่ยวกับการประมวลผลสตรีม ให้ทำดังนี้
- อาการ: คำตอบบางส่วน ข้อความขาดหายไป หรือการสิ้นสุดสตรีมอย่างกะทันหัน
- วิธีแก้ปัญหา: ตรวจสอบการเชื่อมต่อเครือข่ายและดูว่ามีรูปแบบ async/await ที่เหมาะสมในโค้ดหรือไม่
- การวินิจฉัย: ตรวจสอบแผงบันทึกเพื่อดูข้อความแสดงข้อผิดพลาดหรือคำเตือนที่เกี่ยวข้องกับการประมวลผลสตรีม
- แก้ไข: ตรวจสอบว่าการประมวลผลสตรีมทั้งหมดใช้การจัดการข้อผิดพลาดที่เหมาะสมกับบล็อก
try/catch
การเรียกใช้ฟังก์ชันที่ขาดหายไป
หากตรวจไม่พบการเรียกใช้ฟังก์ชันในสตรีม ให้ทำดังนี้
- อาการ: ข้อความปรากฏแต่สีไม่อัปเดต หรือบันทึกแสดงว่าไม่มีการเรียกฟังก์ชัน
- วิธีแก้ไข: ตรวจสอบวิธีการใช้การเรียกใช้ฟังก์ชันในพรอมต์ของระบบ
- การวินิจฉัย: ตรวจสอบแผงบันทึกเพื่อดูว่าได้รับฟังก์ชันการโทรหรือไม่
- วิธีแก้ไข: ปรับพรอมต์ของระบบเพื่อสั่งให้ LLM ใช้เครื่องมือ
set_colorอย่างชัดเจนยิ่งขึ้น
การจัดการข้อผิดพลาดทั่วไป
หากพบปัญหาอื่นๆ ให้ทำดังนี้
- ขั้นตอนที่ 1: ตรวจสอบแผงบันทึกว่ามีข้อความแสดงข้อผิดพลาดหรือไม่
- ขั้นตอนที่ 2: ตรวจสอบการเชื่อมต่อ Firebase AI Logic
- ขั้นตอนที่ 3: ตรวจสอบว่าโค้ดทั้งหมดที่ Riverpod สร้างขึ้นเป็นเวอร์ชันล่าสุด
- ขั้นตอนที่ 4: ตรวจสอบการติดตั้งใช้งานการสตรีมเพื่อดูว่ามีคำสั่ง await ใดขาดหายไป
แนวคิดหลักที่ได้เรียนรู้
- การใช้การตอบกลับแบบสตรีมมิงด้วย Gemini API เพื่อ UX ที่ตอบสนองได้ดียิ่งขึ้น
- การจัดการสถานะการสนทนาเพื่อจัดการการโต้ตอบแบบสตรีมมิงอย่างเหมาะสม
- ประมวลผลข้อความแบบเรียลไทม์และการเรียกใช้ฟังก์ชันเมื่อได้รับ
- การสร้าง UI ที่ตอบสนองซึ่งอัปเดตทีละรายการระหว่างการสตรีม
- การจัดการสตรีมพร้อมกันด้วยรูปแบบที่ไม่พร้อมกันที่เหมาะสม
- ให้ผลตอบสนองแบบภาพที่เหมาะสมระหว่างการตอบกลับแบบสตรีม
การใช้การสตรีมช่วยปรับปรุงประสบการณ์ของผู้ใช้แอป Colorist ได้อย่างมาก ทำให้เกิดอินเทอร์เฟซที่ตอบสนองและน่าสนใจยิ่งขึ้น ซึ่งให้ความรู้สึกเหมือนเป็นการสนทนาจริงๆ
8. การซิงค์บริบทของ LLM
ในขั้นตอนโบนัสนี้ คุณจะใช้การซิงค์บริบท LLM โดยการแจ้งให้ Gemini ทราบเมื่อผู้ใช้เลือกสีจากประวัติ ซึ่งจะสร้างประสบการณ์ที่สอดคล้องกันมากขึ้น โดย LLM จะรับรู้การกระทําของผู้ใช้ในอินเทอร์เฟซ ไม่ใช่แค่ข้อความที่ชัดเจน
สิ่งที่คุณจะทำในขั้นตอนนี้
- การสร้างการซิงค์บริบท LLM ระหว่าง UI กับ LLM
- แปลงเหตุการณ์ UI เป็นบริบทที่ LLM เข้าใจได้
- อัปเดตบริบทการสนทนาตามการกระทำของผู้ใช้
- การสร้างประสบการณ์ที่สอดคล้องกันในวิธีการโต้ตอบต่างๆ
- การปรับปรุงการรับรู้บริบทของ LLM ให้มากกว่าข้อความแชทที่ชัดเจน
ทำความเข้าใจการซิงค์บริบทของ LLM
แชทบอทแบบเดิมจะตอบกลับเฉพาะข้อความของผู้ใช้ที่ชัดเจนเท่านั้น ซึ่งทำให้เกิดการขาดการเชื่อมต่อเมื่อผู้ใช้โต้ตอบกับแอปผ่านช่องทางอื่นๆ การซิงค์บริบท LLM จะช่วยแก้ข้อจำกัดนี้ได้
เหตุผลที่การซิงค์บริบทของ LLM มีความสำคัญ
เมื่อผู้ใช้โต้ตอบกับแอปผ่านองค์ประกอบ UI (เช่น การเลือกสีจากประวัติ) LLM จะไม่มีทางทราบว่าเกิดอะไรขึ้น เว้นแต่คุณจะบอกอย่างชัดเจน การซิงค์บริบทของ LLM:
- คงบริบทไว้: แจ้งให้ LLM ทราบเกี่ยวกับการดำเนินการของผู้ใช้ที่เกี่ยวข้องทั้งหมด
- สร้างความสอดคล้องกัน: สร้างประสบการณ์ที่สอดคล้องกันซึ่ง LLM รับรู้การโต้ตอบ UI
- เพิ่มความอัจฉริยะ: ช่วยให้ LLM ตอบสนองต่อการกระทำของผู้ใช้ทั้งหมดได้อย่างเหมาะสม
- ปรับปรุงประสบการณ์ของผู้ใช้: ทำให้แอปพลิเคชันทั้งหมดดูผสานรวมและตอบสนองได้ดีขึ้น
- ลดความพยายามของผู้ใช้: ช่วยให้ผู้ใช้ไม่ต้องอธิบายการดำเนินการใน UI ด้วยตนเอง
ในแอป Colorist เมื่อผู้ใช้เลือกสีจากประวัติ คุณต้องการให้ Gemini รับทราบการดำเนินการนี้และแสดงความคิดเห็นอย่างชาญฉลาดเกี่ยวกับสีที่เลือก เพื่อคงภาพลวงตาของความราบรื่นและผู้ช่วยที่รับรู้
อัปเดตบริการแชทของ Gemini เพื่อรับการแจ้งเตือนการเลือกสี
ก่อนอื่น คุณจะต้องเพิ่มเมธอดไปยัง 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การเรียกกลับ ซึ่งเชื่อมต่อเหตุการณ์ UI (การเลือกสีจากประวัติ) กับระบบการแจ้งเตือนของ LLM
อัปเดตพรอมต์ของระบบ
ตอนนี้คุณต้องอัปเดตพรอมต์ของระบบเพื่อสั่งให้ LLM ตอบกลับการแจ้งเตือนการเลือกสี แก้ไขไฟล์ assets/system_prompt.md โดยทำดังนี้
assets/system_prompt.md
# Colorist System Prompt
You are a color expert assistant integrated into a desktop app called Colorist. Your job is to interpret natural language color descriptions and set the appropriate color values using a specialized tool.
## Your Capabilities
You are knowledgeable about colors, color theory, and how to translate natural language descriptions into specific RGB values. You have access to the following tool:
`set_color` - Sets the RGB values for the color display based on a description
## How to Respond to User Inputs
When users describe a color:
1. First, acknowledge their color description with a brief, friendly response
2. Interpret what RGB values would best represent that color description
3. Use the `set_color` tool to set those values (all values should be between 0.0 and 1.0)
4. After setting the color, provide a brief explanation of your interpretation
Example:
User: "I want a sunset orange"
You: "Sunset orange is a warm, vibrant color that captures the golden-red hues of the setting sun. It combines a strong red component with moderate orange tones."
[Then you would call the set_color tool with approximately: red=1.0, green=0.5, blue=0.25]
After the tool call: "I've set a warm orange with strong red, moderate green, and minimal blue components that is reminiscent of the sun low on the horizon."
## When Descriptions are Unclear
If a color description is ambiguous or unclear, please ask the user clarifying questions, one at a time.
## When Users Select Historical Colors
Sometimes, the user will manually select a color from the history panel. When this happens, you'll receive a notification about this selection that includes details about the color. Acknowledge this selection with a brief response that recognizes what they've done and comments on the selected color.
Example notification:
User: "User selected color from history: {red: 0.2, green: 0.5, blue: 0.8, hexCode: #3380CC}"
You: "I see you've selected an ocean blue from your history. This tranquil blue with a moderate intensity has a calming, professional quality to it. Would you like to explore similar shades or create a contrasting color?"
## Important Guidelines
- Always keep RGB values between 0.0 and 1.0
- Provide thoughtful, knowledgeable responses about colors
- When possible, include color psychology, associations, or interesting facts about colors
- Be conversational and engaging in your responses
- Focus on being helpful and accurate with your color interpretations
การเปลี่ยนแปลงที่สำคัญคือส่วน "เมื่อผู้ใช้เลือกสีในประวัติ" ซึ่งมีรายละเอียดดังนี้
- อธิบายแนวคิดของการแจ้งเตือนการเลือกประวัติให้ LLM
- แสดงตัวอย่างลักษณะของการแจ้งเตือนเหล่านี้
- แสดงตัวอย่างการตอบกลับที่เหมาะสม
- กำหนดสิ่งที่คาดหวังสำหรับการรับทราบการเลือกและแสดงความคิดเห็นเกี่ยวกับสี
ซึ่งจะช่วยให้ LLM เข้าใจวิธีตอบกลับข้อความพิเศษเหล่านี้อย่างเหมาะสม
สร้างโค้ด Riverpod
เรียกใช้คำสั่ง Build Runner เพื่อสร้างโค้ด Riverpod ที่จำเป็น
dart run build_runner build --delete-conflicting-outputs
เรียกใช้และทดสอบการซิงค์บริบทของ LLM
เรียกใช้แอปพลิเคชัน
flutter run -d DEVICE

การทดสอบการซิงค์บริบท LLM มีขั้นตอนดังนี้
- ก่อนอื่นให้สร้างสี 2-3 สีโดยอธิบายสีในแชท
- "ขอดูสีม่วงสดใสหน่อย"
- "ฉันอยากได้สีเขียวป่า"
- "ให้ฉันสีแดงสด"
- จากนั้นคลิกภาพขนาดย่อของสีในแถบประวัติ
คุณควรสังเกตสิ่งต่อไปนี้
- สีที่เลือกจะปรากฏในจอแสดงผลหลัก
- ข้อความของผู้ใช้จะปรากฏในแชทเพื่อระบุการเลือกสี
- LLM จะตอบกลับโดยรับทราบการเลือกและแสดงความคิดเห็นเกี่ยวกับสี
- การโต้ตอบทั้งหมดให้ความรู้สึกเป็นธรรมชาติและสอดคล้องกัน
ซึ่งจะสร้างประสบการณ์การใช้งานที่ราบรื่นโดยที่ LLM จะรับรู้และตอบสนองต่อทั้งข้อความส่วนตัวและการโต้ตอบใน UI ได้อย่างเหมาะสม
วิธีการทำงานของการซิงค์บริบท LLM
มาดูรายละเอียดทางเทคนิคเกี่ยวกับวิธีการทำงานของการซิงค์นี้กัน
โฟลว์ข้อมูล
- การดำเนินการของผู้ใช้: ผู้ใช้คลิกสีในแถบประวัติ
- เหตุการณ์ UI: วิดเจ็ต
MainScreenตรวจพบการเลือกนี้ - การดำเนินการ Callback: ระบบจะทริกเกอร์ Callback
notifyColorSelection - การสร้างข้อความ: สร้างข้อความที่มีการจัดรูปแบบเป็นพิเศษด้วยข้อมูลสี
- การประมวลผล LLM: ระบบจะส่งข้อความไปยัง Gemini ซึ่งจะจดจำรูปแบบได้
- คำตอบตามบริบท: Gemini ตอบกลับอย่างเหมาะสมตามพรอมต์ของระบบ
- การอัปเดต UI: คำตอบจะปรากฏในแชทเพื่อสร้างประสบการณ์ที่สอดคล้องกัน
การซีเรียลไลซ์ข้อมูล
แง่มุมสำคัญของแนวทางนี้คือวิธีที่คุณทำให้ข้อมูลสีเป็นอนุกรม
'User selected color from history: ${json.encode(color.toLLMContextMap())}'
toLLMContextMap() เมธอด (ที่จัดทำโดยแพ็กเกจ colorist_ui) จะแปลงออบเจ็กต์ ColorData เป็นแผนที่ที่มีพร็อพเพอร์ตี้คีย์ที่ LLM เข้าใจได้ ซึ่งโดยทั่วไปจะมีดังต่อไปนี้
- ค่า RGB (แดง เขียว น้ำเงิน)
- การแสดงรหัสฐานสิบหก
- ชื่อหรือคำอธิบายที่เชื่อมโยงกับสี
การจัดรูปแบบข้อมูลนี้อย่างสม่ำเสมอและรวมไว้ในข้อความจะช่วยให้ LLM มีข้อมูลทั้งหมดที่จำเป็นต่อการตอบกลับอย่างเหมาะสม
การใช้งานการซิงค์บริบท LLM ที่กว้างขึ้น
รูปแบบการแจ้ง LLM เกี่ยวกับเหตุการณ์ UI นี้มีการใช้งานมากมายนอกเหนือจากการเลือกสี
กรณีการใช้งานอื่นๆ
- การเปลี่ยนแปลงตัวกรอง: แจ้ง LLM เมื่อผู้ใช้ใช้ตัวกรองกับข้อมูล
- เหตุการณ์การนำทาง: แจ้ง LLM เมื่อผู้ใช้ไปยังส่วนต่างๆ
- การเปลี่ยนแปลงการเลือก: อัปเดต LLM เมื่อผู้ใช้เลือกรายการจากรายการหรือตารางกริด
- การอัปเดตค่ากำหนด: บอก LLM เมื่อผู้ใช้เปลี่ยนการตั้งค่าหรือค่ากำหนด
- การจัดการข้อมูล: แจ้ง LLM เมื่อผู้ใช้เพิ่ม แก้ไข หรือลบข้อมูล
ในแต่ละกรณี รูปแบบจะยังคงเหมือนเดิม ดังนี้
- ตรวจหาเหตุการณ์ UI
- แปลงข้อมูลที่เกี่ยวข้องเป็นอนุกรม
- ส่งการแจ้งเตือนที่จัดรูปแบบเป็นพิเศษไปยัง LLM
- แนะนํา LLM ให้ตอบกลับอย่างเหมาะสมผ่านพรอมต์ของระบบ
แนวทางปฏิบัติแนะนำสำหรับการซิงค์บริบท LLM
ต่อไปนี้คือแนวทางปฏิบัติแนะนำบางส่วนสำหรับการซิงค์บริบท LLM อย่างมีประสิทธิภาพตามการใช้งานของคุณ
1. ใช้รูปแบบที่สม่ำเสมอ
ใช้รูปแบบที่สอดคล้องกันสำหรับการแจ้งเตือนเพื่อให้ LLM ระบุได้ง่าย
"User [action] [object]: [structured data]"
2. บริบทที่สมบูรณ์
ใส่รายละเอียดในการแจ้งเตือนให้เพียงพอเพื่อให้ LLM ตอบกลับได้อย่างชาญฉลาด สำหรับสี หมายถึงค่า RGB, รหัส Hex และพร็อพเพอร์ตี้อื่นๆ ที่เกี่ยวข้อง
3. วิธีการที่ชัดเจน
ระบุวิธีการที่ชัดเจนในพรอมต์ของระบบเกี่ยวกับวิธีจัดการการแจ้งเตือน โดยควรมีตัวอย่างประกอบ
4. การผสานรวมที่เป็นธรรมชาติ
ออกแบบการแจ้งเตือนให้เป็นไปอย่างเป็นธรรมชาติในการสนทนา ไม่ใช่การขัดจังหวะทางเทคนิค
5. การแจ้งเตือนแบบเลือก
แจ้ง LLM เฉพาะการดำเนินการที่เกี่ยวข้องกับการสนทนา ไม่จำเป็นต้องสื่อสารเหตุการณ์ UI ทุกรายการ
การแก้ปัญหา
ปัญหาการแจ้งเตือน
หาก LLM ไม่ตอบสนองต่อการเลือกสีอย่างถูกต้อง ให้ทำดังนี้
- ตรวจสอบว่ารูปแบบข้อความแจ้งเตือนตรงกับที่อธิบายไว้ในพรอมต์ของระบบ
- ตรวจสอบว่ามีการแปลงข้อมูลสีอย่างถูกต้อง
- ตรวจสอบว่าพรอมต์ของระบบมีวิธีการที่ชัดเจนในการจัดการการเลือก
- มองหาข้อผิดพลาดในบริการแชทเมื่อส่งการแจ้งเตือน
การจัดการบริบท
หาก LLM ดูเหมือนจะสูญเสียบริบท ให้ทำดังนี้
- ตรวจสอบว่าเซสชันการแชทได้รับการดูแลอย่างถูกต้อง
- ตรวจสอบว่าสถานะการสนทนาเปลี่ยนอย่างถูกต้อง
- ตรวจสอบว่าระบบส่งการแจ้งเตือนผ่านเซสชันแชทเดียวกัน
ปัญหาทั่วไป
สำหรับปัญหาทั่วไป
- ตรวจสอบบันทึกเพื่อหาข้อผิดพลาดหรือคำเตือน
- ยืนยันการเชื่อมต่อ Firebase AI Logic
- ตรวจสอบพารามิเตอร์ฟังก์ชันว่ามีประเภทไม่ตรงกันหรือไม่
- ตรวจสอบว่าโค้ดที่ Riverpod สร้างขึ้นทั้งหมดเป็นเวอร์ชันล่าสุด
แนวคิดหลักที่ได้เรียนรู้
- สร้างการซิงค์บริบทของ LLM ระหว่าง UI กับ LLM
- การแปลงเหตุการณ์ UI เป็นบริบทที่ LLM เข้าใจได้
- การกำหนดลักษณะการทำงานของ LLM สำหรับรูปแบบการโต้ตอบต่างๆ
- สร้างประสบการณ์ที่สอดคล้องกันในการโต้ตอบผ่านข้อความและที่ไม่ใช่ข้อความ
- การเพิ่มการรับรู้ของ LLM เกี่ยวกับสถานะแอปพลิเคชันในวงกว้าง
การใช้การซิงค์บริบท LLM ทำให้คุณสร้างประสบการณ์การใช้งานที่ผสานรวมอย่างแท้จริง ซึ่ง LLM จะให้ความรู้สึกเหมือนเป็นผู้ช่วยที่รับรู้และตอบสนองได้ ไม่ใช่แค่เครื่องมือสร้างข้อความ รูปแบบนี้สามารถนำไปใช้กับแอปพลิเคชันอื่นๆ ได้อีกมากมายเพื่อสร้างอินเทอร์เฟซที่ทำงานด้วยระบบ AI ที่เป็นธรรมชาติและใช้งานง่ายยิ่งขึ้น
9. ยินดีด้วย
คุณทำ Codelab Colorist เสร็จเรียบร้อยแล้ว 🎉
สิ่งที่คุณสร้าง
คุณได้สร้างแอปพลิเคชัน Flutter ที่ใช้งานได้อย่างเต็มรูปแบบซึ่งผสานรวม Gemini API ของ Google เพื่อตีความคำอธิบายสีในภาษาที่เป็นธรรมชาติ ตอนนี้แอปของคุณทำสิ่งต่อไปนี้ได้แล้ว
- ประมวลผลคำอธิบายภาษาธรรมชาติ เช่น "ส้มพระอาทิตย์ตก" หรือ "น้ำเงินทะเลลึก"
- ใช้ Gemini เพื่อแปลคำอธิบายเหล่านี้เป็นค่า RGB อย่างชาญฉลาด
- แสดงสีที่ตีความแบบเรียลไทม์ด้วยการตอบกลับแบบสตรีม
- จัดการการโต้ตอบของผู้ใช้ผ่านทั้งแชทและองค์ประกอบ UI
- คงความตระหนักตามบริบทในวิธีการโต้ตอบต่างๆ
ขั้นตอนถัดไป
เมื่อคุณเชี่ยวชาญพื้นฐานของการผสานรวม Gemini กับ Flutter แล้ว ต่อไปนี้คือวิธีต่างๆ ในการเดินทางต่อ
ปรับปรุงแอป Colorist
- ชุดสี: เพิ่มฟังก์ชันการทำงานเพื่อสร้างชุดสีที่เสริมกันหรือเข้ากัน
- การป้อนข้อมูลด้วยเสียง: ผสานรวมการจดจำคำพูดสำหรับคำอธิบายสีที่เป็นคำพูด
- การจัดการประวัติ: เพิ่มตัวเลือกในการตั้งชื่อ จัดระเบียบ และส่งออกชุดสี
- การแจ้งที่กำหนดเอง: สร้างอินเทอร์เฟซเพื่อให้ผู้ใช้ปรับแต่งพรอมต์ของระบบ
- ข้อมูลวิเคราะห์ขั้นสูง: ติดตามว่าคำอธิบายใดทำงานได้ดีที่สุดหรือทำให้เกิดปัญหา
สำรวจฟีเจอร์อื่นๆ ของ Gemini
- อินพุตหลายรูปแบบ: เพิ่มอินพุตรูปภาพเพื่อแยกสีจากรูปภาพ
- การสร้างเนื้อหา: ใช้ Gemini เพื่อสร้างเนื้อหาที่เกี่ยวข้องกับสี เช่น คำอธิบายหรือเรื่องราว
- การปรับปรุงการเรียกใช้ฟังก์ชัน: สร้างการผสานรวมเครื่องมือที่ซับซ้อนมากขึ้นด้วยฟังก์ชันหลายรายการ
- การตั้งค่าความปลอดภัย: สำรวจการตั้งค่าความปลอดภัยต่างๆ และผลกระทบต่อคำตอบ
ใช้รูปแบบเหล่านี้กับโดเมนอื่นๆ
- การวิเคราะห์เอกสาร: สร้างแอปที่เข้าใจและวิเคราะห์เอกสารได้
- ความช่วยเหลือด้านการเขียนเชิงสร้างสรรค์: สร้างเครื่องมือช่วยเขียนด้วยคำแนะนำที่ทำงานด้วย LLM
- การทำงานอัตโนมัติ: ออกแบบแอปที่แปลภาษาธรรมชาติเป็นงานอัตโนมัติ
- แอปพลิเคชันที่อิงตามความรู้: สร้างระบบผู้เชี่ยวชาญในโดเมนที่เฉพาะเจาะจง
แหล่งข้อมูล
แหล่งข้อมูลที่มีประโยชน์บางส่วนที่จะช่วยให้คุณเรียนรู้ต่อไปได้มีดังนี้
เอกสารประกอบอย่างเป็นทางการ
หลักสูตรและคำแนะนำในการเขียนพรอมต์
ชุมชน
Observable Flutter Agentic series
ในตอนที่ 59 Craig Labenz และ Andrew Brogden จะมาสำรวจ Codelab นี้ พร้อมไฮไลต์ส่วนที่น่าสนใจของการสร้างแอป
ในตอนที่ #60 มาเข้าร่วมกับ Craig และ Andrew อีกครั้งขณะที่ทั้งคู่ขยายแอป Codelab ด้วยความสามารถใหม่ๆ และพยายามทำให้ LLM ทำตามที่บอก
ในตอนที่ #61 Craig ได้ร่วมพูดคุยกับ Chris Sells เพื่อวิเคราะห์พาดหัวข่าวและสร้างรูปภาพที่เกี่ยวข้อง
ความคิดเห็น
เราอยากทราบความคิดเห็นเกี่ยวกับประสบการณ์การใช้งาน Codelab นี้ โปรดพิจารณาแสดงความคิดเห็นผ่านช่องทางต่อไปนี้
ขอขอบคุณที่ทำ Codelab นี้จนจบ และเราหวังว่าคุณจะสำรวจความเป็นไปได้ที่น่าตื่นเต้นระหว่าง Flutter กับ AI ต่อไป