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

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

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

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

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

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

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

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

เหตุผลที่เรื่องนี้มีความสำคัญต่อนักพัฒนาแอป Flutter

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

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

Codelab นี้จะแนะนำขั้นตอนการสร้าง Colorist ทีละขั้นตอน

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

ซึ่งจะเพิ่มแพ็กเกจคีย์ต่อไปนี้

  • 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.9.2

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

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

flutter:
  uses-material-design: true

ใช้ไฟล์ main.dart

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

lib/main.dart

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

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

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

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

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

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

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

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

มาใช้เวลาสักครู่เพื่อทำความเข้าใจสถาปัตยกรรมของแอป colorist กัน

แพ็กเกจ colorist_ui

colorist_ui แพ็กเกจมีคอมโพเนนต์ 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 แสดงการแสดงผลมาร์กดาวน์ของบริการ Echo

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

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

ลองพิมพ์ข้อความ เช่น "ฉันอยากได้สีน้ำเงินเข้ม" แล้วกดส่ง บริการ Echo จะพูดข้อความของคุณซ้ำ ในขั้นตอนต่อๆ ไป คุณจะแทนที่ค่านี้ด้วยการตีความสีจริงโดยใช้ตรรกะ AI ของ Firebase

ขั้นตอนต่อไปคืออะไร

ในขั้นตอนถัดไป คุณจะกำหนดค่า Firebase และใช้การผสานรวม Gemini API ขั้นพื้นฐานเพื่อแทนที่บริการก้องด้วยบริการแชทของ 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

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

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

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

ติดตั้ง FlutterFire CLI

FlutterFire CLI ช่วยให้การตั้งค่า Firebase ในแอป Flutter ง่ายขึ้น

dart pub global activate flutterfire_cli

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

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

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

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

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

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

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

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

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

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

macos/Runner/DebugProfile.entitlements

<key>com.apple.security.network.client</key>
<true/>
  1. นอกจากนี้ ให้เปิด macos/Runner/Release.entitlements แล้วเพิ่มรายการเดียวกัน

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

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

ios/Podfile

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

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

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

lib/providers/gemini.dart

import 'dart:async';

import 'package:firebase_ai/firebase_ai.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

import '../firebase_options.dart';

part 'gemini.g.dart';

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

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

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

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

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

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

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

ติดตั้งใช้งานบริการ Gemini Chat

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

lib/services/gemini_chat_service.dart

import 'dart:async';

import 'package:colorist_ui/colorist_ui.dart';
import 'package:firebase_ai/firebase_ai.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

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

part 'gemini_chat_service.g.dart';

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

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

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

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

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

บริการนี้

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

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

สร้างโค้ด Riverpod

เรียกใช้คำสั่ง Build 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),
      ),
    );
  }
}

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

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

เรียกใช้แอป

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

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 ผ่านตรรกะ 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 เพื่อเข้าถึง 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 ที่สอดคล้องและมีประโยชน์ เนื่องจาก

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

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

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

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

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

assets/system_prompt.md

# Colorist System Prompt

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

## Your Capabilities

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

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

## How to Respond to User Inputs

When users describe a color:

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

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

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

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

## When Descriptions are Unclear

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

## Important Guidelines

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

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

มาดูรายละเอียดของพรอมต์นี้กัน

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

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

อัปเดต pubspec.yaml

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

pubspec.yaml

flutter:
  uses-material-design: true

  assets:
    - assets/

เรียกใช้ flutter pub get เพื่อรีเฟรชแพ็กเกจเนื้อหา

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

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

lib/providers/system_prompt.dart

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

part 'system_prompt.g.dart';

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

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

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

ตอนนี้ให้แก้ไขไฟล์ lib/providers/gemini.dart เพื่อรวมพรอมต์ของระบบ

lib/providers/gemini.dart

import 'dart:async';

import 'package:firebase_ai/firebase_ai.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

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

part 'gemini.g.dart';

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

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

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

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

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

สร้างโค้ด Riverpod

เรียกใช้คำสั่ง Build Runner เพื่อสร้างโค้ด 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

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

  • โปรดทราบว่าบริการ Firebase AI Logic มีโควต้าการใช้งาน
  • พิจารณานำตรรกะการลองใหม่ไปใช้กับ 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 สามารถทริกเกอร์การดำเนินการที่เฉพาะเจาะจงในแอปพลิเคชันได้ แทนที่จะให้ LLM อธิบายสิ่งที่ต้องทำเท่านั้น

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

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

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

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

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

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

lib/services/gemini_tools.dart

import 'package:firebase_ai/firebase_ai.dart';
import 'package: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_ai/firebase_ai.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

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

part 'gemini.g.dart';

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

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

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

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

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

เรียกใช้คำสั่ง Build Runner เพื่อสร้างโค้ด 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 คาดหวังว่าจะได้รับผลลัพธ์ของฟังก์ชัน

