สร้างแอป Flutter ที่ทำงานด้วย Gemini

1. สร้างแอป Flutter ที่ทำงานด้วย Gemini

สิ่งที่คุณจะสร้าง

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

Colorist ช่วยให้ผู้ใช้อธิบายสีเป็นภาษาที่เป็นธรรมชาติได้ (เช่น "สีส้มของดวงอาทิตย์ตก" หรือ "น้ำทะเลสีเข้ม") และแอปจะดำเนินการต่อไปนี้

  • ประมวลผลคำอธิบายเหล่านี้โดยใช้ Gemini API ของ Google
  • ตีความคำอธิบายเป็นค่าสี RGB ที่แม่นยำ
  • แสดงสีบนหน้าจอแบบเรียลไทม์
  • ให้รายละเอียดทางเทคนิคเกี่ยวกับสีและบริบทที่น่าสนใจเกี่ยวกับสี
  • เก็บประวัติสีที่สร้างขึ้นล่าสุด

ภาพหน้าจอแอป Colorist ที่แสดงการแสดงผลสีและอินเทอร์เฟซแชท

แอปมีอินเทอร์เฟซแบบแยกหน้าจอที่มีพื้นที่แสดงผลสีและระบบแชทแบบอินเทอร์แอกทีฟด้านหนึ่ง และแผงบันทึกแบบละเอียดที่แสดงการโต้ตอบ LLM ดิบอีกด้านหนึ่ง บันทึกนี้จะช่วยให้คุณเข้าใจวิธีการทำงานของการผสานรวม LLM ได้ดียิ่งขึ้น

เหตุผลที่นักพัฒนา Flutter ควรทราบ

LLM กำลังปฏิวัติวิธีที่ผู้ใช้โต้ตอบกับแอปพลิเคชัน แต่การผสานรวม LLM เข้ากับแอปบนอุปกรณ์เคลื่อนที่และเดสก์ท็อปอย่างมีประสิทธิภาพนั้นเป็นเรื่องท้าทาย โค้ดแล็บนี้จะสอนรูปแบบการใช้งานจริงที่มากกว่าการเรียกใช้ API ดิบ

เส้นทางการเรียนรู้ของคุณ

Codelab นี้จะแนะนำขั้นตอนการสร้าง Colorist โดยละเอียด

  1. การตั้งค่าโปรเจ็กต์ - คุณจะเริ่มด้วยโครงสร้างแอป Flutter พื้นฐานและแพ็กเกจ colorist_ui
  2. การผสานรวม Gemini ขั้นพื้นฐาน - เชื่อมต่อแอปกับ Vertex AI ใน Firebase และใช้การสื่อสาร LLM แบบง่าย
  3. พรอมต์ที่มีประสิทธิภาพ - สร้างพรอมต์ของระบบที่แนะนํา LLM ให้เข้าใจคําอธิบายสี
  4. ประกาศฟังก์ชัน - กำหนดเครื่องมือที่ LLM สามารถใช้เพื่อตั้งค่าสีในแอปพลิเคชัน
  5. การจัดการเครื่องมือ - ประมวลผลการเรียกฟังก์ชันจาก LLM และเชื่อมต่อกับสถานะของแอป
  6. การตอบกลับแบบสตรีม - ปรับปรุงประสบการณ์ของผู้ใช้ด้วยคำตอบ LLM แบบสตรีมแบบเรียลไทม์
  7. การซิงค์บริบท LLM - สร้างประสบการณ์การใช้งานที่สอดคล้องกันโดยการแจ้งให้ LLM ทราบถึงการดำเนินการของผู้ใช้

สิ่งที่คุณจะได้เรียนรู้

  • กําหนดค่า Vertex AI ใน Firebase สําหรับแอปพลิเคชัน 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 พูดคุยเกี่ยวกับโค้ดแล็บนี้ใน Observable Flutter ตอนที่ 59

ข้อกำหนดเบื้องต้น

สิ่งที่ควรมีเพื่อใช้ Codelab นี้ให้ได้ประโยชน์สูงสุด

  • ประสบการณ์การพัฒนา Flutter - ความคุ้นเคยกับพื้นฐานของ Flutter และไวยากรณ์ Dart
  • ความรู้เกี่ยวกับการเขียนโปรแกรมแบบอะซิงโครนัส - ทำความเข้าใจเกี่ยวกับ Futures, async/await และสตรีม
  • บัญชี Firebase - คุณจะต้องมีบัญชี Google เพื่อตั้งค่า Firebase
  • โปรเจ็กต์ Firebase ที่เปิดใช้การเรียกเก็บเงิน - Vertex AI ใน 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

Flag -e บ่งบอกว่าคุณต้องการโปรเจ็กต์ว่างที่ไม่มีแอป counter เริ่มต้น แอปนี้ออกแบบมาให้ทำงานได้บนเดสก์ท็อป อุปกรณ์เคลื่อนที่ และเว็บ แต่ขณะนี้ flutterfire ไม่รองรับ Linux

เพิ่มทรัพยากร 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 สําหรับแอป 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.8.0

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

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

flutter:
  uses-material-design: true

กำหนดค่าตัวเลือกการวิเคราะห์

เพิ่ม custom_lint ลงในไฟล์ analysis_options.yaml ที่รูทของโปรเจ็กต์

include: package:flutter_lints/flutter.yaml

analyzer:
  plugins:
    - custom_lint

การกําหนดค่านี้เปิดใช้โปรแกรมตรวจโค้ดเฉพาะของ Riverpod เพื่อช่วยรักษาคุณภาพโค้ด

ใช้ไฟล์ main.dart

แทนที่เนื้อหาของ lib/main.dart ด้วยข้อมูลต่อไปนี้

lib/main.dart

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

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

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

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

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

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

การดำเนินการนี้จะตั้งค่าแอป Flutter ให้ใช้บริการ Echo แบบง่ายซึ่งเลียนแบบลักษณะการทํางานของ LLM โดยการตอบกลับข้อความของผู้ใช้

ทําความเข้าใจสถาปัตยกรรม

มาดูสถาปัตยกรรมของแอป colorist กัน

แพ็กเกจ colorist_ui

แพ็กเกจ colorist_ui มีคอมโพเนนต์ UI และเครื่องมือการจัดการสถานะที่สร้างขึ้นล่วงหน้า ดังนี้

  1. MainScreen: คอมโพเนนต์ UI หลักที่แสดงข้อมูลต่อไปนี้
    • เลย์เอาต์แบบแยกหน้าจอบนเดสก์ท็อป (พื้นที่การโต้ตอบและแผงบันทึก)
    • อินเทอร์เฟซแบบแท็บบนอุปกรณ์เคลื่อนที่
    • การแสดงผลสี อินเทอร์เฟซการแชท และภาพขนาดย่อของประวัติ
  2. การจัดการสถานะ: แอปใช้ตัวแจ้งสถานะหลายรายการ ดังนี้
    • ChatStateNotifier: จัดการข้อความแชท
    • ColorStateNotifier: จัดการสีปัจจุบันและประวัติ
    • LogStateNotifier: จัดการรายการบันทึกสำหรับการแก้ไขข้อบกพร่อง
  3. การจัดการข้อความ: แอปใช้รูปแบบข้อความที่มีสถานะต่างๆ ดังนี้
    • ข้อความของผู้ใช้: ป้อนโดยผู้ใช้
    • ข้อความ LLM: สร้างขึ้นโดย LLM (หรือบริการ Echo ของคุณในตอนนี้)
    • MessageState: ติดตามว่าข้อความ LLM เสร็จสมบูรณ์แล้วหรือยัง หรือยังสตรีมอยู่

สถาปัตยกรรมแอปพลิเคชัน

แอปเป็นไปตามสถาปัตยกรรมต่อไปนี้

  1. เลเยอร์ UI: มาจากแพ็กเกจ colorist_ui
  2. การจัดการสถานะ: ใช้ Riverpod สำหรับการจัดการสถานะแบบรีแอ็กทีฟ
  3. เลเยอร์บริการ: ปัจจุบันมีบริการ Echo แบบง่าย ซึ่งจะแทนที่ด้วยบริการ Gemini Chat
  4. การผสานรวม LLM: จะเพิ่มในขั้นตอนต่อๆ ไป

การแยกนี้ช่วยให้คุณมุ่งเน้นที่การใช้งานการผสานรวม LLM ได้ในขณะที่คอมโพเนนต์ UI ได้รับการจัดการแล้ว

เรียกใช้แอป

เรียกใช้แอปด้วยคำสั่งต่อไปนี้

flutter run -d DEVICE

แทนที่ DEVICE ด้วยอุปกรณ์เป้าหมาย เช่น macos, windows, chrome หรือรหัสอุปกรณ์

ภาพหน้าจอแอป Colorist แสดงการแสดงผล Markdown ของบริการ Echo

ตอนนี้คุณควรเห็นแอป Colorist พร้อมสิ่งต่อไปนี้

  1. พื้นที่แสดงสีที่มีสีเริ่มต้น
  2. อินเทอร์เฟซแชทที่คุณสามารถพิมพ์ข้อความได้
  3. แผงบันทึกที่แสดงการโต้ตอบทางแชท

ลองพิมพ์ข้อความ เช่น "ฉันต้องการสีน้ำเงินเข้ม" แล้วกดส่ง บริการ Echo จะพูดข้อความของคุณซ้ำ ในขั้นตอนต่อๆ ไป คุณจะต้องแทนที่การตีความสีนี้ด้วยการใช้ Gemini API ผ่าน Vertex 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 โดยใช้ Vertex AI ใน Firebase คุณจะกำหนดค่า Firebase, ตั้งค่าผู้ให้บริการที่จำเป็น และติดตั้งใช้งานบริการแชทพื้นฐานที่สื่อสารกับ Gemini API

สิ่งที่คุณจะได้เรียนรู้ในขั้นตอนนี้

  • การตั้งค่า Firebase ในแอปพลิเคชัน Flutter
  • การกำหนดค่า Vertex AI ใน Firebase เพื่อเข้าถึง Gemini
  • การสร้างผู้ให้บริการ Riverpod สําหรับบริการ Firebase และ Gemini
  • การใช้บริการแชทพื้นฐานด้วย Gemini API
  • การจัดการการตอบกลับแบบไม่พร้อมกันของ API และสถานะข้อผิดพลาด

ตั้งค่า Firebase

ก่อนอื่น คุณต้องตั้งค่า Firebase สําหรับโปรเจ็กต์ Flutter ซึ่งรวมถึงการสร้างโปรเจ็กต์ Firebase การเพิ่มแอปลงในโปรเจ็กต์ และการกำหนดการตั้งค่า Vertex AI ที่จําเป็น

สร้างโปรเจ็กต์ Firebase

  1. ไปที่ Firebase Console แล้วลงชื่อเข้าใช้ด้วยบัญชี Google
  2. คลิกสร้างโปรเจ็กต์ Firebase หรือเลือกโปรเจ็กต์ที่มีอยู่
  3. ทำตามวิซาร์ดการตั้งค่าเพื่อสร้างโปรเจ็กต์
  4. เมื่อสร้างโปรเจ็กต์แล้ว คุณจะต้องอัปเกรดเป็นแพ็กเกจ Blaze (แบบชําระเงินตามการใช้งาน) เพื่อเข้าถึงบริการ Vertex AI คลิกปุ่มอัปเกรดที่ด้านซ้ายล่างของคอนโซล Firebase

ตั้งค่า Vertex AI ในโปรเจ็กต์ Firebase

  1. ในคอนโซล Firebase ให้ไปที่โปรเจ็กต์
  2. เลือก AI ในแถบด้านข้างซ้าย
  3. เลือกเริ่มต้นใช้งานในการ์ด Vertex AI ใน Firebase
  4. ทําตามข้อความแจ้งเพื่อเปิดใช้ Vertex AI ใน Firebase API สําหรับโปรเจ็กต์

ติดตั้ง FlutterFire CLI

FlutterFire CLI ช่วยลดความซับซ้อนของการตั้งค่า Firebase ในแอป Flutter

dart pub global activate flutterfire_cli

เพิ่ม Firebase ไปยังแอป Flutter

  1. เพิ่มแพ็กเกจ Firebase Core และ Vertex AI ลงในโปรเจ็กต์
flutter pub add firebase_core firebase_vertexai
  1. เรียกใช้คำสั่งการกําหนดค่า FlutterFire
flutterfire configure

คำสั่งนี้จะทําสิ่งต่อไปนี้

  • ขอให้คุณเลือกโปรเจ็กต์ Firebase ที่เพิ่งสร้างขึ้น
  • ลงทะเบียนแอป Flutter กับ Firebase
  • สร้างไฟล์ firebase_options.dart ที่มีการกำหนดค่าโปรเจ็กต์