ในสถานะปัจจุบันของแอป ขั้นตอนที่ 1-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. อินพุตของผู้ใช้: ผู้ใช้อธิบายสีในภาษาธรรมชาติ (เช่น "เขียวเข้ม")
  2. การประมวลผล LLM: Gemini วิเคราะห์คำอธิบายและตัดสินใจเรียกใช้ฟังก์ชัน set_color
  3. การสร้างการเรียกใช้ฟังก์ชัน: Gemini สร้าง JSON ที่มีโครงสร้างพร้อมพารามิเตอร์ (ค่าสีแดง เขียว น้ำเงิน)
  4. การรับการเรียกใช้ฟังก์ชัน: แอปของคุณจะได้รับข้อมูลที่มีโครงสร้างนี้จาก Gemini
  5. การเรียกใช้ฟังก์ชัน: แอปของคุณเรียกใช้ฟังก์ชันด้วยพารามิเตอร์ที่ระบุ
  6. การอัปเดตสถานะ: ฟังก์ชันจะอัปเดตสถานะของแอป (เปลี่ยนสีที่แสดง)
  7. การสร้างคำตอบ: ฟังก์ชันจะส่งผลลัพธ์กลับไปยัง LLM
  8. การรวมคำตอบ: LLM จะรวมผลลัพธ์เหล่านี้ไว้ในคำตอบสุดท้าย
  9. การอัปเดต 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
GeminiTools geminiTools(Ref ref) => GeminiTools(ref);

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

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

  1. handleFunctionCall: ผู้จัดส่งส่วนกลางที่มีคุณสมบัติดังนี้
    • บันทึกการเรียกฟังก์ชันเพื่อความโปร่งใสในแผงบันทึก
    • กำหนดเส้นทางไปยังแฮนเดิลที่เหมาะสมตามชื่อฟังก์ชัน
    • แสดงผลการตอบกลับที่มีโครงสร้างซึ่งจะส่งกลับไปยัง LLM
  2. handleSetColor: ตัวแฮนเดิลเฉพาะสำหรับฟังก์ชัน set_color ของคุณซึ่งมีลักษณะดังนี้
    • ดึงค่า RGB จากแผนที่อาร์กิวเมนต์
    • แปลงเป็นประเภทที่คาดไว้ (ดับเบิล)
    • อัปเดตสถานะสีของแอปพลิเคชันโดยใช้ 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_ai/firebase_ai.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

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

part 'gemini_chat_service.g.dart';

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

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

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

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

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

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

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

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

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

รหัสนี้

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

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

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

สร้างโค้ด Riverpod

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

dart run build_runner build --delete-conflicting-outputs

เรียกใช้และทดสอบโฟลว์ทั้งหมด

ตอนนี้ให้เรียกใช้แอปพลิเคชันโดยใช้คำสั่งต่อไปนี้

flutter run -d DEVICE

ภาพหน้าจอแอป Colorist แสดง 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 ไม่ถูกต้อง ให้ทำดังนี้

  • ตรวจสอบว่าฟังก์ชันแสดงผลแผนที่ที่มีรูปแบบถูกต้อง
  • ตรวจสอบว่ามีการสร้าง 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 กัน

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

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

  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_ai/firebase_ai.dart';
import 'package:flutter_riverpod/legacy.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

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

part 'gemini_chat_service.g.dart';

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

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

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

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

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

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

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

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

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

มาดูรายละเอียดว่าโค้ดนี้ทำอะไรได้บ้าง

  1. การติดตามสถานะการสนทนา:
    • conversationStateProvider จะติดตามว่าแอปกำลังประมวลผลคำตอบอยู่หรือไม่
    • สถานะจะเปลี่ยนจาก idlebusy ขณะประมวลผล แล้วเปลี่ยนกลับเป็น idle
    • ซึ่งจะป้องกันไม่ให้มีคำขอที่เกิดขึ้นพร้อมกันหลายรายการที่อาจขัดแย้งกัน
  2. การเริ่มต้นสตรีม:
    • sendMessageStream() จะแสดงสตรีมของกลุ่มการตอบกลับแทน Future ที่มีการตอบกลับที่สมบูรณ์
    • แต่ละก้อนอาจมีข้อความ การเรียกใช้ฟังก์ชัน หรือทั้ง 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

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

dart run build_runner build --delete-conflicting-outputs

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

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

flutter run -d DEVICE

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

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

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

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

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

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

เมื่อคุณโทรหา sendMessageStream() จะเกิดสิ่งต่อไปนี้

  1. แอปสร้างการเชื่อมต่อกับบริการตรรกะ AI ของ Firebase
  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: ยืนยันการเชื่อมต่อ 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:

  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_ai/firebase_ai.dart';
import 'package:flutter_riverpod/legacy.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

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

part 'gemini_chat_service.g.dart';

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

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

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

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

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

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

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

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

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

ฟีเจอร์สำคัญที่เพิ่มเข้ามาคือnotifyColorSelection ซึ่งมีคุณสมบัติดังนี้

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

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

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

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

lib/main.dart

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

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

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

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

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

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

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

อัปเดตพรอมต์ของระบบ

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

assets/system_prompt.md

# Colorist System Prompt

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

## Your Capabilities

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

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

## How to Respond to User Inputs

When users describe a color:

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

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

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

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

## When Descriptions are Unclear

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

## When Users Select Historical Colors

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

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

## Important Guidelines

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

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

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

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

สร้างโค้ด Riverpod

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

dart run build_runner build --delete-conflicting-outputs

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

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

flutter run -d DEVICE

ภาพหน้าจอแอป Colorist แสดง 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 ดูเหมือนจะสูญเสียบริบท ให้ทำดังนี้

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

ปัญหาทั่วไป

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

  • ตรวจสอบบันทึกเพื่อหาข้อผิดพลาดหรือคำเตือน
  • ยืนยันการเชื่อมต่อ 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 ต่อไป