คำสั่งจะตรวจหาแพลตฟอร์มที่เลือก (iOS, Android, macOS, Windows, เว็บ) โดยอัตโนมัติและกำหนดค่าให้เหมาะสม

การกําหนดค่าเฉพาะแพลตฟอร์ม

Firebase กำหนดเวอร์ชันขั้นต่ำที่สูงกว่าค่าเริ่มต้นสำหรับ Flutter นอกจากนี้ ยังต้องมีสิทธิ์เข้าถึงเครือข่ายเพื่อสื่อสารกับ Vertex AI ในเซิร์ฟเวอร์ Firebase ด้วย

กำหนดค่าสิทธิ์ macOS

สำหรับ macOS คุณต้องเปิดใช้การเข้าถึงเครือข่ายในการให้สิทธิ์ของแอป โดยทำดังนี้

  1. เปิด macos/Runner/DebugProfile.entitlements แล้วเพิ่มข้อมูลต่อไปนี้

macos/Runner/DebugProfile.entitlements

<key>com.apple.security.network.client</key>
<true/>
  1. นอกจากนี้ ให้เปิด macos/Runner/Release.entitlements แล้วเพิ่มรายการเดียวกัน
  2. อัปเดตเวอร์ชัน macOS ขั้นต่ำที่ด้านบนของ macos/Podfile

macos/Podfile

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

กำหนดค่าสิทธิ์ iOS

สำหรับ iOS ให้อัปเดตเวอร์ชันต่ำสุดที่ด้านบนของ ios/Podfile ดังนี้

ios/Podfile

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

กำหนดการตั้งค่า Android

สำหรับ Android ให้อัปเดต android/app/build.gradle.kts ดังนี้

android/app/build.gradle.kts

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

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

สร้างผู้ให้บริการโมเดล Gemini

ตอนนี้คุณจะต้องสร้างผู้ให้บริการ Riverpod สำหรับ Firebase และ Gemini สร้างไฟล์ใหม่ lib/providers/gemini.dart

lib/providers/gemini.dart

import 'dart:async';

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

import '../firebase_options.dart';

part 'gemini.g.dart';

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

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

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

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

ไฟล์นี้จะกำหนดพื้นฐานสำหรับผู้ให้บริการหลัก 3 ราย ผู้ให้บริการเหล่านี้สร้างขึ้นเมื่อคุณเรียกใช้ dart run build_runner โดยเครื่องมือสร้างโค้ด Riverpod

  1. firebaseAppProvider: เริ่มต้น Firebase ด้วยการกำหนดค่าโปรเจ็กต์
  2. geminiModelProvider: สร้างอินสแตนซ์โมเดล Generative ของ Gemini
  3. chatSessionProvider: สร้างและดูแลเซสชันการแชทกับโมเดล Gemini

คําอธิบายประกอบ keepAlive: true ในเซสชันการแชทจะช่วยให้คําอธิบายประกอบคงอยู่ตลอดอายุการใช้งานของแอป ซึ่งจะรักษาบริบทการสนทนาไว้

ใช้บริการแชท Gemini

สร้างไฟล์ lib/services/gemini_chat_service.dart ใหม่เพื่อใช้งานบริการแชท โดยทำดังนี้

lib/services/gemini_chat_service.dart

import 'dart:async';

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

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

part 'gemini_chat_service.g.dart';

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

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

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

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

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

บริการนี้

  1. รับข้อความของผู้ใช้และส่งไปยัง Gemini API
  2. อัปเดตอินเทอร์เฟซแชทด้วยคำตอบจากโมเดล
  3. บันทึกการสื่อสารทั้งหมดเพื่อให้เข้าใจขั้นตอนการดำเนินการ LLM จริงได้ง่ายขึ้น
  4. จัดการข้อผิดพลาดด้วยความคิดเห็นของผู้ใช้ที่เหมาะสม

หมายเหตุ: หน้าต่างบันทึกจะมีลักษณะเกือบเหมือนกับหน้าต่างแชท ณ จุดนี้ บันทึกจะน่าสนใจยิ่งขึ้นเมื่อคุณเริ่มการเรียกฟังก์ชันและเริ่มสตรีมการตอบกลับ

สร้างโค้ด Riverpod

เรียกใช้คำสั่งเครื่องมือสร้างเพื่อสร้างโค้ด Riverpod ที่จำเป็น

dart run build_runner build --delete-conflicting-outputs

ซึ่งจะสร้างไฟล์ .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),
      ),
    );
  }
}

การเปลี่ยนแปลงที่สําคัญในการอัปเดตนี้ ได้แก่

  1. การเปลี่ยนบริการ Echo ด้วยบริการแชทที่อิงตาม Gemini API
  2. การเพิ่มหน้าจอการโหลดและข้อผิดพลาดโดยใช้รูปแบบ AsyncValue ของ Riverpod ด้วยเมธอด when
  3. การเชื่อมต่อ UI กับบริการแชทใหม่ผ่าน sendMessage callback

เรียกใช้แอป

เรียกใช้แอปด้วยคำสั่งต่อไปนี้

flutter run -d DEVICE

แทนที่ DEVICE ด้วยอุปกรณ์เป้าหมาย เช่น macos, windows, chrome หรือรหัสอุปกรณ์

ภาพหน้าจอแอป Colorist ที่แสดง LLM ของ Gemini ตอบสนองต่อคําขอสีเหลืองสด

ตอนนี้เมื่อคุณพิมพ์ข้อความ ระบบจะส่งข้อความนั้นไปยัง Gemini API และคุณจะได้รับคำตอบจาก LLM แทนการสะท้อน แผงบันทึกจะแสดงการโต้ตอบกับ API

ทำความเข้าใจการสื่อสาร LLM

เรามาทําความเข้าใจสิ่งที่เกิดขึ้นเมื่อคุณสื่อสารกับ Gemini API กัน

ขั้นตอนการสื่อสาร

  1. อินพุตของผู้ใช้: ผู้ใช้ป้อนข้อความในอินเทอร์เฟซการแชท
  2. การจัดรูปแบบคำขอ: แอปจัดรูปแบบข้อความเป็นออบเจ็กต์ Content สำหรับ Gemini API
  3. การสื่อสารผ่าน API: ระบบจะส่งข้อความไปยัง Gemini API ผ่าน Vertex AI ใน Firebase
  4. การประมวลผล LLM: โมเดล Gemini จะประมวลผลข้อความและสร้างคำตอบ
  5. การจัดการการตอบกลับ: แอปได้รับการตอบกลับและอัปเดต UI
  6. การบันทึก: ระบบจะบันทึกการสื่อสารทั้งหมดเพื่อความโปร่งใส

เซสชันการแชทและบริบทการสนทนา

เซสชันการแชทของ Gemini จะรักษาบริบทระหว่างข้อความไว้เพื่อให้มีการโต้ตอบแบบการสนทนา ซึ่งหมายความว่า LLM จะ "จดจํา" การแลกเปลี่ยนข้อมูลก่อนหน้าในเซสชันปัจจุบัน ซึ่งช่วยให้การสนทนามีความสอดคล้องกันมากขึ้น

คําอธิบายประกอบ keepAlive: true ในผู้ให้บริการเซสชันการแชทช่วยให้บริบทนี้คงอยู่ตลอดอายุการใช้งานของแอป บริบทถาวรนี้มีความสำคัญต่อการรักษาลำดับการสนทนาที่เป็นธรรมชาติกับ LLM

ขั้นตอนถัดไปคือ

เมื่อถึงจุดนี้ คุณสามารถถาม Gemini API ได้ทุกเรื่อง เนื่องจากไม่มีข้อจำกัดว่า API จะตอบกลับอะไรบ้าง เช่น คุณสามารถขอสรุปเกี่ยวกับสงครามกุหลาบ ซึ่งไม่เกี่ยวข้องกับวัตถุประสงค์ของแอปสี

ในขั้นตอนถัดไป คุณจะต้องสร้างพรอมต์ของระบบเพื่อแนะนํา Gemini ในการตีความคําอธิบายสีให้มีประสิทธิภาพมากขึ้น บทนี้จะสาธิตวิธีปรับแต่งลักษณะการทํางานของ LLM เพื่อตอบสนองความต้องการเฉพาะของแอปพลิเคชันและมุ่งเน้นความสามารถของ LLM ไปยังโดเมนของแอป

การแก้ปัญหา

ปัญหาเกี่ยวกับการกําหนดค่า Firebase

หากพบข้อผิดพลาดในการเริ่มต้นใช้งาน Firebase ให้ทำดังนี้

  • ตรวจสอบว่าไฟล์ firebase_options.dart สร้างขึ้นอย่างถูกต้อง
  • ยืนยันว่าคุณได้อัปเกรดเป็นแพ็กเกจ Blaze เพื่อเข้าถึง Vertex AI แล้ว

ข้อผิดพลาดในการเข้าถึง API

หากคุณได้รับข้อผิดพลาดในการเข้าถึง Gemini API ให้ทำดังนี้

  • ยืนยันว่าการตั้งค่าการเรียกเก็บเงินในโปรเจ็กต์ Firebase ถูกต้อง
  • ตรวจสอบว่าเปิดใช้ Vertex AI และ Cloud AI API ในโปรเจ็กต์ Firebase แล้ว
  • ตรวจสอบการเชื่อมต่อเครือข่ายและการตั้งค่าไฟร์วอลล์
  • ตรวจสอบว่าชื่อรุ่น (gemini-2.0-flash) ถูกต้องและพร้อมใช้งาน

ปัญหาเกี่ยวกับบริบทการสนทนา

หากพบว่า Gemini ไม่ได้จดจำบริบทก่อนหน้าจากแชท ให้ทำดังนี้

  • ยืนยันว่าฟังก์ชัน chatSession มีคำอธิบายประกอบด้วย @Riverpod(keepAlive: true)
  • ตรวจสอบว่าคุณกำลังใช้เซสชันการแชทเดียวกันสำหรับการแลกเปลี่ยนข้อความทั้งหมด
  • ตรวจสอบว่าเซสชันการแชทเริ่มต้นอย่างถูกต้องก่อนส่งข้อความ

ปัญหาเฉพาะแพลตฟอร์ม

สำหรับปัญหาเฉพาะแพลตฟอร์ม

  • iOS/macOS: ตรวจสอบว่าได้ตั้งค่าการให้สิทธิ์ที่เหมาะสมและกำหนดค่าเวอร์ชันขั้นต่ำแล้ว
  • Android: ยืนยันว่าตั้งค่าเวอร์ชัน SDK ขั้นต่ำอย่างถูกต้อง
  • ตรวจสอบข้อความแสดงข้อผิดพลาดเฉพาะแพลตฟอร์มในคอนโซล

แนวคิดสําคัญที่เรียนรู้

  • การตั้งค่า Firebase ในแอปพลิเคชัน Flutter
  • การกำหนดค่า Vertex AI ใน Firebase เพื่อเข้าถึง Gemini
  • การสร้างผู้ให้บริการ Riverpod สําหรับบริการแบบไม่พร้อมกัน
  • การใช้บริการแชทที่สื่อสารกับ LLM
  • การจัดการสถานะ API แบบไม่ซิงค์ (การโหลด ข้อผิดพลาด ข้อมูล)
  • ทำความเข้าใจขั้นตอนการติดต่อสื่อสารของ LLM และเซสชันการแชท

4. พรอมต์ที่มีประสิทธิภาพสำหรับคำอธิบายสี

ในขั้นตอนนี้ คุณจะต้องสร้างและใช้พรอมต์ของระบบที่แนะนํา Gemini ในการตีความคําอธิบายสี พรอมต์ของระบบเป็นวิธีที่มีประสิทธิภาพในการปรับแต่งลักษณะการทํางานของ LLM สําหรับงานหนึ่งๆ โดยไม่ต้องเปลี่ยนโค้ด

สิ่งที่คุณจะได้เรียนรู้ในขั้นตอนนี้

  • ทำความเข้าใจข้อความแจ้งของระบบและความสำคัญในการใช้งาน LLM
  • การสร้างพรอมต์ที่มีประสิทธิภาพสำหรับงานเฉพาะโดเมน
  • การโหลดและใช้ข้อความแจ้งของระบบในแอป Flutter
  • แนะนำ LLM ให้แสดงคำตอบที่มีรูปแบบสอดคล้องกัน
  • การทดสอบว่าข้อความแจ้งของระบบส่งผลต่อลักษณะการทํางานของ LLM อย่างไร

ทำความเข้าใจพรอมต์ของระบบ

ก่อนจะลงรายละเอียดการใช้งาน เรามาทําความเข้าใจว่าข้อความแจ้งของระบบคืออะไรและทําไมจึงสําคัญ

ข้อความแจ้งของระบบคืออะไร

พรอมต์ของระบบคือคำสั่งประเภทพิเศษที่ส่งไปยัง LLM ซึ่งจะกำหนดบริบท หลักเกณฑ์ด้านลักษณะการทำงาน และสิ่งที่คาดหวังสำหรับคำตอบ ข้อความแจ้งของระบบจะมีลักษณะดังนี้ ซึ่งแตกต่างจากข้อความของผู้ใช้

  • กำหนดบทบาทและบุคลิกของ LLM
  • กำหนดความรู้หรือความสามารถเฉพาะทาง
  • ระบุวิธีการจัดรูปแบบ
  • กำหนดข้อจำกัดในการตอบกลับ
  • อธิบายวิธีจัดการสถานการณ์ต่างๆ

ให้คิดว่าพรอมต์ของระบบคือการให้ "คำอธิบายลักษณะงาน" แก่ LLM ซึ่งจะบอกให้โมเดลรู้ว่าควรทำอย่างไรตลอดการสนทนา

เหตุผลที่ข้อความแจ้งของระบบมีความสำคัญ

พรอมต์ของระบบมีความสําคัญอย่างยิ่งต่อการสร้างการโต้ตอบ LLM ที่เป็นประโยชน์และสอดคล้องกัน เนื่องจากพรอมต์ของระบบจะทําสิ่งต่อไปนี้

  1. ตรวจสอบความสอดคล้อง: แนะนําให้นายแบบ/นางแบบตอบคำถามในรูปแบบที่สอดคล้องกัน
  2. เพิ่มความเกี่ยวข้อง: มุ่งเน้นโมเดลไปยังโดเมนที่เฉพาะเจาะจง (ในกรณีนี้คือสี)
  3. กําหนดขอบเขต: กําหนดสิ่งที่โมเดลควรและไม่ควรทํา
  4. ปรับปรุงประสบการณ์ของผู้ใช้: สร้างรูปแบบการโต้ตอบที่เป็นประโยชน์และเป็นธรรมชาติมากขึ้น
  5. ลดการประมวลผลหลังการขาย: รับคำตอบในรูปแบบที่แยกวิเคราะห์หรือแสดงผลได้ง่ายขึ้น

สําหรับแอป Colorist คุณต้องใช้ LLM เพื่อตีความคําอธิบายสีอย่างสอดคล้องกันและระบุค่า RGB ในรูปแบบที่เฉพาะเจาะจง

สร้างชิ้นงานพรอมต์ของระบบ

ก่อนอื่น คุณจะต้องสร้างไฟล์พรอมต์ของระบบที่จะโหลดเมื่อรันไทม์ วิธีนี้ช่วยให้คุณแก้ไขพรอมต์ได้โดยไม่ต้องคอมไพล์แอปอีกครั้ง

สร้างไฟล์ assets/system_prompt.md ใหม่ที่มีเนื้อหาต่อไปนี้

assets/system_prompt.md

# Colorist System Prompt

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

## Your Capabilities

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

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

## How to Respond to User Inputs

When users describe a color:

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

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

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

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

## When Descriptions are Unclear

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

## Important Guidelines

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

ทำความเข้าใจโครงสร้างข้อความแจ้งของระบบ

มาดูรายละเอียดว่าพรอมต์นี้ทําอะไรบ้าง

  1. คำจำกัดความของบทบาท: กำหนด LLM เป็น "ผู้ช่วยผู้เชี่ยวชาญด้านสี"
  2. คำอธิบายงาน: กำหนดงานหลักเป็นการตีความคำอธิบายสีเป็นค่า RGB
  3. รูปแบบการตอบกลับ: ระบุวิธีจัดรูปแบบค่า RGB ให้สอดคล้องกัน
  4. ตัวอย่างการแลกเปลี่ยน: แสดงตัวอย่างที่ชัดเจนของรูปแบบการโต้ตอบที่คาดไว้
  5. การจัดการกรณีสุดโต่ง: อธิบายวิธีจัดการคำอธิบายที่ไม่ชัดเจน
  6. ข้อจำกัดและหลักเกณฑ์: กําหนดขอบเขต เช่น รักษาค่า RGB ให้อยู่ระหว่าง 0.0 ถึง 1.0

แนวทางที่มีโครงสร้างนี้ช่วยให้มั่นใจได้ว่าคำตอบของ LLM จะสอดคล้องกัน มีข้อมูล และจัดรูปแบบในลักษณะที่แยกวิเคราะห์ได้ง่ายหากคุณต้องการดึงค่า RGB โดยใช้โปรแกรม

อัปเดต pubspec.yaml

ตอนนี้ให้อัปเดตด้านล่างของ pubspec.yaml ให้รวมไดเรกทอรีชิ้นงาน

pubspec.yaml

flutter:
  uses-material-design: true

  assets:
    - assets/

เรียกใช้ flutter pub get เพื่อรีเฟรชกลุ่มชิ้นงาน

สร้างผู้ให้บริการพรอมต์ของระบบ

สร้างไฟล์ lib/providers/system_prompt.dart ใหม่เพื่อโหลดข้อความแจ้งของระบบ

lib/providers/system_prompt.dart

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

part 'system_prompt.g.dart';

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

ผู้ให้บริการรายนี้ใช้ระบบการโหลดชิ้นงานของ Flutter เพื่ออ่านไฟล์พรอมต์ที่รันไทม์

อัปเดตผู้ให้บริการโมเดล Gemini

ตอนนี้ให้แก้ไขไฟล์ lib/providers/gemini.dart ให้รวมข้อความแจ้งของระบบ

lib/providers/gemini.dart

import 'dart:async';

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

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

part 'gemini.g.dart';

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

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

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

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

การเปลี่ยนแปลงที่สําคัญคือการเพิ่ม systemInstruction: Content.system(systemPrompt) เมื่อสร้างโมเดล Generative ซึ่งจะบอกให้ Gemini ใช้คำสั่งของคุณเป็นพรอมต์ของระบบสำหรับการโต้ตอบทั้งหมดในเซสชันการแชทนี้

สร้างโค้ด Riverpod

เรียกใช้คำสั่งเครื่องมือสร้างเพื่อสร้างโค้ด Riverpod ที่จำเป็น

dart run build_runner build --delete-conflicting-outputs

เรียกใช้และทดสอบแอปพลิเคชัน

จากนั้นเรียกใช้แอปพลิเคชันโดยทำดังนี้

flutter run -d DEVICE

ภาพหน้าจอแอป Colorist ที่แสดง LLM ของ Gemini ตอบกลับด้วยคำตอบในลักษณะของแอปการเลือกสี

ลองทดสอบด้วยคำอธิบายสีต่างๆ ดังนี้

  • "ฉันต้องการสีฟ้า"
  • "ขอสีเขียวเข้ม"
  • "ทำสีส้มพระอาทิตย์ตกที่สดใส"
  • "ฉันต้องการสีลาเวนเดอร์สด"
  • "ขอดูสีประมาณสีน้ำเงินเข้มของทะเล"

คุณควรสังเกตว่าตอนนี้ Gemini ตอบกลับด้วยคำอธิบายแบบสนทนาเกี่ยวกับสีพร้อมกับค่า RGB ที่รูปแบบสอดคล้องกัน พรอมต์ของระบบได้แนะนํา LLM ให้แสดงคำตอบประเภทที่คุณต้องการอย่างมีประสิทธิภาพ

นอกจากนี้ ให้ลองค้นหาเนื้อหานอกบริบทของสี เช่น สาเหตุหลักของสงครามกุหลาบ คุณควรเห็นความแตกต่างจากขั้นตอนก่อนหน้า

ความสำคัญของวิศวกรรมที่รวดเร็วสำหรับงานเฉพาะทาง

พรอมต์ของระบบเป็นทั้งศาสตร์และศิลป์ พรอมต์เป็นส่วนสําคัญของการผสานรวม LLM ซึ่งอาจส่งผลอย่างมากต่อประโยชน์ของโมเดลสําหรับแอปพลิเคชันหนึ่งๆ สิ่งที่คุณทําในขั้นตอนนี้คือรูปแบบหนึ่งของวิศวกรรมพรอมต์ ซึ่งก็คือการปรับแต่งวิธีการเพื่อให้โมเดลทํางานในลักษณะที่เหมาะกับความต้องการของแอปพลิเคชัน

วิศวกรรมพรอมต์ที่มีประสิทธิภาพประกอบด้วยสิ่งต่อไปนี้

  1. คำจำกัดความบทบาทที่ชัดเจน: การกำหนดวัตถุประสงค์ของ LLM
  2. วิธีการที่ชัดเจน: ระบุรายละเอียดว่า LLM ควรตอบสนองอย่างไร
  3. ตัวอย่างที่ชัดเจน: แสดงแทนที่จะบอกเพียงว่าคำตอบที่ดีมีลักษณะเป็นอย่างไร
  4. การจัดการกรณีพิเศษ: บอกวิธีการจัดการกับสถานการณ์ที่คลุมเครือให้ LLM ทราบ
  5. ข้อกำหนดการจัดรูปแบบ: ตรวจสอบว่าคำตอบมีโครงสร้างที่สอดคล้องกันและใช้งานได้

พรอมต์ของระบบที่คุณสร้างขึ้นจะเปลี่ยนความสามารถทั่วไปของ Gemini ให้เป็นผู้ช่วยด้านการตีความสีเฉพาะทางซึ่งให้คำตอบที่มีการจัดรูปแบบมาเพื่อตอบสนองความต้องการของแอปพลิเคชันโดยเฉพาะ รูปแบบนี้เป็นรูปแบบที่มีประสิทธิภาพซึ่งคุณนำไปใช้กับโดเมนและงานต่างๆ ได้มากมาย

ขั้นตอนถัดไปคือ

ในขั้นตอนถัดไป คุณจะต้องสร้างรากฐานนี้ด้วยการเพิ่มประกาศฟังก์ชัน ซึ่งจะช่วยให้ LLM ไม่เพียงแนะนำค่า RGB เท่านั้น แต่ยังเรียกใช้ฟังก์ชันในแอปเพื่อตั้งค่าสีโดยตรง ซึ่งแสดงให้เห็นว่า LLM สามารถเชื่อมโยงช่องว่างระหว่างภาษาธรรมชาติกับฟีเจอร์แอปพลิเคชันจริงได้อย่างไร

การแก้ปัญหา

ปัญหาการโหลดชิ้นงาน

หากพบข้อผิดพลาดในการโหลดข้อความแจ้งของระบบ ให้ทำดังนี้

  • ตรวจสอบว่า pubspec.yaml แสดงไดเรกทอรีชิ้นงานอย่างถูกต้อง
  • ตรวจสอบว่าเส้นทางใน rootBundle.loadString() ตรงกับตำแหน่งไฟล์
  • เรียกใช้ flutter clean ตามด้วย flutter pub get เพื่อรีเฟรชกลุ่มชิ้นงาน

คำตอบไม่สอดคล้องกัน

หาก LLM ไม่ทำตามวิธีการจัดรูปแบบของคุณอย่างสม่ำเสมอ ให้ทำดังนี้

  • ลองระบุข้อกำหนดด้านรูปแบบให้ชัดเจนขึ้นในพรอมต์ของระบบ
  • เพิ่มตัวอย่างเพิ่มเติมเพื่อแสดงรูปแบบที่คาดไว้
  • ตรวจสอบว่ารูปแบบที่คุณขอนั้นเหมาะสมกับโมเดล

การจำกัดอัตรา API

หากพบข้อผิดพลาดที่เกี่ยวข้องกับการจำกัดอัตราการส่งข้อมูล ให้ทำดังนี้

  • โปรดทราบว่าบริการ Vertex AI มีขีดจํากัดการใช้งาน
  • ลองใช้ตรรกะการดำเนินการซ้ำด้วย Exponential Backoff
  • ตรวจสอบปัญหาเกี่ยวกับโควต้าในคอนโซล Firebase

แนวคิดสําคัญที่เรียนรู้

  • ทำความเข้าใจบทบาทและความสำคัญของข้อความแจ้งของระบบในแอปพลิเคชัน LLM
  • การสร้างพรอมต์ที่มีประสิทธิภาพด้วยวิธีการ ตัวอย่าง และสิ่งจํากัดที่ชัดเจน
  • การโหลดและใช้ข้อความแจ้งของระบบในแอปพลิเคชัน Flutter
  • การกำหนดลักษณะการทํางานของ LLM สําหรับงานเฉพาะโดเมน
  • การใช้การออกแบบพรอมต์เพื่อกำหนดคำตอบของ LLM

ขั้นตอนนี้แสดงวิธีปรับแต่งลักษณะการทํางานของ LLM อย่างมีนัยสําคัญโดยไม่ต้องเปลี่ยนโค้ด เพียงระบุวิธีการที่ชัดเจนในพรอมต์ของระบบ

5. การประกาศฟังก์ชันสําหรับเครื่องมือ LLM

ในขั้นตอนนี้ คุณจะเริ่มทํางานเพื่อเปิดใช้ Gemini ให้ดําเนินการในแอปด้วยการใช้คําประกาศฟังก์ชัน ฟีเจอร์ที่มีประสิทธิภาพนี้ช่วยให้ LLM ไม่เพียงแนะนำค่า RGB เท่านั้น แต่ยังตั้งค่าค่าเหล่านั้นใน UI ของแอปผ่านคําเรียกเครื่องมือเฉพาะได้ อย่างไรก็ตาม คุณจะต้องดำเนินการในขั้นตอนถัดไปเพื่อดูคําขอ LLM ที่ดำเนินการในแอป Flutter

สิ่งที่คุณจะได้เรียนรู้ในขั้นตอนนี้

  • ทําความเข้าใจการเรียกฟังก์ชัน LLM และประโยชน์สําหรับแอปพลิเคชัน Flutter
  • การกําหนดประกาศฟังก์ชันตามสคีมาสําหรับ Gemini
  • การผสานรวมการประกาศฟังก์ชันกับโมเดล Gemini
  • การอัปเดตข้อความแจ้งของระบบเพื่อใช้ประโยชน์จากความสามารถของเครื่องมือ

ทําความเข้าใจการเรียกใช้ฟังก์ชัน

ก่อนใช้การประกาศฟังก์ชัน เรามาทําความเข้าใจว่าฟังก์ชันคืออะไรและทําไมจึงมีประโยชน์

การเรียกใช้ฟังก์ชันคืออะไร

การเรียกใช้ฟังก์ชัน (บางครั้งเรียกว่า "การใช้เครื่องมือ") เป็นความสามารถที่ช่วยให้ LLM ทําสิ่งต่อไปนี้ได้

  1. จดจำเมื่อคำขอของผู้ใช้จะได้รับประโยชน์จากการเรียกใช้ฟังก์ชันที่เฉพาะเจาะจง
  2. สร้างออบเจ็กต์ JSON ที่มีโครงสร้างพร้อมพารามิเตอร์ที่จําเป็นสําหรับฟังก์ชันนั้น
  3. อนุญาตให้แอปพลิเคชันเรียกใช้ฟังก์ชันด้วยพารามิเตอร์เหล่านั้น
  4. รับผลลัพธ์ของฟังก์ชันและรวมไว้ในคำตอบ

การเรียกใช้ฟังก์ชันช่วยให้ LLM ทริกเกอร์การดำเนินการที่แน่ชัดในแอปพลิเคชันได้ ไม่ใช่แค่อธิบายสิ่งที่ต้องทำ

เหตุใดการเรียกใช้ฟังก์ชันจึงสำคัญต่อแอป Flutter

การเรียกใช้ฟังก์ชันเป็นสะพานเชื่อมที่มีประสิทธิภาพระหว่างภาษาธรรมชาติกับฟีเจอร์แอปพลิเคชัน

  1. การดำเนินการโดยตรง: ผู้ใช้อธิบายสิ่งที่ต้องการเป็นภาษาพูดง่ายๆ ได้ แล้วแอปจะตอบสนองด้วยการดำเนินการที่ชัดเจน
  2. เอาต์พุตที่มีโครงสร้าง: LLM จะสร้าง Structured Data ที่สะอาด ไม่ใช่ข้อความที่ต้องแยกวิเคราะห์
  3. การดำเนินการที่ซับซ้อน: ช่วยให้ LLM สามารถเข้าถึงข้อมูลภายนอก ทำการคํานวณ หรือแก้ไขสถานะแอปพลิเคชันได้
  4. ประสบการณ์ของผู้ใช้ที่ดีขึ้น: สร้างการผสานรวมที่ราบรื่นระหว่างการสนทนากับฟังก์ชันการทำงาน

ในแอป Colorist การเรียกใช้ฟังก์ชันช่วยให้ผู้ใช้พูดว่า "ฉันต้องการสีเขียวเข้ม" แล้ว UI จะอัปเดตเป็นสีนั้นทันทีโดยไม่ต้องแยกวิเคราะห์ค่า RGB จากข้อความ

กำหนดประกาศฟังก์ชัน

สร้างไฟล์ lib/services/gemini_tools.dart ใหม่เพื่อกำหนดประกาศฟังก์ชัน

lib/services/gemini_tools.dart

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

part 'gemini_tools.g.dart';

class GeminiTools {
  GeminiTools(this.ref);

  final Ref ref;

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

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

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

ทำความเข้าใจการประกาศฟังก์ชัน

มาดูกันว่าโค้ดนี้ทําอะไรบ้าง

  1. การตั้งชื่อฟังก์ชัน: คุณตั้งชื่อฟังก์ชัน set_color เพื่อระบุวัตถุประสงค์อย่างชัดเจน
  2. คําอธิบายฟังก์ชัน: คุณระบุคําอธิบายที่ชัดเจนซึ่งช่วยให้ LLM เข้าใจว่าควรใช้ฟังก์ชันเมื่อใด
  3. คําจํากัดความพารามิเตอร์: คุณกําหนดพารามิเตอร์ที่มีโครงสร้างพร้อมคําอธิบายของตนเองได้
    • red: ส่วนประกอบสีแดงของ RGB ซึ่งระบุเป็นตัวเลขระหว่าง 0.0 ถึง 1.0
    • green: ส่วนประกอบสีเขียวของ RGB ซึ่งระบุเป็นตัวเลขระหว่าง 0.0 ถึง 1.0
    • blue: ส่วนประกอบสีน้ำเงินของ RGB ซึ่งระบุเป็นตัวเลขระหว่าง 0.0 ถึง 1.0
  4. ประเภทสคีมา: คุณใช้ Schema.number() เพื่อระบุว่าค่าเหล่านี้เป็นค่าตัวเลข
  5. คอลเล็กชันเครื่องมือ: คุณสร้างรายการเครื่องมือที่มีประกาศฟังก์ชัน

แนวทางที่มีโครงสร้างนี้จะช่วยให้ LLM ของ Gemini เข้าใจข้อมูลต่อไปนี้

  • กรณีที่ควรเรียกใช้ฟังก์ชันนี้
  • พารามิเตอร์ที่ต้องระบุ
  • ข้อจำกัดที่มีผลกับพารามิเตอร์เหล่านั้น (เช่น ช่วงค่า)

อัปเดตผู้ให้บริการโมเดล Gemini

ตอนนี้ให้แก้ไขไฟล์ lib/providers/gemini.dart เพื่อรวมประกาศฟังก์ชันเมื่อเริ่มต้นโมเดล Gemini โดยทำดังนี้

lib/providers/gemini.dart

import 'dart:async';

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

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

part 'gemini.g.dart';

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

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

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

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

การเปลี่ยนแปลงที่สําคัญคือการเพิ่มพารามิเตอร์ tools: geminiTools.tools เมื่อสร้างโมเดล 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

การเปลี่ยนแปลงที่สำคัญในพรอมต์ของระบบมีดังนี้

  1. การแนะนำเครื่องมือ: ตอนนี้คุณบอก LLM เกี่ยวกับเครื่องมือ set_color แทนที่จะขอค่า RGB ที่จัดรูปแบบแล้ว
  2. กระบวนการที่แก้ไขแล้ว: คุณเปลี่ยนขั้นตอนที่ 3 จาก "จัดรูปแบบค่าในการตอบกลับ" เป็น "ใช้เครื่องมือเพื่อตั้งค่า"
  3. ตัวอย่างที่อัปเดต: คุณแสดงวิธีที่คำตอบควรมีการเรียกใช้เครื่องมือแทนข้อความที่มีการจัดรูปแบบ
  4. นำข้อกำหนดการจัดรูปแบบออก: เนื่องจากคุณใช้การเรียกฟังก์ชันที่มีโครงสร้าง คุณจึงไม่จำเป็นต้องใช้รูปแบบข้อความที่เฉพาะเจาะจงอีกต่อไป

พรอมต์ที่อัปเดตนี้จะกำหนดให้ LLM ใช้การเรียกฟังก์ชันแทนการให้ค่า RGB ในรูปแบบข้อความ

สร้างโค้ด Riverpod

เรียกใช้คำสั่งเครื่องมือสร้างเพื่อสร้างโค้ด Riverpod ที่จำเป็น

dart run build_runner build --delete-conflicting-outputs

เรียกใช้แอปพลิเคชัน

เมื่อถึงขั้นตอนนี้ Gemini จะสร้างเนื้อหาที่พยายามใช้การเรียกฟังก์ชัน แต่คุณยังไม่ได้ติดตั้งใช้งานตัวแฮนเดิลสําหรับการเรียกฟังก์ชัน เมื่อเรียกใช้แอปและอธิบายสี คุณจะเห็น Gemini ตอบสนองราวกับว่าได้เรียกใช้เครื่องมือ แต่คุณจะไม่เห็นการเปลี่ยนแปลงสีใน UI จนกว่าจะถึงขั้นตอนถัดไป

วิธีเรียกใช้แอป

flutter run -d DEVICE

ภาพหน้าจอแอป Colorist ที่แสดง LLM ของ Gemini ตอบกลับด้วยคำตอบบางส่วน

ลองอธิบายสี เช่น "น้ำเงินเข้ม" หรือ "เขียวเข้ม" แล้วสังเกตการตอบสนอง LLM พยายามเรียกฟังก์ชันที่กําหนดไว้ข้างต้น แต่โค้ดของคุณยังไม่ตรวจพบการเรียกฟังก์ชัน

กระบวนการเรียกใช้ฟังก์ชัน

มาทําความเข้าใจสิ่งที่จะเกิดขึ้นเมื่อ Gemini ใช้การเรียกฟังก์ชันกัน

  1. การเลือกฟังก์ชัน: LLM จะตัดสินใจว่าการเรียกใช้ฟังก์ชันจะมีประโยชน์หรือไม่ตามคําขอของผู้ใช้
  2. การสร้างพารามิเตอร์: LLM จะสร้างค่าพารามิเตอร์ที่เหมาะกับสคีมาของฟังก์ชัน
  3. รูปแบบการเรียกใช้ฟังก์ชัน: LLM จะส่งออบเจ็กต์การเรียกใช้ฟังก์ชันที่มีโครงสร้างในการตอบกลับ
  4. การจัดการแอปพลิเคชัน: แอปของคุณจะได้รับการเรียกใช้นี้และดำเนินการฟังก์ชันที่เกี่ยวข้อง (ติดตั้งใช้งานในขั้นตอนถัดไป)
  5. การผสานรวมคำตอบ: ในการสนทนาแบบหลายรอบ LLM จะคาดหวังว่าจะได้รับผลลัพธ์ของฟังก์ชัน

ในแอปปัจจุบัน ขั้นตอนแรก 3 ขั้นตอนกำลังเกิดขึ้น แต่คุณยังไม่ได้ติดตั้งใช้งานขั้นตอนที่ 4 หรือ 5 (การจัดการการเรียกใช้ฟังก์ชัน) ซึ่งจะทำในขั้นตอนถัดไป

รายละเอียดทางเทคนิค: วิธีที่ Gemini ตัดสินใจว่าจะใช้ฟังก์ชันเมื่อใด

Gemini จะตัดสินใจอย่างชาญฉลาดเกี่ยวกับเวลาในการใช้ฟังก์ชันโดยพิจารณาจากปัจจัยต่อไปนี้

  1. ความตั้งใจของผู้ใช้: ฟังก์ชันจะแสดงคำขอของผู้ใช้ให้ดีที่สุดหรือไม่
  2. ความเกี่ยวข้องของฟังก์ชัน: ฟังก์ชันที่ใช้ได้ตรงกับงานมากน้อยเพียงใด
  3. ความพร้อมใช้งานของพารามิเตอร์: ระบุค่าพารามิเตอร์ได้อย่างมั่นใจหรือไม่
  4. วิธีการของระบบ: คําแนะนําจากพรอมต์ของระบบเกี่ยวกับการใช้ฟังก์ชัน

การให้ประกาศฟังก์ชันและคำสั่งของระบบที่ชัดเจนเป็นการตั้งค่าให้ 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
  • การบันทึกการเรียกฟังก์ชันและการตอบกลับเพื่อความโปร่งใส

ทําความเข้าใจไปป์ไลน์การเรียกฟังก์ชัน

ก่อนจะไปลงรายละเอียดการใช้งาน เรามาทําความเข้าใจไปป์ไลน์การเรียกฟังก์ชันโดยสมบูรณ์กัน

ขั้นตอนตั้งแต่ต้นจบ

  1. อินพุตของผู้ใช้: ผู้ใช้อธิบายสีเป็นภาษาที่เป็นธรรมชาติ (เช่น "forest green")
  2. การประมวลผล LLM: Gemini วิเคราะห์คำอธิบายและตัดสินใจเรียกใช้ฟังก์ชัน set_color
  3. การสร้างการเรียกฟังก์ชัน: Gemini จะสร้าง JSON ที่มีโครงสร้างพร้อมพารามิเตอร์ (ค่าสีแดง เขียว น้ำเงิน)
  4. การรับการเรียกฟังก์ชัน: แอปของคุณได้รับ Structured Data นี้จาก Gemini
  5. การดำเนินการของฟังก์ชัน: แอปของคุณดำเนินการฟังก์ชันด้วยพารามิเตอร์ที่ระบุ
  6. การอัปเดตสถานะ: ฟังก์ชันจะอัปเดตสถานะของแอป (เปลี่ยนสีที่แสดง)
  7. การสร้างคำตอบ: ฟังก์ชันจะแสดงผลลัพธ์กลับไปยัง LLM
  8. การรวมคำตอบ: LLM จะรวมผลลัพธ์เหล่านี้ไว้ในคำตอบสุดท้าย
  9. การอัปเดต UI: UI ตอบสนองต่อการเปลี่ยนแปลงสถานะโดยแสดงสีใหม่

วงจรการสื่อสารที่สมบูรณ์เป็นสิ่งจําเป็นสําหรับการผสานรวม LLM อย่างเหมาะสม เมื่อ LLM เรียกใช้ฟังก์ชัน LLM จะไม่เพียงแค่ส่งคําขอแล้วดำเนินการต่อ แต่ระบบจะรอให้แอปพลิเคชันเรียกใช้ฟังก์ชันและแสดงผลลัพธ์แทน จากนั้น LLM จะใช้ผลลัพธ์เหล่านี้เพื่อกำหนดคำตอบสุดท้าย ซึ่งจะสร้างลำดับการสนทนาที่เป็นธรรมชาติที่รับทราบการดำเนินการ

ใช้ตัวแฮนเดิลฟังก์ชัน

มาอัปเดตไฟล์ lib/services/gemini_tools.dart เพื่อเพิ่มตัวแฮนเดิลสำหรับการเรียกใช้ฟังก์ชันกัน

lib/services/gemini_tools.dart

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

part 'gemini_tools.g.dart';

class GeminiTools {
  GeminiTools(this.ref);

  final Ref ref;

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

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

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

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

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

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

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

ทำความเข้าใจตัวแฮนเดิลฟังก์ชัน

มาดูรายละเอียดว่าตัวแฮนเดิลฟังก์ชันเหล่านี้ทําอะไรบ้าง

  1. handleFunctionCall: โปรแกรมกระจายงานส่วนกลางที่มีลักษณะดังนี้
    • บันทึกการเรียกฟังก์ชันเพื่อความโปร่งใสในแผงบันทึก
    • กำหนดเส้นทางไปยังตัวแฮนเดิลที่เหมาะสมตามชื่อฟังก์ชัน
    • แสดงผลลัพธ์ที่มีโครงสร้างซึ่งจะส่งกลับไปยัง LLM
  2. handleSetColor: แฮนเดิลที่เฉพาะเจาะจงสำหรับฟังก์ชัน set_color ของคุณที่มีลักษณะดังนี้
    • ดึงค่า RGB ออกจากแผนที่อาร์กิวเมนต์
    • แปลงเป็นประเภทที่คาดหวัง (Double)
    • อัปเดตสถานะสีของแอปพลิเคชันโดยใช้ colorStateNotifier
    • สร้างคำตอบที่มีโครงสร้างพร้อมสถานะ "สำเร็จ" และข้อมูลสีปัจจุบัน
    • บันทึกผลลัพธ์ของฟังก์ชันเพื่อแก้ไขข้อบกพร่อง
  3. handleUnknownFunction: ตัวจัดการสำรองสำหรับฟังก์ชันที่ไม่รู้จักซึ่งมีลักษณะดังนี้
    • บันทึกคําเตือนเกี่ยวกับฟังก์ชันที่ไม่รองรับ
    • ส่งการตอบกลับข้อผิดพลาดไปยัง LLM

ฟังก์ชัน 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_vertexai/firebase_vertexai.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

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

part 'gemini_chat_service.g.dart';

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

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

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

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

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

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

ทำความเข้าใจขั้นตอนการสื่อสาร

สิ่งที่เพิ่มเข้ามาที่สำคัญคือการจัดการการเรียกฟังก์ชันและการตอบกลับอย่างสมบูรณ์

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

โค้ดนี้

  1. ตรวจสอบว่าคำตอบ LLM มีการเรียกใช้ฟังก์ชันหรือไม่
  2. สําหรับการเรียกฟังก์ชันแต่ละครั้ง ให้เรียกใช้เมธอด handleFunctionCall ด้วยชื่อฟังก์ชันและอาร์กิวเมนต์
  3. รวบรวมผลลัพธ์ของการเรียกฟังก์ชันแต่ละครั้ง
  4. ส่งผลลัพธ์เหล่านี้กลับไปที่ LLM โดยใช้ Content.functionResponses
  5. ประมวลผลคําตอบของ LLM ต่อผลลัพธ์ของฟังก์ชัน
  6. อัปเดต UI ด้วยข้อความตอบกลับสุดท้าย

ซึ่งจะสร้างโฟลว์แบบไปกลับดังนี้

  • ผู้ใช้ → LLM: ขอสี
  • LLM → แอป: การเรียกใช้ฟังก์ชันที่มีพารามิเตอร์
  • แอป → ผู้ใช้: แสดงสีใหม่
  • แอป → LLM: ผลลัพธ์ของฟังก์ชัน
  • LLM → ผู้ใช้: คำตอบสุดท้ายที่รวมผลลัพธ์ของฟังก์ชัน

สร้างโค้ด Riverpod

เรียกใช้คำสั่งเครื่องมือสร้างเพื่อสร้างโค้ด Riverpod ที่จำเป็น

dart run build_runner build --delete-conflicting-outputs

เรียกใช้และทดสอบขั้นตอนทั้งหมด

จากนั้นเรียกใช้แอปพลิเคชันโดยทำดังนี้

flutter run -d DEVICE

ภาพหน้าจอแอป Colorist แสดง LLM ของ Gemini ที่ตอบสนองด้วยการเรียกใช้ฟังก์ชัน

ลองป้อนคำอธิบายสีต่างๆ ดังนี้

  • "ฉันต้องการสีแดงเข้ม"
  • "แสดงสีฟ้าสดชื่นของท้องฟ้า"
  • "บอกสีของใบมินต์สด"
  • "ฉันอยากเห็นสีส้มแบบพระอาทิตย์ตกที่อบอุ่น"
  • "ทำให้เป็นสีม่วงเข้ม"

ตอนนี้คุณควรเห็นข้อมูลต่อไปนี้

  1. ข้อความของคุณปรากฏในอินเทอร์เฟซแชท
  2. คำตอบของ Gemini ที่ปรากฏในแชท
  3. การเรียกใช้ฟังก์ชันที่บันทึกไว้ในแผงบันทึก
  4. ผลลัพธ์ของฟังก์ชันที่บันทึกทันทีหลังจาก
  5. สี่เหลี่ยมผืนผ้าสีที่อัปเดตเพื่อแสดงสีที่อธิบาย
  6. ค่า RGB อัปเดตเพื่อแสดงองค์ประกอบของสีใหม่
  7. คำตอบสุดท้ายของ Gemini ที่ปรากฏขึ้น ซึ่งมักจะแสดงความคิดเห็นเกี่ยวกับสีที่ตั้งไว้

แผงบันทึกจะให้ข้อมูลเชิงลึกเกี่ยวกับสิ่งที่เกิดขึ้นเบื้องหลัง คุณจะเห็นข้อมูลดังนี้

  • การเรียกใช้ฟังก์ชันที่แน่นอนที่ Gemini ดําเนินการ
  • พารามิเตอร์ที่เลือกสำหรับค่า RGB แต่ละค่า
  • ผลลัพธ์ที่ฟังก์ชันแสดง
  • คำตอบติดตามผลจาก Gemini

ตัวแจ้งสถานะสี

colorStateNotifier ที่คุณใช้อัปเดตสีเป็นส่วนหนึ่งของแพ็กเกจ colorist_ui โดยจัดการสิ่งต่อไปนี้

  • สีปัจจุบันที่แสดงใน UI
  • ประวัติสี (สี 10 รายการล่าสุด)
  • การแจ้งเตือนการเปลี่ยนแปลงสถานะของคอมโพเนนต์ UI

เมื่อคุณเรียก updateColor ด้วยค่า RGB ใหม่ ระบบจะดำเนินการดังนี้

  1. สร้างออบเจ็กต์ ColorData ใหม่ด้วยค่าที่ระบุ
  2. อัปเดตสีปัจจุบันในสถานะแอป
  3. เพิ่มสีลงในประวัติ
  4. ทริกเกอร์การอัปเดต UI ผ่านการจัดการสถานะของ Riverpod

คอมโพเนนต์ UI ในแพ็กเกจ colorist_ui จะคอยตรวจสอบสถานะนี้และอัปเดตโดยอัตโนมัติเมื่อมีการเปลี่ยนแปลง ซึ่งจะสร้างประสบการณ์การใช้งานแบบเรียลไทม์

ทำความเข้าใจการจัดการข้อผิดพลาด

การใช้งานของคุณมีการจัดการข้อผิดพลาดที่มีประสิทธิภาพ ดังนี้

  1. บล็อก Try-catch: รวมการโต้ตอบ LLM ทั้งหมดเพื่อจับข้อยกเว้น
  2. การบันทึกข้อผิดพลาด: บันทึกข้อผิดพลาดในแผงบันทึกพร้อมสแต็กเทรซ
  3. ความคิดเห็นของผู้ใช้: แสดงข้อความแสดงข้อผิดพลาดที่เข้าใจง่ายในแชท
  4. การจัดเก็บสถานะ: สรุปสถานะข้อความแม้ว่าจะเกิดข้อผิดพลาดก็ตาม

วิธีนี้ช่วยให้แอปทำงานได้อย่างเสถียรและแสดงความคิดเห็นที่เหมาะสมแม้ว่าจะเกิดปัญหากับบริการ LLM หรือการดำเนินการของฟังก์ชันก็ตาม

ประโยชน์ของการเรียกใช้ฟังก์ชันสําหรับประสบการณ์ของผู้ใช้

สิ่งที่คุณทําได้แสดงให้เห็นว่า LLM สามารถสร้างอินเทอร์เฟซที่ใช้งานง่ายและมีประสิทธิภาพได้อย่างไร

  1. อินเทอร์เฟซภาษาธรรมชาติ: ผู้ใช้แสดงเจตนาเป็นภาษาที่ใช้ในชีวิตประจำวัน
  2. การตีความที่ชาญฉลาด: LLM จะแปลคําอธิบายที่คลุมเครือเป็นค่าที่แม่นยํา
  3. การโต้ตอบโดยตรง: UI จะอัปเดตตามภาษาที่เป็นธรรมชาติ
  4. การตอบกลับตามบริบท: LLM จะระบุบริบทการสนทนาเกี่ยวกับการเปลี่ยนแปลง
  5. ภาระการรับรู้ต่ำ: ผู้ใช้ไม่จำเป็นต้องเข้าใจค่า RGB หรือทฤษฎีสี

รูปแบบการใช้การเรียกฟังก์ชัน LLM เพื่อเชื่อมภาษาที่เป็นธรรมชาติเข้ากับการดําเนินการของ UI นี้สามารถขยายไปยังโดเมนอื่นๆ อีกมากมายนอกเหนือจากการเลือกสี

ขั้นตอนถัดไปคือ

ในขั้นตอนถัดไป คุณจะปรับปรุงประสบการณ์ของผู้ใช้ด้วยการใช้การตอบกลับแบบสตรีม คุณจะต้องประมวลผลข้อมูลโค้ดและคำเรียกฟังก์ชันเมื่อได้รับแทนที่จะรอการตอบกลับที่สมบูรณ์ ซึ่งจะทำให้แอปพลิเคชันตอบสนองและดึงดูดผู้ใช้ได้มากขึ้น

การแก้ปัญหา

ปัญหาเกี่ยวกับการเรียกใช้ฟังก์ชัน

หาก Gemini ไม่เรียกใช้ฟังก์ชันหรือพารามิเตอร์ไม่ถูกต้อง ให้ทำดังนี้

  • ยืนยันว่าการประกาศฟังก์ชันตรงกับที่อธิบายไว้ในข้อความแจ้งของระบบ
  • ตรวจสอบว่าชื่อและประเภทพารามิเตอร์สอดคล้องกัน
  • ตรวจสอบว่าข้อความแจ้งของระบบบอกให้ LLM ใช้เครื่องมืออย่างชัดเจน
  • ยืนยันว่าชื่อฟังก์ชันในตัวจัดการตรงกับชื่อในประกาศทุกประการ
  • ตรวจสอบแผงบันทึกเพื่อดูข้อมูลโดยละเอียดเกี่ยวกับการเรียกใช้ฟังก์ชัน

ปัญหาเกี่ยวกับการตอบกลับของฟังก์ชัน

หากผลลัพธ์ของฟังก์ชันไม่ได้รับการส่งกลับไปยัง LLM อย่างถูกต้อง ให้ทำดังนี้

  • ตรวจสอบว่าฟังก์ชันแสดงผล Map ที่มีรูปแบบถูกต้อง
  • ตรวจสอบว่า Content.functionResponses สร้างขึ้นอย่างถูกต้อง
  • มองหาข้อผิดพลาดในบันทึกที่เกี่ยวข้องกับการตอบกลับของฟังก์ชัน
  • ตรวจสอบว่าคุณใช้เซสชันการแชทเดียวกันสำหรับการตอบกลับ

ปัญหาเกี่ยวกับการแสดงสี

หากสีแสดงไม่ถูกต้อง ให้ทำดังนี้

  • ตรวจสอบว่าค่า RGB ได้รับการแปลงเป็นเลขทศนิยมอย่างถูกต้อง (LLM อาจส่งเป็นจำนวนเต็ม)
  • ตรวจสอบว่าค่าอยู่ในช่วงที่ต้องการ (0.0 ถึง 1.0)
  • ตรวจสอบว่ามีการเรียกใช้ตัวแจ้งสถานะสีอย่างถูกต้อง
  • ตรวจสอบบันทึกเพื่อหาค่าที่ส่งไปยังฟังก์ชัน

ปัญหาทั่วไป

สำหรับปัญหาทั่วไป

  • ตรวจสอบบันทึกเพื่อหาข้อผิดพลาดหรือคำเตือน
  • ยืนยันการเชื่อมต่อ Vertex AI ใน Firebase
  • ตรวจสอบประเภทที่ไม่ตรงกันในพารามิเตอร์ฟังก์ชัน
  • ตรวจสอบว่าโค้ดที่ Riverpod สร้างขึ้นทั้งหมดเป็นเวอร์ชันล่าสุด

แนวคิดสําคัญที่เรียนรู้

  • การใช้ไปป์ไลน์การเรียกฟังก์ชันที่สมบูรณ์ใน Flutter
  • การสร้างการสื่อสารที่สมบูรณ์ระหว่าง LLM กับแอปพลิเคชันของคุณ
  • ประมวลผล Structured Data จากการตอบกลับ LLM
  • ส่งผลลัพธ์ของฟังก์ชันกลับไปยัง LLM เพื่อรวมไว้ในคำตอบ
  • การใช้แผงบันทึกเพื่อดูการโต้ตอบระหว่าง LLM กับแอปพลิเคชัน
  • เชื่อมโยงอินพุตที่เป็นภาษาธรรมชาติกับการเปลี่ยนแปลง UI ที่ชัดเจน

เมื่อทำขั้นตอนนี้เสร็จแล้ว แอปของคุณจะแสดงรูปแบบที่มีประสิทธิภาพมากที่สุดอย่างหนึ่งสำหรับการผสานรวม LLM ซึ่งก็คือการแปลอินพุตที่เป็นภาษาธรรมชาติเป็นการดําเนินการ UI ที่ชัดเจน ขณะเดียวกันก็คงการสนทนาที่สอดคล้องกันซึ่งยอมรับการดําเนินการเหล่านี้ วิธีนี้สร้างอินเทอร์เฟซแบบสนทนาที่ใช้งานง่ายซึ่งทำให้ผู้ใช้รู้สึกเหมือนมีเวทมนตร์

7. การตอบกลับแบบสตรีมเพื่อประสบการณ์ของผู้ใช้ที่ดียิ่งขึ้น

ในขั้นตอนนี้ คุณจะปรับปรุงประสบการณ์ของผู้ใช้ด้วยการใช้คำตอบแบบสตรีมจาก Gemini คุณจะประมวลผลข้อมูลโค้ดและฟังก์ชันการเรียกเมื่อได้รับข้อมูลแทนที่จะรอให้ระบบสร้างคำตอบทั้งหมด ซึ่งจะทำให้แอปพลิเคชันตอบสนองและดึงดูดผู้ใช้ได้มากขึ้น

สิ่งที่คุณจะพูดถึงในขั้นตอนนี้

  • ความสำคัญของการสตรีมสําหรับแอปพลิเคชันที่ทำงานด้วย LLM
  • การใช้คำตอบ LLM แบบสตรีมในแอปพลิเคชัน Flutter
  • ประมวลผลข้อความบางส่วนเมื่อได้รับจาก API
  • การจัดการสถานะการสนทนาเพื่อป้องกันไม่ให้ข้อความขัดแย้งกัน
  • การจัดการการเรียกฟังก์ชันในการตอบกลับแบบสตรีม
  • การสร้างตัวบ่งชี้ภาพสําหรับคําตอบที่อยู่ระหว่างดำเนินการ

เหตุใดการสตรีมจึงสำคัญต่อใบสมัคร LLM

ก่อนที่จะใช้งาน มาดูกันว่าเหตุใดการตอบกลับแบบสตรีมจึงสำคัญต่อการสร้างประสบการณ์การใช้งานที่ยอดเยี่ยมด้วย LLM

ประสบการณ์ของผู้ใช้ที่ดีขึ้น

การตอบกลับแบบสตรีมมีประโยชน์ที่สำคัญหลายประการต่อประสบการณ์ของผู้ใช้ ดังนี้

  1. เวลาในการตอบสนองที่รับรู้ลดลง: ผู้ใช้จะเห็นข้อความเริ่มปรากฏขึ้นทันที (โดยปกติภายใน 100-300 มิลลิวินาที) แทนที่จะต้องรอหลายวินาทีเพื่อให้ระบบตอบกลับอย่างสมบูรณ์ ความรู้สึกนี้จะช่วยเพิ่มความพึงพอใจของผู้ใช้ได้อย่างมาก
  2. จังหวะการสนทนาที่เป็นธรรมชาติ: ข้อความที่ปรากฏขึ้นทีละน้อยจะเลียนแบบวิธีที่มนุษย์สื่อสารกัน ซึ่งทำให้การสนทนาเป็นธรรมชาติมากขึ้น
  3. การประมวลผลข้อมูลแบบเป็นขั้นเป็นตอน: ผู้ใช้สามารถเริ่มประมวลผลข้อมูลได้ทันทีที่ได้รับข้อมูล แทนที่จะเห็นข้อความจำนวนมากปรากฏขึ้นพร้อมกันจนทำให้สับสน
  4. โอกาสในการขัดจังหวะตั้งแต่เนิ่นๆ: ในแอปพลิเคชันเวอร์ชันเต็ม ผู้ใช้อาจขัดจังหวะหรือเปลี่ยนเส้นทาง LLM ได้หากเห็นว่า LLM กำลังดำเนินการไปในทิศทางที่ไม่เป็นประโยชน์
  5. การยืนยันกิจกรรมด้วยภาพ: ข้อความสตรีมจะแสดงผลทันทีว่าระบบทำงานอยู่ ซึ่งช่วยลดความไม่แน่นอน

ข้อดีทางเทคนิค

นอกจากการปรับปรุง UX แล้ว สตรีมมิงยังมีประโยชน์ทางเทคนิคดังต่อไปนี้

  1. การดำเนินการฟังก์ชันตั้งแต่เนิ่นๆ: ระบบจะตรวจหาและดำเนินการเรียกฟังก์ชันทันทีที่ปรากฏในสตรีมโดยไม่ต้องรอการตอบกลับที่สมบูรณ์
  2. การอัปเดต UI เพิ่มเติม: คุณสามารถอัปเดต UI อย่างต่อเนื่องเมื่อมีข้อมูลใหม่เข้ามา ซึ่งจะสร้างประสบการณ์การใช้งานที่ปรับเปลี่ยนได้มากขึ้น
  3. การจัดการสถานะการสนทนา: สตรีมมิงจะให้สัญญาณที่ชัดเจนว่าคำตอบเสร็จสมบูรณ์แล้วหรือยังอยู่ระหว่างดำเนินการ ซึ่งช่วยให้จัดการสถานะได้ดียิ่งขึ้น
  4. ลดความเสี่ยงการหมดเวลา: การสร้างที่ใช้เวลานานอาจทำให้การเชื่อมต่อหมดเวลาได้หากใช้การตอบกลับแบบไม่สตรีม สตรีมมิงจะสร้างการเชื่อมต่อตั้งแต่เนิ่นๆ และรักษาการเชื่อมต่อไว้

สําหรับแอป Colorist การใช้การสตรีมหมายความว่าผู้ใช้จะเห็นทั้งคําตอบที่เป็นข้อความและการเปลี่ยนแปลงสีปรากฏขึ้นอย่างรวดเร็วยิ่งขึ้น ซึ่งจะทําให้ประสบการณ์การใช้งานตอบสนองได้ดีขึ้นอย่างมาก

เพิ่มการจัดการสถานะการสนทนา

ก่อนอื่นมาเพิ่มผู้ให้บริการสถานะเพื่อติดตามว่าขณะนี้แอปกำลังจัดการการตอบกลับสตรีมมิงอยู่หรือไม่ อัปเดตไฟล์ lib/services/gemini_chat_service.dart โดยทำดังนี้

lib/services/gemini_chat_service.dart

import 'dart:async';

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

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

part 'gemini_chat_service.g.dart';

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

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

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

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

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

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

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

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

ทำความเข้าใจการใช้งานสตรีมมิง

มาดูกันว่าโค้ดนี้ทําอะไรบ้าง

  1. การติดตามสถานะการสนทนา:
    • conversationStateProvider จะติดตามว่าแอปกำลังประมวลผลคำตอบอยู่หรือไม่
    • สถานะเปลี่ยนจาก idlebusy ขณะประมวลผล จากนั้นเปลี่ยนกลับไปเป็น idle
    • วิธีนี้จะช่วยป้องกันคำขอหลายรายการที่เกิดขึ้นพร้อมกันซึ่งอาจขัดแย้งกัน
  2. การเริ่มต้นสตรีม:
    • sendMessageStream() จะแสดงผลสตรีมข้อมูลโค้ดแทน Future ที่มีคำตอบที่สมบูรณ์
    • แต่ละกลุ่มอาจมีข้อความ การเรียกใช้ฟังก์ชัน หรือทั้ง 2 อย่าง
  3. การประมวลผลแบบเป็นขั้นๆ:
    • await for จะประมวลผลแต่ละกลุ่มเมื่อเข้ามาแบบเรียลไทม์
    • ระบบจะเพิ่มข้อความต่อท้าย UI ทันทีเพื่อสร้างเอฟเฟกต์สตรีมมิง
    • การเรียกฟังก์ชันจะดำเนินการทันทีที่ตรวจพบ
  4. การจัดการการเรียกฟังก์ชัน:
    • เมื่อตรวจพบการเรียกฟังก์ชันในข้อมูลโค้ด ระบบจะเรียกใช้ฟังก์ชันนั้นทันที
    • ระบบจะส่งผลลัพธ์กลับไปยัง LLM ผ่านสายสตรีมมิงอีกสายหนึ่ง
    • การตอบสนองของ LLM ต่อผลลัพธ์เหล่านี้ยังได้รับการประมวลผลแบบสตรีมด้วย
  5. การจัดการและล้างข้อผิดพลาด:
    • try/catch มีการจัดการข้อผิดพลาดที่มีประสิทธิภาพ
    • บล็อก finally ช่วยให้มั่นใจว่าสถานะการสนทนาจะรีเซ็ตอย่างถูกต้อง
    • ข้อความจะได้รับการสรุปเสมอ แม้ว่าจะเกิดข้อผิดพลาดก็ตาม

การใช้งานนี้สร้างประสบการณ์การสตรีมที่ตอบสนองและเชื่อถือได้ ในขณะที่ยังคงสถานะการสนทนาที่เหมาะสม

อัปเดตหน้าจอหลักเพื่อเชื่อมต่อสถานะการสนทนา

แก้ไขไฟล์ lib/main.dart เพื่อส่งสถานะการสนทนาไปยังหน้าจอหลัก โดยทำดังนี้

lib/main.dart

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

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

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

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

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

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

การเปลี่ยนแปลงที่สําคัญคือการส่ง conversationState ไปยังวิดเจ็ต MainScreen MainScreen (ซึ่งมาจากแพ็กเกจ colorist_ui) จะใช้สถานะนี้เพื่อปิดใช้การป้อนข้อความขณะประมวลผลคำตอบ

ซึ่งจะสร้างประสบการณ์การใช้งานที่สอดคล้องกันโดย UI จะแสดงสถานะปัจจุบันของการสนทนา

สร้างโค้ด Riverpod

เรียกใช้คำสั่งเครื่องมือสร้างเพื่อสร้างโค้ด Riverpod ที่จำเป็น

dart run build_runner build --delete-conflicting-outputs

เรียกใช้และทดสอบการตอบกลับแบบสตรีม

เรียกใช้แอปพลิเคชัน

flutter run -d DEVICE

ภาพหน้าจอแอป Colorist ที่แสดง LLM ของ Gemini กำลังตอบกลับแบบสตรีม

ตอนนี้ให้ลองทดสอบลักษณะการสตรีมด้วยคำอธิบายสีต่างๆ ลองใช้คำอธิบาย เช่น

  • "แสดงภาพทะเลสีเทอร์ควอยซ์เข้มยามพลบค่ำ"
  • "ฉันอยากเห็นปะการังสีสันสดใสที่ทำให้นึกถึงดอกไม้เขตร้อน"
  • "สร้างสีเขียวมะกอกหม่นๆ เหมือนชุดยูนิฟอร์มทหารเก่าๆ"

ขั้นตอนทางเทคนิคของสตรีมมิงโดยละเอียด

มาดูสิ่งที่จะเกิดขึ้นเมื่อสตรีมคำตอบกัน

การสร้างการเชื่อมต่อ

สิ่งที่จะเกิดขึ้นเมื่อคุณพูดว่า sendMessageStream() มีดังนี้

  1. แอปสร้างการเชื่อมต่อกับบริการ Vertex AI
  2. ระบบส่งคําขอของผู้ใช้ไปยังบริการ
  3. เซิร์ฟเวอร์เริ่มประมวลผลคําขอ
  4. การเชื่อมต่อสตรีมจะยังคงเปิดอยู่และพร้อมที่จะส่งข้อมูล

การส่งข้อมูลเป็นกลุ่ม

ขณะที่ Gemini สร้างเนื้อหา ระบบจะส่งข้อมูลผ่านสตรีมดังนี้

  1. เซิร์ฟเวอร์จะส่งข้อมูลข้อความเป็นกลุ่มๆ ขณะสร้างขึ้น (โดยปกติจะเป็นคำหรือประโยค 2-3 ประโยค)
  2. เมื่อ Gemini ตัดสินใจเรียกใช้ฟังก์ชัน ระบบจะส่งข้อมูลการเรียกใช้ฟังก์ชัน
  3. อาจมีกลุ่มข้อความเพิ่มเติมตามหลังการเรียกใช้ฟังก์ชัน
  4. สตรีมจะดำเนินต่อไปจนกว่าการสร้างจะเสร็จสมบูรณ์

การประมวลผลแบบเป็นขั้นๆ

แอปจะประมวลผลแต่ละกลุ่มทีละกลุ่ม ดังนี้

  1. ระบบจะเพิ่มข้อมูลแต่ละกลุ่มต่อท้ายคำตอบที่มีอยู่
  2. การเรียกฟังก์ชันจะดำเนินการทันทีที่ตรวจพบ
  3. UI จะอัปเดตแบบเรียลไทม์พร้อมทั้งแสดงผลลัพธ์ของข้อความและฟังก์ชัน
  4. ระบบจะติดตามสถานะเพื่อแสดงว่าระบบยังสตรีมคำตอบอยู่

การดูสตรีมจนจบ

เมื่อสร้างเสร็จแล้ว

  1. เซิร์ฟเวอร์ปิดสตรีม
  2. await for ของคุณจะออกจากลูปโดยอัตโนมัติ
  3. ข้อความได้รับการทำเครื่องหมายว่าเสร็จสมบูรณ์แล้ว
  4. ระบบจะตั้งค่าสถานะการสนทนากลับเป็น "ไม่มีการใช้งาน"
  5. UI จะอัปเดตเพื่อแสดงสถานะ "เสร็จสมบูรณ์"

การเปรียบเทียบสตรีมมิงกับไม่ใช่สตรีมมิง

มาดูวิธีเปรียบเทียบแนวทางแบบสตรีมมิงกับแบบไม่สตรีมเพื่อให้เข้าใจประโยชน์ของการสตรีมได้ดียิ่งขึ้น

อัตราส่วน

ไม่ใช่สตรีมมิง

สตรีมมิง

เวลาในการตอบสนองที่รับรู้

ผู้ใช้จะไม่เห็นข้อความใดๆ จนกว่าคำตอบจะพร้อม

ผู้ใช้เห็นคําแรกภายในไม่กี่มิลลิวินาที

ประสบการณ์ของผู้ใช้

การรอเป็นเวลานานตามด้วยข้อความที่ปรากฏขึ้นอย่างฉับพลัน

ลักษณะข้อความที่แสดงอย่างเป็นธรรมชาติและต่อเนื่อง

การจัดการสถานะ

ง่ายขึ้น (ข้อความจะรอดำเนินการหรือเสร็จสมบูรณ์)

ซับซ้อนมากขึ้น (ข้อความอาจอยู่ในสถานะสตรีมมิง)

การดำเนินการของฟังก์ชัน

เกิดขึ้นหลังจากการตอบกลับเสร็จสมบูรณ์เท่านั้น

เกิดขึ้นระหว่างการสร้างคำตอบ

ความซับซ้อนในการติดตั้งใช้งาน

ติดตั้งใช้งานได้ง่าย

ต้องใช้การจัดการสถานะเพิ่มเติม

การแก้ไขข้อผิดพลาด

การตอบกลับแบบทั้งหมดหรือไม่มีเลย

คำตอบบางส่วนอาจยังคงมีประโยชน์

ความซับซ้อนของโค้ด

ซับซ้อนน้อยลง

ซับซ้อนกว่าเนื่องจากการจัดการสตรีม

สําหรับแอปพลิเคชันอย่าง Colorist ประโยชน์ด้าน UX ของสตรีมมิงจะมากกว่าความซับซ้อนในการใช้งาน โดยเฉพาะสําหรับการตีความสีที่อาจใช้เวลาหลายวินาทีในการสร้าง

แนวทางปฏิบัติแนะนำสำหรับ UX ของสตรีมมิง

เมื่อใช้การสตรีมในแอปพลิเคชัน LLM ของคุณเอง ให้พิจารณาแนวทางปฏิบัติแนะนำต่อไปนี้

  1. ตัวบ่งชี้ที่มองเห็นได้ชัดเจน: ระบุตัวบ่งชี้ที่มองเห็นได้ชัดเจนเสมอเพื่อแยกความแตกต่างระหว่างข้อความแบบสตรีมมิงกับข้อความแบบสมบูรณ์
  2. การบล็อกอินพุต: ปิดใช้อินพุตของผู้ใช้ระหว่างสตรีมมิงเพื่อป้องกันคำขอที่ทับซ้อนกันหลายรายการ
  3. การกู้คืนข้อผิดพลาด: ออกแบบ UI ให้จัดการการกู้คืนอย่างราบรื่นหากการสตรีมถูกขัดจังหวะ
  4. การเปลี่ยนสถานะ: ตรวจสอบว่าการเปลี่ยนสถานะระหว่าง "ไม่มีการใช้งาน" "สตรีมมิง" และ "เสร็จสมบูรณ์" เป็นไปอย่างราบรื่น
  5. การแสดงภาพความคืบหน้า: ลองใช้ภาพเคลื่อนไหวหรือตัวบ่งชี้ที่แสดงการประมวลผลที่ทำงานอยู่
  6. ตัวเลือกการยกเลิก: ในแอปที่สมบูรณ์ ให้ระบุวิธีให้ผู้ใช้ยกเลิกการสร้างที่อยู่ระหว่างดำเนินการ
  7. การผสานรวมผลลัพธ์ของฟังก์ชัน: ออกแบบ UI ให้จัดการผลลัพธ์ของฟังก์ชันที่ปรากฏขึ้นระหว่างสตรีม
  8. การเพิ่มประสิทธิภาพ: ลดการสร้าง UI ขึ้นใหม่ในระหว่างการอัปเดตสตรีมอย่างรวดเร็ว

แพ็กเกจ colorist_ui ใช้แนวทางปฏิบัติแนะนำเหล่านี้หลายข้อให้คุณ แต่สิ่งเหล่านี้เป็นข้อควรพิจารณาที่สำคัญสำหรับการใช้งาน LLM แบบสตรีมมิง

ขั้นตอนถัดไปคือ

ในขั้นตอนถัดไป คุณจะใช้การซิงค์ LLM โดยแจ้งให้ Gemini ทราบเมื่อผู้ใช้เลือกสีจากประวัติ ซึ่งจะสร้างประสบการณ์การใช้งานที่เชื่อมโยงกันมากขึ้นเมื่อ LLM รับรู้การเปลี่ยนแปลงสถานะแอปพลิเคชันที่ผู้ใช้เป็นผู้เริ่ม

การแก้ปัญหา

ปัญหาการประมวลผลสตรีม

หากพบปัญหาเกี่ยวกับการประมวลผลสตรีม ให้ทำดังนี้

  • อาการ: การตอบกลับบางส่วน ไม่มีข้อความ หรือสตรีมสิ้นสุดอย่างกะทันหัน
  • วิธีแก้ไข: ตรวจสอบการเชื่อมต่อเครือข่ายและตรวจสอบว่าโค้ดมีรูปแบบ async/await ที่เหมาะสม
  • การวินิจฉัย: ตรวจสอบแผงบันทึกเพื่อหาข้อความแสดงข้อผิดพลาดหรือคำเตือนที่เกี่ยวข้องกับการประมวลผลสตรีม
  • แก้ไข: ตรวจสอบว่าการประมวลผลสตรีมทั้งหมดใช้การจัดการข้อผิดพลาดที่เหมาะสมด้วยบล็อก try/catch

การเรียกใช้ฟังก์ชันขาดหายไป

หากไม่พบการเรียกฟังก์ชันในสตรีม ให้ทำดังนี้

  • อาการ: ข้อความปรากฏขึ้นแต่สีไม่อัปเดต หรือบันทึกไม่แสดงการเรียกใช้ฟังก์ชัน
  • วิธีแก้ไข: ตรวจสอบวิธีการของข้อความแจ้งของระบบเกี่ยวกับการใช้การเรียกฟังก์ชัน
  • การวินิจฉัย: ตรวจสอบแผงบันทึกเพื่อดูว่าระบบได้รับคําเรียกฟังก์ชันหรือไม่
  • แก้ไข: ปรับข้อความแจ้งของระบบให้ระบุให้ LLM ใช้เครื่องมือ set_color อย่างชัดเจนยิ่งขึ้น

การจัดการข้อผิดพลาดทั่วไป

หากพบปัญหาอื่นๆ ให้ทำดังนี้

  • ขั้นตอนที่ 1: ตรวจสอบแผงบันทึกเพื่อหาข้อความแสดงข้อผิดพลาด
  • ขั้นตอนที่ 2: ยืนยันการเชื่อมต่อ Vertex AI ใน Firebase
  • ขั้นตอนที่ 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:

  1. รักษาบริบท: แจ้งให้ LLM ทราบเกี่ยวกับการดําเนินการทั้งหมดของผู้ใช้ที่เกี่ยวข้อง
  2. สร้างความสอดคล้อง: สร้างประสบการณ์การใช้งานที่เชื่อมโยงกันโดย LLM จะรับรู้ถึงการโต้ตอบกับ UI
  3. เพิ่มความฉลาด: ช่วยให้ LLM ตอบสนองต่อการดำเนินการทั้งหมดของผู้ใช้อย่างเหมาะสม
  4. ปรับปรุงประสบการณ์ของผู้ใช้: ทําให้ทั้งแอปพลิเคชันรู้สึกผสานรวมและตอบสนองมากขึ้น
  5. ลดความพยายามของผู้ใช้: ช่วยให้ผู้ใช้ไม่ต้องอธิบายการดําเนินการใน 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_vertexai/firebase_vertexai.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

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

part 'gemini_chat_service.g.dart';

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

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

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

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

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

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

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

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

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

การเพิ่มที่สำคัญคือเมธอด notifyColorSelection ซึ่งมีลักษณะดังนี้

  1. ใช้ออบเจ็กต์ ColorData ที่แสดงสีที่เลือก
  2. เข้ารหัสเป็นรูปแบบ JSON ที่รวมไว้ในข้อความได้
  3. ส่งข้อความที่มีการจัดรูปแบบพิเศษไปยัง LLM เพื่อระบุการเลือกของผู้ใช้
  4. ใช้เมธอด sendMessage ที่มีอยู่ซ้ำเพื่อจัดการการแจ้งเตือน

วิธีนี้จะช่วยหลีกเลี่ยงการทำซ้ำโดยใช้โครงสร้างพื้นฐานการจัดการข้อความที่มีอยู่

อัปเดตแอปหลักเพื่อเชื่อมต่อการแจ้งเตือนการเลือกสี

ตอนนี้ให้แก้ไขไฟล์ lib/main.dart เพื่อส่งฟังก์ชันการแจ้งเตือนการเลือกสีไปยังหน้าจอหลัก โดยทำดังนี้

lib/main.dart

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

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

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

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

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

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

การเปลี่ยนแปลงที่สําคัญคือการเพิ่มการเรียกกลับ notifyColorSelection ซึ่งจะเชื่อมต่อเหตุการณ์ UI (การเลือกสีจากประวัติ) กับระบบการแจ้งเตือน LLM

อัปเดตข้อความแจ้งของระบบ

ตอนนี้คุณต้องอัปเดตพรอมต์ของระบบเพื่อสั่งให้ LLM ตอบสนองต่อการแจ้งเตือนการเลือกสี แก้ไขไฟล์ assets/system_prompt.md โดยทำดังนี้

assets/system_prompt.md

# Colorist System Prompt

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

## Your Capabilities

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

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

## How to Respond to User Inputs

When users describe a color:

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

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

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

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

## When Descriptions are Unclear

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

## When Users Select Historical Colors

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

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

## Important Guidelines

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

การเพิ่มที่สำคัญคือส่วน "เมื่อผู้ใช้เลือกสีที่ผ่านมา" ซึ่งมีลักษณะดังนี้

  1. อธิบายแนวคิดของการแจ้งเตือนการเลือกประวัติให้กับ LLM
  2. แสดงตัวอย่างลักษณะของการแจ้งเตือนเหล่านี้
  3. แสดงตัวอย่างคำตอบที่เหมาะสม
  4. กำหนดความคาดหวังสำหรับการรับทราบการเลือกและแสดงความคิดเห็นเกี่ยวกับสี

ซึ่งช่วยให้ LLM เข้าใจวิธีตอบกลับข้อความพิเศษเหล่านี้อย่างเหมาะสม

สร้างโค้ด Riverpod

เรียกใช้คำสั่งเครื่องมือสร้างเพื่อสร้างโค้ด Riverpod ที่จำเป็น

dart run build_runner build --delete-conflicting-outputs

เรียกใช้และทดสอบการซิงค์บริบท LLM

เรียกใช้แอปพลิเคชัน

flutter run -d DEVICE

ภาพหน้าจอแอป Colorist แสดง LLM ของ Gemini ที่ตอบสนองต่อการเลือกจากประวัติสี

การทดสอบการซิงค์บริบท LLM ประกอบด้วยขั้นตอนต่อไปนี้

  1. ก่อนอื่น ให้สร้างสี 2-3 สีโดยอธิบายสีในแชท
    • "แสดงสีม่วงสด"
    • "ฉันต้องการสีเขียวเข้ม"
    • "ขอสีแดงสด"
  2. จากนั้นคลิกภาพขนาดย่อสีภาพใดภาพหนึ่งในการแถบประวัติ

สิ่งที่ควรสังเกตมีดังนี้

  1. สีที่เลือกจะปรากฏในจอแสดงผลหลัก
  2. ข้อความของผู้ใช้จะปรากฏในแชทเพื่อระบุการเลือกสี
  3. LLM ตอบสนองด้วยการรับทราบการเลือกและแสดงความคิดเห็นเกี่ยวกับสี
  4. การโต้ตอบทั้งหมดดูเป็นธรรมชาติและเชื่อมโยงกัน

วิธีนี้ช่วยให้ LLM รับรู้และตอบสนองต่อทั้งข้อความโดยตรงและการโต้ตอบกับ UI อย่างเหมาะสม

วิธีการทํางานของการซิงค์บริบท LLM

มาดูรายละเอียดทางเทคนิคเกี่ยวกับการทํางานของการซิงค์นี้กัน

โฟลว์ข้อมูล

  1. การดําเนินการของผู้ใช้: ผู้ใช้คลิกสีในแถบประวัติ
  2. เหตุการณ์ UI: วิดเจ็ต MainScreen ตรวจพบการเลือกนี้
  3. การดำเนินการของ Callback: ทริกเกอร์ Callback notifyColorSelection
  4. การสร้างข้อความ: สร้างข้อความที่มีการจัดรูปแบบพิเศษโดยใช้ข้อมูลสี
  5. การประมวลผล LLM: ระบบจะส่งข้อความไปยัง Gemini ซึ่งจะจดจำรูปแบบได้
  6. การตอบกลับตามบริบท: Gemini ตอบกลับอย่างเหมาะสมตามพรอมต์ของระบบ
  7. การอัปเดต UI: คำตอบจะปรากฏในแชทเพื่อสร้างประสบการณ์การใช้งานที่ราบรื่น

การจัดรูปแบบข้อมูล

สิ่งสำคัญของแนวทางนี้คือวิธีจัดรูปแบบข้อมูลสี

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

เมธอด toLLMContextMap() (จากแพ็กเกจ colorist_ui) จะแปลงออบเจ็กต์ ColorData เป็นแผนที่ที่มีพร็อพเพอร์ตี้หลักที่ LLM เข้าใจ ซึ่งโดยทั่วไปจะมีดังต่อไปนี้

  • ค่า RGB (แดง เขียว น้ำเงิน)
  • การนำเสนอรหัสฐานสิบหก
  • ชื่อหรือคำอธิบายที่เชื่อมโยงกับสี

การจัดรูปแบบข้อมูลนี้อย่างสม่ำเสมอและใส่ไว้ในข้อความจะช่วยให้ LLM มีข้อมูลทั้งหมดที่จำเป็นในการตอบกลับอย่างเหมาะสม

การใช้งานการซิงค์บริบท LLM ที่กว้างขึ้น

รูปแบบการแจ้งเตือน LLM เกี่ยวกับเหตุการณ์ UI นี้มีประโยชน์หลายอย่างนอกเหนือจากการเลือกสี

กรณีการใช้งานอื่นๆ

  1. การเปลี่ยนแปลงตัวกรอง: แจ้ง LLM เมื่อผู้ใช้ใช้ตัวกรองกับข้อมูล
  2. เหตุการณ์การนําทาง: แจ้ง LLM เมื่อผู้ใช้ไปยังส่วนต่างๆ
  3. การเปลี่ยนแปลงการเลือก: อัปเดต LLM เมื่อผู้ใช้เลือกรายการจากรายการหรือตารางกริด
  4. การอัปเดตค่ากําหนด: บอก LLM เมื่อผู้ใช้เปลี่ยนการตั้งค่าหรือค่ากําหนด
  5. การจัดการข้อมูล: แจ้ง LLM เมื่อผู้ใช้เพิ่ม แก้ไข หรือลบข้อมูล

ในแต่ละกรณี รูปแบบจะยังคงเหมือนเดิม ดังนี้

  1. ตรวจหาเหตุการณ์ UI
  2. แปลงข้อมูลที่เกี่ยวข้องเป็นรูปแบบอนุกรม
  3. ส่งการแจ้งเตือนที่มีรูปแบบพิเศษไปยัง LLM
  4. แนะนําให้ LLM ตอบกลับอย่างเหมาะสมผ่านพรอมต์ของระบบ

แนวทางปฏิบัติแนะนำสำหรับการซิงค์บริบท LLM

แนวทางปฏิบัติแนะนำบางส่วนสำหรับการซิงค์บริบท LLM ที่มีประสิทธิภาพตามการใช้งานของคุณมีดังนี้

1. ใช้รูปแบบที่สม่ำเสมอ

ใช้รูปแบบที่สอดคล้องกันสำหรับการแจ้งเตือนเพื่อให้ LLM ระบุการแจ้งเตือนเหล่านั้นได้ง่าย

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

2. บริบทที่สมบูรณ์

ระบุรายละเอียดในการแจ้งเตือนให้เพียงพอเพื่อให้ LLM ตอบสนองอย่างชาญฉลาด สำหรับสี ข้อมูลนี้หมายถึงค่า RGB, รหัส Hex และพร็อพเพอร์ตี้อื่นๆ ที่เกี่ยวข้อง

3. วิธีการที่ชัดเจน

ระบุวิธีการที่ชัดเจนในข้อความแจ้งของระบบเกี่ยวกับวิธีจัดการการแจ้งเตือน โดยควรมีตัวอย่างด้วย

4. การผสานรวมอย่างเป็นธรรมชาติ

ออกแบบการแจ้งเตือนให้สอดคล้องไปกับการสนทนาอย่างเป็นธรรมชาติ ไม่ใช่การขัดจังหวะทางเทคนิค

5. การแจ้งเตือนแบบเลือก

แจ้งให้ LLM ทราบเฉพาะการดําเนินการที่เกี่ยวข้องกับการสนทนาเท่านั้น ไม่จำเป็นต้องสื่อสารเหตุการณ์ UI ทั้งหมด

การแก้ปัญหา

ปัญหาการแจ้งเตือน

หาก LLM ไม่ตอบสนองต่อการเลือกสีอย่างถูกต้อง ให้ทำดังนี้

  • ตรวจสอบว่ารูปแบบข้อความการแจ้งเตือนตรงกับที่อธิบายไว้ในข้อความแจ้งของระบบ
  • ยืนยันว่ามีการจัดรูปแบบข้อมูลสีอย่างถูกต้อง
  • ตรวจสอบว่าข้อความแจ้งของระบบมีวิธีการที่ชัดเจนสำหรับการจัดการการเลือก
  • มองหาข้อผิดพลาดในบริการแชทเมื่อส่งการแจ้งเตือน

การจัดการบริบท

หาก LLM ดูเหมือนจะสูญเสียบริบท ให้ทำดังนี้

  • ตรวจสอบว่าเซสชันการแชทได้รับการดูแลรักษาอย่างเหมาะสม
  • ยืนยันว่าสถานะการสนทนาเปลี่ยนอย่างถูกต้อง
  • ตรวจสอบว่าระบบส่งการแจ้งเตือนผ่านเซสชันการแชทเดียวกัน

ปัญหาทั่วไป

สำหรับปัญหาทั่วไป

  • ตรวจสอบบันทึกเพื่อหาข้อผิดพลาดหรือคำเตือน
  • ยืนยันการเชื่อมต่อ Vertex AI ใน Firebase
  • ตรวจสอบประเภทที่ไม่ตรงกันในพารามิเตอร์ฟังก์ชัน
  • ตรวจสอบว่าโค้ดที่ 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

ในตอนที่ 59 Craig Labenz และ Andrew Brogden สำรวจ Codelab นี้ โดยไฮไลต์ส่วนที่น่าสนใจของการสร้างแอป

ในตอนที่ 60 นี้ มาร่วมสนุกกับ Craig และ Andrew อีกครั้งขณะขยายแอป Codelab ด้วยความสามารถใหม่ๆ และพยายามทำให้ LLM ทําตามคําสั่ง

ในตอนที่ 61 นี้ Craig จะพูดคุยกับ Chris Sells เพื่อวิเคราะห์บรรทัดแรกของข่าวและสร้างรูปภาพที่เกี่ยวข้อง

ความคิดเห็น

เราอยากทราบความคิดเห็นของคุณเกี่ยวกับ Codelab นี้ โปรดแสดงความคิดเห็นผ่านช่องทางต่อไปนี้

ขอขอบคุณที่ทํา Codelab นี้จนเสร็จสมบูรณ์ และหวังว่าคุณจะได้สำรวจความเป็นไปได้ที่น่าตื่นเต้นที่จุดตัดระหว่าง Flutter กับ AI ต่อไป