Gemini-gestützte Flutter-App erstellen

Gemini-gestützte Flutter-App erstellen

Informationen zu diesem Codelab

subjectZuletzt aktualisiert: Mai 19, 2025
account_circleVerfasst von Brett Morgan

1. Gemini-gestützte Flutter-App erstellen

Aufgaben

In diesem Codelab erstellen Sie Colorist, eine interaktive Flutter-Anwendung, die die Vorteile der Gemini API direkt in Ihre Flutter-App einbindet. Sie wollten schon immer, dass Nutzer Ihre App per natürlicher Sprache steuern können, wussten aber nicht, wo Sie anfangen sollten? In diesem Codelab erfahren Sie, wie das geht.

Mit Colorist können Nutzer Farben in natürlicher Sprache beschreiben (z. B. „das Orange eines Sonnenuntergangs“ oder „tiefes Ozeanblau“). Die App:

  • Diese Beschreibungen werden mit der Gemini API von Google verarbeitet.
  • Die Beschreibungen werden in genaue RGB-Farbwerte umgewandelt.
  • Die Farbe wird in Echtzeit auf dem Bildschirm angezeigt.
  • Enthält technische Farbdetails und interessante Informationen zur Farbe
  • Behält einen Verlauf der kürzlich generierten Farben bei

Screenshot der Colorist App mit Farbanzeige und Chat-Oberfläche

Die App verfügt über eine Splitscreen-Oberfläche mit einem Farbdisplaybereich und einem interaktiven Chatsystem auf der einen Seite und einem detaillierten Protokollbereich mit den Rohdaten der LLM-Interaktionen auf der anderen Seite. Anhand dieses Protokolls können Sie besser nachvollziehen, wie eine LLM-Integration im Detail funktioniert.

Warum das für Flutter-Entwickler wichtig ist

LLMs revolutionieren die Interaktion von Nutzern mit Anwendungen. Ihre effektive Einbindung in mobile und Desktop-Apps stellt jedoch einzigartige Herausforderungen dar. In diesem Codelab lernen Sie praktische Muster kennen, die über die reinen API-Aufrufe hinausgehen.

Ihre Lernreise

In diesem Codelab erfahren Sie Schritt für Schritt, wie Sie Colorist erstellen:

  1. Projekteinrichtung: Sie beginnen mit einer einfachen Flutter-App-Struktur und dem colorist_ui-Paket.
  2. Einfache Gemini-Integration: Sie verbinden Ihre App mit Vertex AI in Firebase und implementieren eine einfache LLM-Kommunikation.
  3. Effektive Prompts: Erstellen Sie einen Systemprompt, der das LLM bei der Interpretation von Farbbeschreibungen unterstützt.
  4. Funktionsdeklarationen: Hiermit werden Tools definiert, mit denen das LLM Farben in Ihrer Anwendung festlegen kann.
  5. Tool-Verarbeitung: Funktionaufrufe vom LLM verarbeiten und mit dem Status Ihrer App verknüpfen
  6. Streamingantworten: Verbessern Sie die Nutzerfreundlichkeit mit Echtzeit-Streaming-LLM-Antworten.
  7. LLM-Kontextsynchronisierung: Sorgen Sie für eine einheitliche Nutzererfahrung, indem Sie das LLM über Nutzeraktionen informieren.

Lerninhalte

  • Vertex AI in Firebase für Flutter-Anwendungen konfigurieren
  • Effektive Systemprompts erstellen, um das LLM-Verhalten zu steuern
  • Funktionsdeklarationen implementieren, die eine Brücke zwischen natürlicher Sprache und App-Funktionen schlagen
  • Streamingantworten verarbeiten, um die Nutzerfreundlichkeit zu verbessern
  • Status zwischen UI-Ereignissen und dem LLM synchronisieren
  • LLM-Unterhaltungsstatus mit Riverpod verwalten
  • Fehler in LLM-gestützten Anwendungen ordnungsgemäß behandeln

Codevorschau: Ein kleiner Einblick in das, was Sie implementieren

Hier ein Ausschnitt aus der Funktionsdeklaration, die Sie erstellen, damit der LLM Farben in Ihrer App festlegen kann:

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)'),
  },
);

Videoübersicht dieses Codelabs

In der 59. Folge von Observable Flutter sprechen Craig Labenz und Andrew Brogdon über dieses Codelab:

Vorbereitung

Damit Sie dieses Codelab optimal nutzen können, sollten Sie Folgendes haben:

  • Flutter-Entwicklungserfahrung: Kenntnisse über die Grundlagen von Flutter und die Dart-Syntax
  • Kenntnisse zur asynchronen Programmierung: Kenntnisse zu Futures, async/await und Streams
  • Firebase-Konto: Sie benötigen ein Google-Konto, um Firebase einzurichten.
  • Firebase-Projekt mit aktivierter Abrechnung: Für Vertex AI in Firebase ist ein Rechnungskonto erforderlich.

Beginnen wir mit der Entwicklung Ihrer ersten LLM-gestützten Flutter-App.

2. Projekteinrichtung und Echo-Dienst

In diesem ersten Schritt richten Sie die Projektstruktur ein und implementieren einen einfachen Echodienst, der später durch die Integration der Gemini API ersetzt wird. So wird die Anwendungsarchitektur festgelegt und sichergestellt, dass die Benutzeroberfläche richtig funktioniert, bevor die Komplexität von LLM-Aufrufen hinzugefügt wird.

Was Sie in diesem Schritt lernen

  • Einrichten eines Flutter-Projekts mit den erforderlichen Abhängigkeiten
  • Mit dem colorist_ui-Paket für UI-Komponenten arbeiten
  • Echo-Nachrichtendienst implementieren und mit der Benutzeroberfläche verbinden

Wichtiger Hinweis zu Preisen

Neues Flutter-Projekt erstellen

Erstellen Sie zuerst ein neues Flutter-Projekt mit dem folgenden Befehl:

flutter create -e colorist --platforms=android,ios,macos,web,windows

Das Flag -e gibt an, dass Sie ein leeres Projekt ohne die Standard-counter-App benötigen. Die App ist für Computer, Mobilgeräte und das Web konzipiert. Linux wird von flutterfire derzeit jedoch nicht unterstützt.

Abhängigkeiten hinzufügen

Gehen Sie zu Ihrem Projektverzeichnis und fügen Sie die erforderlichen Abhängigkeiten hinzu:

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

Dadurch werden die folgenden wichtigen Pakete hinzugefügt:

  • colorist_ui: Ein benutzerdefiniertes Paket, das die UI-Komponenten für die Colorist-App bereitstellt
  • flutter_riverpod und riverpod_annotation: Für die Statusverwaltung
  • logging: Für strukturiertes Logging
  • Entwicklungsabhängigkeiten für die Codegenerierung und das Linting

Ihre pubspec.yaml sieht dann in etwa so aus:

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

Analyseoptionen konfigurieren

Fügen Sie der Datei analysis_options.yaml im Stammverzeichnis Ihres Projekts custom_lint hinzu:

include: package:flutter_lints/flutter.yaml

analyzer:
  plugins:
    - custom_lint

Mit dieser Konfiguration können Sie Riverpod-spezifische Lints verwenden, um die Codequalität aufrechtzuerhalten.

main.dart-Datei implementieren

Ersetzen Sie den Inhalt von lib/main.dart durch Folgendes:

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);
 
}
}

Dadurch wird eine Flutter-App eingerichtet, die einen einfachen Echodienst implementiert, der das Verhalten eines LLM nachahmt, indem er einfach die Nachricht des Nutzers zurückgibt.

Übersicht der Architektur

Sehen wir uns die Architektur der colorist-App an:

Das Paket colorist_ui

Das colorist_ui-Paket bietet vorgefertigte UI-Komponenten und Tools zur Zustandsverwaltung:

  1. MainScreen: Die Hauptkomponente der Benutzeroberfläche, die Folgendes enthält:
    • Ein Splitscreen-Layout auf dem Computer (Interaktionsbereich und Protokollbereich)
    • Tab-Oberfläche auf Mobilgeräten
    • Farbanzeige, Chatoberfläche und Miniaturansichten des Protokolls
  2. Statusverwaltung: Die App verwendet mehrere Statusbenachrichtigungen:
    • ChatStateNotifier: Verwaltet die Chatnachrichten
    • ColorStateNotifier: Verwaltet die aktuelle Farbe und den Verlauf
    • LogStateNotifier: Verwaltet die Logeinträge für die Fehlerbehebung
  3. Nachrichtenverwaltung: Die App verwendet ein Nachrichtenmodell mit verschiedenen Status:
    • Nutzermitteilungen: Vom Nutzer eingegeben
    • LLM-Nachrichten: Vom LLM (oder vorerst von Ihrem Echo-Dienst) generiert
    • MessageState: Gibt an, ob LLM-Nachrichten vollständig sind oder noch gestreamt werden

Anwendungsarchitektur

Die App folgt der folgenden Architektur:

  1. UI-Ebene: Wird vom colorist_ui-Paket bereitgestellt
  2. Zustandsverwaltung: Verwendet Riverpod für die reaktive Zustandsverwaltung
  3. Dienstebene: Enthält derzeit den einfachen Echodienst, der durch den Gemini Chat-Dienst ersetzt wird.
  4. LLM-Integration: Wird in späteren Schritten hinzugefügt

Durch diese Trennung können Sie sich auf die Implementierung der LLM-Integration konzentrieren, während die UI-Komponenten bereits vorhanden sind.

Anwendung ausführen

Führen Sie die App mit dem folgenden Befehl aus:

flutter run -d DEVICE

Ersetzen Sie DEVICE durch das Zielgerät, z. B. macos, windows, chrome oder eine Geräte-ID.

Screenshot der Colorist App, in dem der Echo-Dienst Markdown rendert

Jetzt sollte die Colorist App mit folgenden Elementen angezeigt werden:

  1. Ein farbiger Anzeigebereich mit einer Standardfarbe
  2. Eine Chatoberfläche, in der Sie Nachrichten eingeben können
  3. Ein Log-Bereich mit den Chatinteraktionen

Geben Sie eine Nachricht wie „Ich möchte eine dunkelblaue Farbe“ ein und drücken Sie auf „Senden“. Der Echodienst wiederholt einfach Ihre Nachricht. In späteren Schritten ersetzen Sie dies durch eine tatsächliche Farbinterpretation mit der Gemini API über Vertex AI in Firebase.

Nächste Schritte

Im nächsten Schritt konfigurieren Sie Firebase und implementieren eine grundlegende Gemini API-Integration, um Ihren Echo-Dienst durch den Gemini-Chatdienst zu ersetzen. So kann die App Farbbeschreibungen interpretieren und intelligente Antworten geben.

Fehlerbehebung

Probleme mit UI-Paketen

Wenn Probleme mit dem colorist_ui-Paket auftreten:

  • Prüfen, ob die neueste Version verwendet wird
  • Prüfen, ob die Abhängigkeit richtig hinzugefügt wurde
  • Prüfen Sie, ob Paketversionen in Konflikt stehen

Build-Fehler

Wenn Buildfehler auftreten:

  • Prüfen Sie, ob Sie das neueste Flutter SDK für den stabilen Kanal installiert haben.
  • flutter clean gefolgt von flutter pub get ausführen
  • Konsolenausgabe auf bestimmte Fehlermeldungen prüfen

Wichtige Konzepte

  • Ein Flutter-Projekt mit den erforderlichen Abhängigkeiten einrichten
  • Architektur und Komponentenverantwortlichkeiten der Anwendung
  • Implementieren eines einfachen Dienstes, der das Verhalten eines LLM nachahmt
  • Dienst mit den UI-Komponenten verbinden
  • Riverpod für die Zustandsverwaltung verwenden

3. Einfache Gemini Chat-Integration

In diesem Schritt ersetzen Sie den Echodienst aus dem vorherigen Schritt durch die Gemini API-Integration mit Vertex AI in Firebase. Sie konfigurieren Firebase, richten die erforderlichen Anbieter ein und implementieren einen einfachen Chatdienst, der mit der Gemini API kommuniziert.

Was Sie in diesem Schritt lernen

  • Firebase in einer Flutter-Anwendung einrichten
  • Vertex AI in Firebase für den Gemini-Zugriff konfigurieren
  • Riverpod-Anbieter für Firebase- und Gemini-Dienste erstellen
  • Einen einfachen Chatdienst mit der Gemini API implementieren
  • Umgang mit asynchronen API-Antworten und Fehlerstatus

Firebase einrichten

Zuerst müssen Sie Firebase für Ihr Flutter-Projekt einrichten. Dazu müssen Sie ein Firebase-Projekt erstellen, Ihre App hinzufügen und die erforderlichen Vertex AI-Einstellungen konfigurieren.

Firebase-Projekt erstellen

  1. Rufen Sie die Firebase Console auf und melden Sie sich mit Ihrem Google-Konto an.
  2. Klicken Sie auf Firebase-Projekt erstellen oder wählen Sie ein vorhandenes Projekt aus.
  3. Folgen Sie dem Einrichtungsassistenten, um Ihr Projekt zu erstellen.
  4. Nachdem Ihr Projekt erstellt wurde, müssen Sie ein Upgrade auf den Blaze-Tarif (Pay-per-Use) durchführen, um auf Vertex AI-Dienste zugreifen zu können. Klicken Sie links unten in der Firebase Console auf die Schaltfläche Aktualisieren.

Vertex AI in Ihrem Firebase-Projekt einrichten

  1. Rufen Sie in der Firebase Console Ihr Projekt auf.
  2. Wählen Sie in der linken Seitenleiste AI aus.
  3. Wählen Sie auf der Karte „Vertex AI in Firebase“ die Option Jetzt starten aus.
  4. Folgen Sie der Anleitung, um die Vertex AI in Firebase APIs für Ihr Projekt zu aktivieren.

FlutterFire CLI installieren

Mit der FlutterFire CLI wird die Firebase-Einrichtung in Flutter-Apps vereinfacht:

dart pub global activate flutterfire_cli

Firebase zu Ihrer Flutter-App hinzufügen

  1. Fügen Sie Ihrem Projekt die Firebase Core- und Vertex AI-Pakete hinzu:
flutter pub add firebase_core firebase_vertexai
  1. Führen Sie den FlutterFire-Konfigurationsbefehl aus:
flutterfire configure

Mit diesem Befehl wird Folgendes ausgeführt:

  • Sie werden aufgefordert, das Firebase-Projekt auszuwählen, das Sie gerade erstellt haben.
  • Flutter-App(s) bei Firebase registrieren
  • firebase_options.dart-Datei mit Ihrer Projektkonfiguration generieren

Der Befehl erkennt automatisch die ausgewählten Plattformen (iOS, Android, macOS, Windows, Web) und konfiguriert sie entsprechend.

Plattformspezifische Konfiguration

Für Firebase sind höhere Mindestversionen erforderlich als für Flutter standardmäßig. Außerdem ist Netzwerkzugriff erforderlich, um mit Vertex AI auf Firebase-Servern zu kommunizieren.

macOS-Berechtigungen konfigurieren

Unter macOS müssen Sie den Netzwerkzugriff in den Berechtigungen Ihrer App aktivieren:

  1. Öffnen Sie macos/Runner/DebugProfile.entitlements und fügen Sie Folgendes hinzu:

macos/Runner/DebugProfile.entitlements

<key>com.apple.security.network.client</key>
<true/>
  1. Öffnen Sie auch macos/Runner/Release.entitlements und fügen Sie denselben Eintrag hinzu.
  2. Aktualisieren Sie die Mindestversion von macOS oben in macos/Podfile:

macos/Podfile

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

iOS-Berechtigungen konfigurieren

Aktualisieren Sie für iOS die Mindestversion oben in ios/Podfile:

ios/Podfile

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

Android-Einstellungen konfigurieren

Aktualisieren Sie android/app/build.gradle.kts auf Android-Geräten:

android/app/build.gradle.kts

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

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

Gemini-Modellanbieter erstellen

Jetzt erstellen Sie die Riverpod-Anbieter für Firebase und Gemini. So erstellen Sie eine neue Datei 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();
}

Diese Datei definiert die Grundlage für drei wichtige Anbieter. Diese Anbieter werden von den Riverpod-Codegeneratoren generiert, wenn Sie dart run build_runner ausführen.

  1. firebaseAppProvider: Firebase wird mit Ihrer Projektkonfiguration initialisiert.
  2. geminiModelProvider: Erstellt eine Instanz eines generativen Gemini-Modells
  3. chatSessionProvider: Erstellt und verwaltet eine Chatsitzung mit dem Gemini-Modell

Die Anmerkung keepAlive: true in der Chatsitzung sorgt dafür, dass sie während des gesamten App-Lebenszyklus erhalten bleibt und der Unterhaltungskontext erhalten bleibt.

Gemini Chat-Dienst implementieren

Erstellen Sie eine neue Datei lib/services/gemini_chat_service.dart, um den Chatdienst zu implementieren:

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);

Dieser Dienst:

  1. Nimmt Nutzernachrichten entgegen und sendet sie an die Gemini API
  2. Aktualisiert die Chatoberfläche mit Antworten des Modells
  3. Protokolliert alle Kommunikation, um den tatsächlichen LLM-Ablauf besser nachvollziehen zu können
  4. Fehler werden mit entsprechendem Nutzerfeedback behandelt

Hinweis:Das Log-Fenster sieht jetzt fast identisch mit dem Chatfenster aus. Das Protokoll wird interessanter, sobald Sie Funktionsaufrufe und dann Streamingantworten einführen.

Riverpod-Code generieren

Führen Sie den Befehl „build runner“ aus, um den erforderlichen Riverpod-Code zu generieren:

dart run build_runner build --delete-conflicting-outputs

Dadurch werden die .g.dart-Dateien erstellt, die für die Funktion von Riverpod erforderlich sind.

Datei „main.dart“ aktualisieren

Aktualisieren Sie Ihre lib/main.dart-Datei, um den neuen Gemini-Chatdienst zu verwenden:

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),
     
),
   
);
 
}
}

Die wichtigsten Änderungen in diesem Update:

  1. Echo-Dienst durch den Gemini API-basierten Chatdienst ersetzen
  2. Lade- und Fehlerbildschirme mit dem AsyncValue-Muster von Riverpod und der when-Methode hinzufügen
  3. UI über den sendMessage-Callback mit dem neuen Chatdienst verbinden

Anwendung ausführen

Führen Sie die App mit dem folgenden Befehl aus:

flutter run -d DEVICE

Ersetzen Sie DEVICE durch das Zielgerät, z. B. macos, windows, chrome oder eine Geräte-ID.

Screenshot der Colorist App, in dem das Gemini-LLM auf eine Anfrage nach einer sonnengelben Farbe antwortet

Wenn Sie jetzt eine Nachricht eingeben, wird sie an die Gemini API gesendet und Sie erhalten eine Antwort vom LLM anstelle eines Echos. Im Logbereich werden die Interaktionen mit der API angezeigt.

LLM-Kommunikation

Sehen wir uns an, was passiert, wenn Sie mit der Gemini API kommunizieren:

Der Kommunikationsablauf

  1. Nutzerinput: Der Nutzer gibt Text in die Chatoberfläche ein.
  2. Anfrageformatierung: Die App formatiert den Text als Content-Objekt für die Gemini API.
  3. API-Kommunikation: Der Text wird über Vertex AI in Firebase an die Gemini API gesendet.
  4. LLM-Verarbeitung: Das Gemini-Modell verarbeitet den Text und generiert eine Antwort.
  5. Antwortverarbeitung: Die App empfängt die Antwort und aktualisiert die Benutzeroberfläche.
  6. Logging: Alle Kommunikation wird aus Transparenzgründen protokolliert.

Chatsitzungen und Unterhaltungskontext

Die Gemini-Chatsitzung behält den Kontext zwischen den Nachrichten bei, sodass Konversationsinteraktionen möglich sind. Das bedeutet, dass das LLM frühere Unterhaltungen in der aktuellen Sitzung „in Erinnerung behält“, was kohärentere Unterhaltungen ermöglicht.

Die Anmerkung keepAlive: true für Ihren Chatsitzungsanbieter sorgt dafür, dass dieser Kontext während des gesamten Lebenszyklus der App erhalten bleibt. Dieser persistente Kontext ist entscheidend, um einen natürlichen Gesprächsfluss mit dem LLM aufrechtzuerhalten.

Nächste Schritte

Sie können der Gemini API derzeit alles fragen, da es keine Einschränkungen gibt, auf was sie reagieren soll. Sie könnten sie beispielsweise um eine Zusammenfassung der Rosenkriege bitten, was nichts mit dem Zweck Ihrer Farb-App zu tun hat.

Im nächsten Schritt erstellen Sie einen Systemprompt, mit dem Gemini Farbbeschreibungen effektiver interpretieren kann. Hier erfahren Sie, wie Sie das Verhalten eines LLM für anwendungsspezifische Anforderungen anpassen und seine Funktionen auf die Domain Ihrer App ausrichten.

Fehlerbehebung

Probleme mit der Firebase-Konfiguration

Wenn bei der Firebase-Initialisierung Fehler auftreten:

  • Prüfen, ob die firebase_options.dart-Datei korrekt generiert wurde
  • Prüfen, ob Sie für den Vertex AI-Zugriff ein Upgrade auf den Blaze-Tarif durchgeführt haben

Fehler beim API-Zugriff

Wenn Sie beim Zugriff auf die Gemini API Fehler erhalten:

  • Prüfen, ob die Abrechnung für Ihr Firebase-Projekt richtig eingerichtet ist
  • Prüfen, ob Vertex AI und die Cloud AI API in Ihrem Firebase-Projekt aktiviert sind
  • Netzwerkverbindung und Firewalleinstellungen prüfen
  • Prüfen Sie, ob der Modellname (gemini-2.0-flash) korrekt und verfügbar ist.

Probleme mit dem Konversationskontext

Wenn Sie feststellen, dass Gemini den bisherigen Kontext aus dem Chat nicht merkt, gehen Sie so vor:

  • Prüfen Sie, ob die Funktion chatSession mit @Riverpod(keepAlive: true) kommentiert ist.
  • Prüfen, ob Sie dieselbe Chatsitzung für alle Nachrichtenübermittlungen verwenden
  • Prüfen, ob die Chatsitzung ordnungsgemäß initialisiert wurde, bevor Nachrichten gesendet werden

Plattformspezifische Probleme

Bei plattformspezifischen Problemen:

  • iOS/macOS: Prüfen, ob die richtigen Berechtigungen festgelegt und Mindestversionen konfiguriert sind
  • Android: Prüfen, ob die Mindest-SDK-Version korrekt festgelegt ist
  • Plattformspezifische Fehlermeldungen in der Konsole prüfen

Wichtige Konzepte

  • Firebase in einer Flutter-Anwendung einrichten
  • Vertex AI in Firebase für den Zugriff auf Gemini konfigurieren
  • Riverpod-Anbieter für asynchrone Dienste erstellen
  • Chatdienst implementieren, der mit einem LLM kommuniziert
  • Umgang mit asynchronen API-Status (Laden, Fehler, Daten)
  • LLM-Kommunikationsablauf und Chatsitzungen

4. Effektive Prompts für Farbbeschreibungen

In diesem Schritt erstellen und implementieren Sie einen Systemprompt, der Gemini bei der Interpretation von Farbbeschreibungen unterstützt. Systemprompts sind eine leistungsstarke Möglichkeit, das LLM-Verhalten für bestimmte Aufgaben anzupassen, ohne den Code zu ändern.

Was Sie in diesem Schritt lernen

  • Systemprompts und ihre Bedeutung in LLM-Anwendungen
  • Effektive Prompts für domainspezifische Aufgaben erstellen
  • Systemaufforderungen in einer Flutter-App laden und verwenden
  • LLMs dazu anleiten, einheitlich formatierte Antworten zu geben
  • Testen, wie sich Systemaufforderungen auf das LLM-Verhalten auswirken

Systemaufforderungen

Bevor wir uns mit der Implementierung befassen, sollten wir uns ansehen, was Systemaufforderungen sind und warum sie wichtig sind:

Was sind Systemaufforderungen?

Ein Systemprompt ist eine spezielle Art von Anweisung, die an ein LLM gegeben wird und den Kontext, die Verhaltensrichtlinien und die Erwartungen an seine Antworten festlegt. Im Gegensatz zu Nutzernachrichten haben Systemaufforderungen folgende Eigenschaften:

  • Rolle und Persona des LLM festlegen
  • Fachwissen oder Fähigkeiten definieren
  • Formatierungsanleitung bereitstellen
  • Einschränkungen für Antworten festlegen
  • Beschreiben, wie mit verschiedenen Szenarien umgegangen wird

Ein Systemprompt ist sozusagen die „Jobbeschreibung“ für das LLM. Er gibt dem Modell vor, wie es sich während der Unterhaltung verhalten soll.

Warum sind Systemaufforderungen wichtig?

Systemprompts sind entscheidend für konsistente, nützliche LLM-Interaktionen, da sie:

  1. Konsistenz gewährleisten: Das Modell dazu anleiten, Antworten in einem einheitlichen Format zu liefern
  2. Relevanz verbessern: Richten Sie das Modell auf Ihre spezifische Domain (in Ihrem Fall Farben) aus.
  3. Grenzen festlegen: Definieren Sie, was das Modell tun soll und was nicht.
  4. Nutzerfreundlichkeit verbessern: Ein natürlicheres, hilfreicheres Interaktionsmuster schaffen
  5. Nachbearbeitung reduzieren: Antworten in Formaten erhalten, die sich leichter analysieren oder anzeigen lassen

Für Ihre Colorist-App benötigen Sie den LLM, um Farbbeschreibungen einheitlich zu interpretieren und RGB-Werte in einem bestimmten Format bereitzustellen.

Systemprompt-Asset erstellen

Erstellen Sie zuerst eine Systemaufforderungsdatei, die zur Laufzeit geladen wird. So können Sie den Prompt ändern, ohne Ihre App neu zu kompilieren.

Erstellen Sie eine neue Datei vom Typ assets/system_prompt.md mit folgendem Inhalt:

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

Struktur von Systemprompts

Sehen wir uns an, was dieser Prompt bewirkt:

  1. Rollendefinition: Das LLM wird als „Farbexpertenassistent“ definiert.
  2. Aufgabenbeschreibung: Die Hauptaufgabe besteht darin, Farbbeschreibungen in RGB-Werte umzuwandeln.
  3. Antwortformat: Gibt genau an, wie RGB-Werte formatiert werden sollen, um für Einheitlichkeit zu sorgen.
  4. Beispiel für einen Austausch: Bietet ein konkretes Beispiel für das erwartete Interaktionsmuster.
  5. Umgang mit Grenzfällen: Hier erfahren Sie, wie Sie mit unklaren Beschreibungen umgehen.
  6. Einschränkungen und Richtlinien: Hiermit werden Grenzen festgelegt, z. B. dass RGB-Werte zwischen 0,0 und 1,0 liegen müssen.

Dieser strukturierte Ansatz sorgt dafür, dass die Antworten des LLM konsistent, informativ und so formatiert sind, dass sie sich leicht parsen lassen, wenn Sie die RGB-Werte programmatisch extrahieren möchten.

pubspec.yaml aktualisieren

Aktualisieren Sie nun den unteren Teil Ihrer pubspec.yaml, um das Assets-Verzeichnis aufzunehmen:

pubspec.yaml

flutter:
  uses-material-design: true

  assets:
    - assets/

Führen Sie flutter pub get aus, um das Asset-Bundle zu aktualisieren.

Anbieter für Systemprompts erstellen

Erstellen Sie eine neue Datei vom Typ lib/providers/system_prompt.dart, um den Systemprompt zu laden:

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');

Dieser Anbieter verwendet das Asset-Ladesystem von Flutter, um die Promptdatei zur Laufzeit zu lesen.

Gemini-Modellanbieter aktualisieren

Ändern Sie nun die Datei lib/providers/gemini.dart so, dass sie die Systemaufforderung enthält:

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();
}

Die wichtigste Änderung besteht darin, dass beim Erstellen des generativen Modells systemInstruction: Content.system(systemPrompt) hinzugefügt wird. Dadurch wird Gemini angewiesen, Ihre Anleitung als Systemprompt für alle Interaktionen in dieser Chatsitzung zu verwenden.

Riverpod-Code generieren

Führen Sie den Befehl „build runner“ aus, um den erforderlichen Riverpod-Code zu generieren:

dart run build_runner build --delete-conflicting-outputs

Anwendung ausführen und testen

Führen Sie nun Ihre Anwendung aus:

flutter run -d DEVICE

Screenshot der Colorist App, in dem das Gemini-LLM mit einer Antwort in Form einer Person für eine App zur Farbauswahl antwortet

Testen Sie es mit verschiedenen Farbbeschreibungen:

  • „Ich hätte gerne ein Hemd in Himmelblau.“
  • „Zeig mir ein Waldgrün“
  • „Leuchtendes Orange wie ein Sonnenuntergang“
  • „Ich möchte die Farbe von frischem Lavendel“
  • „Zeig mir etwas in einem tiefen Ozeanblau“

Sie sollten feststellen, dass Gemini jetzt mit gesprächsorientierten Erklärungen zu den Farben und einheitlich formatierten RGB-Werten antwortet. Der Systemprompt hat das LLM effektiv dazu angeleitet, die gewünschten Antworten zu liefern.

Fragen Sie es auch nach Inhalten außerhalb des Kontexts von Farben. Sagen wir, die Hauptursachen der Rosenkriege. Sie sollten einen Unterschied zum vorherigen Schritt feststellen.

Die Bedeutung von Prompt Engineering für spezielle Aufgaben

Systemprompts sind sowohl Kunst als auch Wissenschaft. Sie sind ein wichtiger Bestandteil der LLM-Integration und können sich erheblich auf die Nützlichkeit des Modells für Ihre spezifische Anwendung auswirken. Sie haben hier eine Art Prompt-Engineering durchgeführt, also Anweisungen angepasst, damit sich das Modell so verhält, wie es für Ihre Anwendung erforderlich ist.

Effektives Prompt Engineering umfasst:

  1. Klare Rollendefinition: Festlegen des Zwecks der LLM
  2. Ausdrückliche Anweisungen: Sie geben genau an, wie das LLM reagieren soll.
  3. Konkrete Beispiele: Zeigen, anstatt nur zu sagen, wie gute Antworten aussehen
  4. Behandlung von Sonderfällen: Anweisen des LLM, wie mit zweideutigen Szenarien umgegangen werden soll
  5. Formatierungsspezifikationen: Sorgen dafür, dass Antworten einheitlich und nutzerfreundlich strukturiert sind

Der von Ihnen erstellte Systemprompt wandelt die generischen Funktionen von Gemini in einen speziellen Assistenten für die Farbinterpretation um, der Antworten liefert, die speziell auf die Anforderungen Ihrer Anwendung zugeschnitten sind. Dies ist ein leistungsstarkes Muster, das Sie auf viele verschiedene Bereiche und Aufgaben anwenden können.

Nächste Schritte

Im nächsten Schritt bauen Sie auf dieser Grundlage auf, indem Sie Funktionsdeklarationen hinzufügen. So kann der LLM nicht nur RGB-Werte vorschlagen, sondern auch Funktionen in Ihrer App aufrufen, um die Farbe direkt festzulegen. Das zeigt, wie LLMs die Lücke zwischen natürlicher Sprache und konkreten Anwendungsfunktionen schließen können.

Fehlerbehebung

Probleme beim Laden von Assets

Wenn beim Laden des Systemprompts Fehler auftreten:

  • Prüfen Sie, ob das Assets-Verzeichnis in pubspec.yaml korrekt aufgeführt ist.
  • Prüfen Sie, ob der Pfad in rootBundle.loadString() mit dem Speicherort der Datei übereinstimmt.
  • Führen Sie flutter clean und dann flutter pub get aus, um das Asset-Bundle zu aktualisieren.

Inkonsistente Antworten

Wenn die LLM Ihre Formatierungsanleitung nicht konsequent befolgt:

  • Formatanforderungen im Systemprompt klarer formulieren
  • Fügen Sie weitere Beispiele hinzu, um das erwartete Muster zu veranschaulichen.
  • Das angeforderte Format muss für das Modell geeignet sein.

API-Ratenbegrenzung

Wenn Fehler im Zusammenhang mit der Ratenbegrenzung auftreten, gehen Sie so vor:

  • Der Vertex AI-Dienst unterliegt Nutzungsbeschränkungen
  • Implementieren Sie eine Wiederholungslogik mit exponentiellem Backoff.
  • Kontingentprobleme in der Firebase Console prüfen

Wichtige Konzepte

  • Rolle und Bedeutung von Systemprompts in LLM-Anwendungen
  • Effektive Prompts mit klaren Anweisungen, Beispielen und Einschränkungen erstellen
  • Systemaufforderungen in einer Flutter-Anwendung laden und verwenden
  • LLM-Verhalten für domainspezifische Aufgaben steuern
  • Prompt-Engineering zur Gestaltung von LLM-Antworten

In diesem Schritt wird gezeigt, wie Sie das LLM-Verhalten erheblich anpassen können, ohne den Code zu ändern. Dazu müssen Sie lediglich klare Anweisungen in der Systemaufforderung angeben.

5. Funktionsdeklarationen für LLM-Tools

In diesem Schritt aktivieren Sie Gemini für Aktionen in Ihrer App, indem Sie Funktionsdeklarationen implementieren. Mit dieser leistungsstarken Funktion kann der LLM nicht nur RGB-Werte vorschlagen, sondern sie auch über spezielle Toolaufrufe in der Benutzeroberfläche Ihrer App festlegen. Im nächsten Schritt sehen Sie jedoch, dass die LLM-Anfragen in der Flutter-App ausgeführt werden.

Was Sie in diesem Schritt lernen

  • LLM-Funktionsaufrufe und ihre Vorteile für Flutter-Anwendungen
  • Schemabasierte Funktionsdeklarationen für Gemini definieren
  • Funktionsdeklarationen in Ihr Gemini-Modell einbinden
  • Systemaufforderung aktualisieren, um Toolfunktionen zu nutzen

Funktionsaufrufe

Bevor wir Funktionsdeklarationen implementieren, sollten wir uns ansehen, was sie sind und warum sie nützlich sind:

Was ist ein Funktionsaufruf?

Der Funktionsaufruf (manchmal auch als „Tool-Nutzung“ bezeichnet) ist eine Funktion, mit der ein LLM Folgendes tun kann:

  1. Erkennen, wenn eine Nutzeranfrage von der Ausführung einer bestimmten Funktion profitieren würde
  2. Ein strukturiertes JSON-Objekt mit den für diese Funktion erforderlichen Parametern generieren
  3. Die Funktion mit diesen Parametern in Ihrer Anwendung ausführen
  4. Ergebnis der Funktion empfangen und in die Antwort einbinden

Anstatt nur zu beschreiben, was zu tun ist, kann das LLM durch Funktionsaufrufe konkrete Aktionen in Ihrer Anwendung auslösen.

Warum ist der Funktionsaufruf für Flutter-Apps wichtig?

Funktionsaufrufe schaffen eine leistungsstarke Brücke zwischen natürlicher Sprache und Anwendungsfunktionen:

  1. Direkte Aktion: Nutzer können in natürlicher Sprache beschreiben, was sie möchten, und die App reagiert mit konkreten Aktionen.
  2. Strukturierte Ausgabe: Das LLM generiert saubere, strukturierte Daten anstelle von Text, der geparst werden muss.
  3. Komplexe Vorgänge: Ermöglicht es dem LLM, auf externe Daten zuzugreifen, Berechnungen durchzuführen oder den Anwendungsstatus zu ändern.
  4. Verbesserte Nutzerfreundlichkeit: Die nahtlose Integration von Unterhaltungen und Funktionen

In Ihrer Colorist-App können Nutzer durch Funktionsaufrufe sagen: „Ich möchte ein dunkelgrünes.“ Die Benutzeroberfläche wird dann sofort mit dieser Farbe aktualisiert, ohne dass RGB-Werte aus Text geparst werden müssen.

Funktionsdeklarationen definieren

Erstellen Sie eine neue Datei lib/services/gemini_tools.dart, um Ihre Funktionsdeklarationen zu definieren:

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);

Funktionsdeklarationen

Sehen wir uns an, was dieser Code bewirkt:

  1. Funktionsnamen: Sie nennen Ihre Funktion set_color, um ihren Zweck klar zu kennzeichnen.
  2. Funktionsbeschreibung: Sie geben eine klare Beschreibung an, anhand derer das LLM weiß, wann es die Funktion verwenden soll.
  3. Parameterdefinitionen: Sie definieren strukturierte Parameter mit eigenen Beschreibungen:
    • red: Die rote Komponente von RGB, angegeben als Zahl zwischen 0,0 und 1,0
    • green: Die grüne Komponente des RGB-Farbmodells, angegeben als Zahl zwischen 0,0 und 1,0
    • blue: Die blaue Komponente von RGB, angegeben als Zahl zwischen 0,0 und 1,0
  4. Schematypen: Mit Schema.number() geben Sie an, dass es sich um numerische Werte handelt.
  5. Tools-Sammlung: Sie erstellen eine Liste von Tools, die Ihre Funktionsdeklaration enthält.

Dieser strukturierte Ansatz hilft dem Gemini-LLM, Folgendes zu verstehen:

  • Wann diese Funktion aufgerufen werden soll
  • Welche Parameter angegeben werden müssen
  • Welche Einschränkungen gelten für diese Parameter (z. B. der Wertebereich)

Gemini-Modellanbieter aktualisieren

Ändern Sie nun Ihre lib/providers/gemini.dart-Datei so, dass die Funktionsdeklarationen beim Initialisieren des Gemini-Modells enthalten sind:

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();
}

Die wichtigste Änderung besteht darin, dass beim Erstellen des generativen Modells der Parameter tools: geminiTools.tools hinzugefügt wird. So wird Gemini über die Funktionen informiert, die es aufrufen kann.

Systemaufforderung aktualisieren

Jetzt müssen Sie die Systemaufforderung ändern, um dem LLM die Verwendung des neuen set_color-Tools zuzuweisen. Aktualisieren Sie 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

Die wichtigsten Änderungen an der Systemaufforderung sind:

  1. Tool-Einführung: Anstatt nach formatierten RGB-Werten zu fragen, informieren Sie den LLM jetzt über das Tool set_color.
  2. Geänderter Prozess: Sie ändern Schritt 3 von „Werte in der Antwort formatieren“ in „Werte mit dem Tool festlegen“.
  3. Aktualisiertes Beispiel: Sie zeigen, wie die Antwort einen Toolaufruf anstelle von formatiertem Text enthalten sollte.
  4. Anforderungen an die Formatierung entfernt: Da Sie strukturierte Funktionsaufrufe verwenden, ist kein bestimmtes Textformat mehr erforderlich.

Mit diesem aktualisierten Prompt wird das LLM angewiesen, Funktionsaufrufe zu verwenden, anstatt nur RGB-Werte in Textform anzugeben.

Riverpod-Code generieren

Führen Sie den Befehl „build runner“ aus, um den erforderlichen Riverpod-Code zu generieren:

dart run build_runner build --delete-conflicting-outputs

Anwendung ausführen

In diesem Fall generiert Gemini Inhalte, in denen versucht wird, Funktionsaufrufe zu verwenden, Sie haben jedoch noch keine Handler für die Funktionsaufrufe implementiert. Wenn Sie die App ausführen und eine Farbe beschreiben, reagiert Gemini, als würde ein Tool aufgerufen. Erst im nächsten Schritt werden Farbänderungen in der Benutzeroberfläche sichtbar.

Führen Sie die Anwendung aus:

flutter run -d DEVICE

Screenshot der Colorist App, in dem zu sehen ist, dass der Gemini-LLM mit einer teilweisen Antwort antwortet

Beschreiben Sie eine Farbe wie „tiefes Ozeanblau“ oder „dunkelgrün“ und beobachten Sie die Antworten. Der LLM versucht, die oben definierten Funktionen aufzurufen, aber Ihr Code erkennt noch keine Funktionsaufrufe.

Funktionsaufruf

Sehen wir uns an, was passiert, wenn Gemini Funktionsaufrufe verwendet:

  1. Funktionsauswahl: Das LLM entscheidet basierend auf der Anfrage des Nutzers, ob ein Funktionsaufruf hilfreich wäre.
  2. Parametergenerierung: Der LLM generiert Parameterwerte, die zum Schema der Funktion passen.
  3. Format für Funktionsaufrufe: Das LLM sendet in seiner Antwort ein strukturiertes Funktionsaufrufobjekt.
  4. Anwendungsverwaltung: Ihre App empfängt diesen Aufruf und führt die entsprechende Funktion aus (im nächsten Schritt implementiert).
  5. Antwortintegration: Bei Unterhaltungen mit mehreren Runden erwartet das LLM, dass das Ergebnis der Funktion zurückgegeben wird.

Im aktuellen Zustand Ihrer App werden die ersten drei Schritte ausgeführt, aber Sie haben noch nicht Schritt 4 oder 5 (Verarbeitung der Funktionsaufrufe) implementiert. Das tun Sie im nächsten Schritt.

Technische Details: So entscheidet Gemini, wann Funktionen verwendet werden

Gemini trifft intelligente Entscheidungen darüber, wann Funktionen verwendet werden sollen, basierend auf:

  1. Nutzerabsicht: Gibt an, ob die Anfrage des Nutzers am besten durch eine Funktion beantwortet werden kann
  2. Funktionsrelevanz: Wie gut die verfügbaren Funktionen zur Aufgabe passen
  3. Verfügbarkeit von Parametern: Gibt an, ob Parameterwerte mit Sicherheit ermittelt werden können.
  4. Systemanweisungen: Informationen zur Verwendung von Funktionen, die vom System angezeigt werden

Durch klare Funktionsdeklarationen und Systemanweisungen haben Sie Gemini so eingerichtet, dass Anfragen zur Farbbeschreibung als Aufruf der Funktion set_color erkannt werden.

Nächste Schritte

Im nächsten Schritt implementieren Sie Handler für die Funktionsaufrufe, die von Gemini stammen. Damit ist der Kreis geschlossen und Nutzerbeschreibungen können über die Funktionsaufrufe des LLM tatsächliche Farbänderungen in der Benutzeroberfläche auslösen.

Fehlerbehebung

Probleme mit Funktionsdeklarationen

Wenn Fehler bei Funktionsdeklarationen auftreten:

  • Prüfen, ob die Parameternamen und ‑typen den Erwartungen entsprechen
  • Prüfen Sie, ob der Funktionsname klar und aussagekräftig ist.
  • Der Zweck der Funktion muss in der Funktionsbeschreibung korrekt erläutert werden.

Probleme mit Systemaufforderungen

Wenn der LLM nicht versucht, die Funktion zu verwenden:

  • Prüfen Sie, ob die Systemaufforderung den LLM klar anweist, das set_color-Tool zu verwenden.
  • Prüfen, ob im Beispiel im Systemprompt die Funktion verwendet wird
  • Erläutern Sie die Anleitung zur Verwendung des Tools genauer.

Allgemeine Probleme

Wenn andere Probleme auftreten:

  • Konsole auf Fehler bei Funktionsdeklarationen prüfen
  • Prüfen, ob die Tools richtig an das Modell übergeben werden
  • Prüfen, ob der gesamte von Riverpod generierte Code auf dem neuesten Stand ist

Wichtige Konzepte

  • Funktionsdeklarationen definieren, um die LLM-Funktionen in Flutter-Apps zu erweitern
  • Parameterschemata für die Erhebung strukturierter Daten erstellen
  • Funktionsdeklarationen in das Gemini-Modell einbinden
  • Systemaufforderungen aktualisieren, um die Nutzung der Funktion zu fördern
  • So wählen und rufen LLMs Funktionen aus

In diesem Schritt wird veranschaulicht, wie LLMs die Lücke zwischen natürlicher Spracheingaben und strukturierten Funktionsaufrufen schließen können. So wird die Grundlage für eine nahtlose Integration von Konversations- und Anwendungsfunktionen geschaffen.

6. Tool-Handhabung implementieren

In diesem Schritt implementieren Sie Handler für die Funktionsaufrufe von Gemini. So schließt sich der Kommunikationskreis zwischen Eingaben in natürlicher Sprache und konkreten Anwendungsfunktionen. So kann der LLM Ihre Benutzeroberfläche direkt anhand von Nutzerbeschreibungen bearbeiten.

Was Sie in diesem Schritt lernen

  • Die vollständige Pipeline für Funktionsaufrufe in LLM-Anwendungen
  • Funktionsaufrufe aus Gemini in einer Flutter-Anwendung verarbeiten
  • Funktionshandler implementieren, die den Anwendungsstatus ändern
  • Funktionsantworten verarbeiten und Ergebnisse an das LLM zurückgeben
  • Einen vollständigen Kommunikationsablauf zwischen LLM und UI erstellen
  • Funktionaufrufe und ‑antworten protokollieren

Informationen zur Pipeline für Funktionsaufrufe

Bevor wir uns mit der Implementierung befassen, sehen wir uns die vollständige Funktionaufruf-Pipeline an:

Der End-to-End-Workflow

  1. Nutzerinput: Der Nutzer beschreibt eine Farbe in natürlicher Sprache (z.B. „dunkelgrün“)
  2. LLM-Verarbeitung: Gemini analysiert die Beschreibung und entscheidet, die Funktion set_color aufzurufen.
  3. Generierung von Funktionsaufrufen: Gemini erstellt ein strukturiertes JSON mit Parametern (Rot-, Grün- und Blauwerte).
  4. Empfang des Funktionsaufrufs: Ihre App empfängt diese strukturierten Daten von Gemini.
  5. Funktionsausführung: Ihre App führt die Funktion mit den angegebenen Parametern aus.
  6. Statusaktualisierung: Die Funktion aktualisiert den Status Ihrer App (Änderung der angezeigten Farbe)
  7. Antwortgenerierung: Ihre Funktion gibt Ergebnisse an das LLM zurück.
  8. Einbindung der Antwort: Das LLM fügt diese Ergebnisse in die endgültige Antwort ein.
  9. UI-Aktualisierung: Die Benutzeroberfläche reagiert auf den Statuswechsel und zeigt die neue Farbe an.

Der vollständige Kommunikationszyklus ist für die ordnungsgemäße LLM-Integration unerlässlich. Wenn ein LLM einen Funktionsaufruf ausführt, sendet er nicht einfach die Anfrage und fährt fort. Stattdessen wartet er, bis Ihre Anwendung die Funktion ausgeführt und Ergebnisse zurückgegeben hat. Das LLM verwendet diese Ergebnisse dann, um seine endgültige Antwort zu formulieren und einen natürlichen Gesprächsfluss zu schaffen, in dem die ausgeführten Aktionen berücksichtigt werden.

Funktions-Handler implementieren

Aktualisieren wir die lib/services/gemini_tools.dart-Datei, um Handler für Funktionsaufrufe hinzuzufügen:

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);

Funktions-Handler

Sehen wir uns an, was diese Funktions-Handler tun:

  1. handleFunctionCall: Ein zentraler Dispatcher, der:
    • Der Funktionsaufruf wird im Protokollbereich protokolliert.
    • Leitet basierend auf dem Funktionsnamen an den entsprechenden Handler weiter
    • Gibt eine strukturierte Antwort zurück, die an das LLM gesendet wird
  2. handleSetColor: Der spezifische Handler für Ihre set_color-Funktion, der:
    • Extrahiert RGB-Werte aus der Argumentkarte
    • Sie werden in die erwarteten Typen (Doppel) konvertiert.
    • Aktualisiert den Farbstatus der Anwendung mit der colorStateNotifier
    • Erstellt eine strukturierte Antwort mit Erfolgsstatus und aktuellen Farbinformationen
    • Erfasst die Funktionsergebnisse zur Fehlerbehebung
  3. handleUnknownFunction: Ein Fallback-Handler für unbekannte Funktionen, der:
    • Eine Warnung zur nicht unterstützten Funktion wird protokolliert.
    • Gibt eine Fehlerantwort an das LLM zurück

Die Funktion handleSetColor ist besonders wichtig, da sie die Lücke zwischen dem Verständnis der natürlichen Sprache des LLM und konkreten UI-Änderungen schließt.

Gemini-Chatdienst aktualisieren, um Funktionsaufrufe und ‑antworten zu verarbeiten

Aktualisieren Sie nun die lib/services/gemini_chat_service.dart-Datei, um Funktionsaufrufe aus den LLM-Antworten zu verarbeiten und die Ergebnisse an das LLM zurückzugeben:

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);

Der Kommunikationsablauf

Der wichtigste Zusatz ist die vollständige Verarbeitung von Funktionsaufrufen und ‑antworten:

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);
 
}
}

Dieser Code:

  1. Prüft, ob die LLM-Antwort Funktionsaufrufe enthält
  2. Ruft für jeden Funktionsaufruf die handleFunctionCall-Methode mit dem Funktionsnamen und den Argumenten auf
  3. Erfasst die Ergebnisse jedes Funktionsaufrufs.
  4. Diese Ergebnisse werden über Content.functionResponses an das LLM zurückgesendet.
  5. Verarbeitet die Antwort des LLM auf die Funktionsergebnisse
  6. Die UI wird mit dem endgültigen Antworttext aktualisiert.

Dadurch entsteht ein Roundtrip-Vorgang:

  • Nutzer → LLM: Farbe anfordern
  • LLM → App: Funktionsaufrufe mit Parametern
  • App → Nutzer: Neue Farbe wird angezeigt
  • App → LLM: Funktionsergebnisse
  • LLM → Nutzer: Endgültige Antwort mit Funktionsergebnissen

Riverpod-Code generieren

Führen Sie den Befehl „build runner“ aus, um den erforderlichen Riverpod-Code zu generieren:

dart run build_runner build --delete-conflicting-outputs

Vollständigen Ablauf ausführen und testen

Führen Sie nun Ihre Anwendung aus:

flutter run -d DEVICE

Screenshot der Colorist App, in dem zu sehen ist, dass das Gemini-LLM mit einem Funktionsaufruf antwortet

Probieren Sie verschiedene Farbbeschreibungen aus:

  • „Ich möchte ein tiefes Karminrot“
  • „Zeig mir ein beruhigendes Himmelblau“
  • „Welche Farbe hat frische Minze?“
  • „Ich möchte ein warmes Orange sehen, wie bei einem Sonnenuntergang“
  • „Machen Sie es zu einem satten Königspurpur.“

Jetzt sollten Sie Folgendes sehen:

  1. Ihre Nachricht wird in der Chatoberfläche angezeigt
  2. Antwort von Gemini im Chat
  3. Im Logbereich protokollierte Funktionsaufrufe
  4. Funktionergebnisse werden sofort nach
  5. Das Farbrechteck wird aktualisiert, um die beschriebene Farbe anzuzeigen.
  6. Die RGB-Werte werden aktualisiert, um die Komponenten der neuen Farbe anzuzeigen.
  7. Die finale Antwort von Gemini wird angezeigt, oft mit einem Kommentar zur festgelegten Farbe

Im Log-Bereich erhalten Sie einen Einblick in die Vorgänge im Hintergrund. Sie sehen hier Folgendes:

  • Die genauen Funktionsaufrufe, die von Gemini ausgeführt werden
  • Die Parameter, die für jeden RGB-Wert ausgewählt werden
  • Die Ergebnisse, die von Ihrer Funktion zurückgegeben werden
  • Die Folgeantworten von Gemini

Benachrichtigung zum Farbstatus

Die colorStateNotifier, mit der Sie Farben aktualisieren, ist Teil des colorist_ui-Pakets. Folgendes wird verwaltet:

  • Die aktuelle Farbe, die in der Benutzeroberfläche angezeigt wird
  • Farbverlauf (letzte 10 Farben)
  • Benachrichtigung über Statusänderungen bei UI-Komponenten

Wenn Sie updateColor mit neuen RGB-Werten aufrufen, geschieht Folgendes:

  1. Erstellt ein neues ColorData-Objekt mit den angegebenen Werten.
  2. Aktualisiert die aktuelle Farbe im App-Status
  3. Fügen Sie die Farbe dem Verlauf hinzu.
  4. Lösen UI-Updates über die Zustandsverwaltung von Riverpod aus

Die UI-Komponenten im colorist_ui-Paket überwachen diesen Status und aktualisieren sich automatisch, wenn er sich ändert. So entsteht eine responsive Benutzeroberfläche.

Fehlerbehandlung

Ihre Implementierung umfasst eine robuste Fehlerbehandlung:

  1. Try-Catch-Block: Umschließt alle LLM-Interaktionen, um Ausnahmen abzufangen
  2. Fehlerprotokollierung: Fehler werden im Protokollbereich mit Stack-Traces protokolliert.
  3. Nutzerfeedback: Eine freundliche Fehlermeldung wird im Chat angezeigt.
  4. Statusbereinigung: Der Nachrichtenstatus wird abgeschlossen, auch wenn ein Fehler auftritt.

So bleibt die App stabil und gibt auch bei Problemen mit dem LLM-Dienst oder der Funktionsausführung entsprechendes Feedback.

Die Vorteile von Funktionsaufrufen für die Nutzererfahrung

Was Sie hier erreicht haben, zeigt, wie LLMs leistungsstarke natürliche Benutzeroberflächen erstellen können:

  1. Natürliche Sprache: Nutzer drücken ihre Absicht in der Alltagssprache aus.
  2. Intelligente Interpretation: Der LLM übersetzt vage Beschreibungen in präzise Werte.
  3. Direkte Manipulation: Die Benutzeroberfläche wird als Reaktion auf natürliche Sprache aktualisiert.
  4. Kontextbezogene Antworten: Der LLM liefert den Konversationskontext zu den Änderungen.
  5. Geringer kognitiver Aufwand: Nutzer müssen keine RGB-Werte oder Farbtheorie verstehen.

Dieses Muster, bei dem LLM-Funktionsaufrufe verwendet werden, um die Lücke zwischen natürlicher Sprache und UI-Aktionen zu schließen, kann auf unzählige andere Bereiche als die Farbauswahl ausgeweitet werden.

Nächste Schritte

Im nächsten Schritt verbessern Sie die Nutzerfreundlichkeit, indem Sie Streamingantworten implementieren. Anstatt auf die vollständige Antwort zu warten, verarbeiten Sie Textblöcke und Funktionsaufrufe, sobald sie empfangen werden. So entsteht eine responsivere und ansprechendere Anwendung.

Fehlerbehebung

Probleme mit Funktionsaufrufen

Wenn Gemini Ihre Funktionen nicht aufruft oder die Parameter falsch sind:

  • Prüfen, ob die Funktionsdeklaration mit der Beschreibung in der Systemaufforderung übereinstimmt
  • Prüfen, ob Parameternamen und ‑typen einheitlich sind
  • Achten Sie darauf, dass der Systemprompt den LLM ausdrücklich zur Verwendung des Tools auffordert.
  • Prüfen Sie, ob der Funktionsname in Ihrem Handler genau mit dem in der Deklaration übereinstimmt.
  • Im Protokollbereich finden Sie detaillierte Informationen zu Funktionsaufrufen.

Probleme mit Funktionsantworten

Wenn die Funktionsergebnisse nicht ordnungsgemäß an den LLM zurückgesendet werden:

  • Prüfen Sie, ob Ihre Funktion eine korrekt formatierte Map zurückgibt
  • Prüfen, ob Content.functionResponses richtig erstellt wird
  • Suchen Sie im Protokoll nach Fehlern im Zusammenhang mit Funktionsantworten.
  • Achten Sie darauf, dass Sie für die Antwort dieselbe Chatsitzung verwenden.

Probleme mit dem Farbdisplay

Wenn Farben nicht richtig angezeigt werden:

  • Achten Sie darauf, dass RGB-Werte korrekt in Doppelwerte konvertiert werden. LLM sendet sie möglicherweise als Ganzzahlen.
  • Prüfen Sie, ob die Werte im erwarteten Bereich (0,0 bis 1,0) liegen.
  • Prüfen, ob der Benachrichtigungs-Dienst für den Farbstatus richtig aufgerufen wird
  • Prüfen Sie im Protokoll, welche genauen Werte an die Funktion übergeben werden.

Allgemeine Probleme

Bei allgemeinen Problemen:

  • Protokolle auf Fehler oder Warnungen prüfen
  • Vertex AI in Firebase-Verbindung prüfen
  • Prüfen Sie, ob die Typen der Funktionsparameter übereinstimmen.
  • Prüfen, ob der gesamte von Riverpod generierte Code auf dem neuesten Stand ist

Wichtige Konzepte

  • Eine vollständige Funktion zum Aufrufen einer Pipeline in Flutter implementieren
  • Vollständige Kommunikation zwischen einem LLM und Ihrer Anwendung herstellen
  • Strukturierte Daten aus LLM-Antworten verarbeiten
  • Funktionsergebnisse zur Einbindung in Antworten an das LLM zurücksenden
  • Über den Logbereich Informationen zu LLM-Anwendungsinteraktionen abrufen
  • Natürliche Spracheingaben mit konkreten UI-Änderungen verknüpfen

Nachdem Sie diesen Schritt ausgeführt haben, zeigt Ihre App jetzt eines der leistungsstärksten Muster für die LLM-Integration: die Übersetzung von Eingaben in natürlicher Sprache in konkrete UI-Aktionen bei gleichzeitiger Aufrechterhaltung einer kohärenten Unterhaltung, in der diese Aktionen berücksichtigt werden. So entsteht eine intuitive, konversationelle Benutzeroberfläche, die für Nutzer magisch wirkt.

7. Streamingantworten für eine bessere Nutzerfreundlichkeit

In diesem Schritt verbessern Sie die Nutzerfreundlichkeit, indem Sie Streamingantworten von Gemini implementieren. Anstatt darauf zu warten, dass die gesamte Antwort generiert wird, verarbeiten Sie Textblöcke und Funktionsaufrufe, sobald sie empfangen werden. So entsteht eine responsivere und ansprechendere Anwendung.

In diesem Schritt geht es um:

  • Die Bedeutung des Streamings für LLM-gestützte Anwendungen
  • Streaming-LLM-Antworten in einer Flutter-Anwendung implementieren
  • Teiltextstücke verarbeiten, sobald sie von der API empfangen werden
  • Unterhaltungsstatus verwalten, um Nachrichtenkonflikte zu vermeiden
  • Funktionsaufrufe in Streamingantworten verarbeiten
  • Visuelle Indikatoren für in Bearbeitung befindliche Antworten erstellen

Warum Streaming für LLM-Anwendungen wichtig ist

Bevor wir mit der Implementierung beginnen, sollten wir uns ansehen, warum das Streaming von Antworten für eine hervorragende Nutzererfahrung mit LLMs entscheidend ist:

Verbesserte Nutzerfreundlichkeit

Streamingantworten bieten mehrere wichtige Vorteile für die Nutzererfahrung:

  1. Verringerte wahrgenommene Latenz: Nutzer sehen sofort Text (in der Regel innerhalb von 100–300 ms), anstatt mehrere Sekunden auf eine vollständige Antwort zu warten. Dieser Eindruck von Unmittelbarkeit verbessert die Nutzerzufriedenheit drastisch.
  2. Natürlicher Konversationsrhythmus: Das allmähliche Einblenden von Text ahmt die Kommunikation von Menschen nach und sorgt für eine natürlichere Unterhaltung.
  3. Progressive Informationsverarbeitung: Nutzer können mit der Verarbeitung von Informationen beginnen, sobald sie eintreffen, anstatt von einem großen Textblock auf einmal überwältigt zu werden.
  4. Möglichkeit zur frühzeitigen Unterbrechung: In einer vollständigen Anwendung können Nutzer die LLM unterbrechen oder umleiten, wenn sie feststellen, dass sie in eine ungünstige Richtung geht.
  5. Visuelle Bestätigung der Aktivität: Der Streamingtext gibt sofort Feedback dazu, ob das System funktioniert, und reduziert so Ungewissheit.

Technische Vorteile

Neben den Verbesserungen der UX bietet Streaming auch technische Vorteile:

  1. Vorzeitige Funktionsausführung: Funktionsaufrufe können erkannt und ausgeführt werden, sobald sie im Stream erscheinen, ohne auf die vollständige Antwort zu warten.
  2. Inkrementelle UI-Änderungen: Sie können Ihre Benutzeroberfläche schrittweise aktualisieren, sobald neue Informationen verfügbar sind, und so eine dynamischere Oberfläche schaffen.
  3. Verwaltung des Unterhaltungsstatus: Durch Streaming erhalten Sie klare Signale dazu, wann Antworten abgeschlossen sind und wann sie noch in Bearbeitung sind. So lässt sich der Status besser verwalten.
  4. Risiko von Zeitüberschreitungen verringert: Bei nicht streamenden Antworten besteht bei langwierigen Generierungen das Risiko von Verbindungszeitenüberschreitungen. Beim Streaming wird die Verbindung frühzeitig hergestellt und aufrechterhalten.

Wenn Sie Streaming in Ihrer Colorist App implementieren, werden Nutzern sowohl Textantworten als auch Farbänderungen schneller angezeigt, was die App deutlich reaktionsfähiger macht.

Verwaltung des Unterhaltungsstatus hinzufügen

Fügen wir zuerst einen Statusanbieter hinzu, um zu verfolgen, ob die App derzeit eine Streamingantwort verarbeitet. Aktualisieren Sie Ihre lib/services/gemini_chat_service.dart-Datei:

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);

Streamingimplementierung

Sehen wir uns an, was dieser Code bewirkt:

  1. Tracking des Konversationsstatus:
    • Mit einem conversationStateProvider wird erfasst, ob die App derzeit eine Antwort verarbeitet.
    • Der Status ändert sich während der Verarbeitung von idle → busy und dann wieder zu idle.
    • So werden mehrere gleichzeitige Anfragen verhindert, die zu Konflikten führen könnten.
  2. Streaminitialisierung:
    • sendMessageStream() gibt einen Stream von Antwort-Chunks zurück, anstatt einen Future mit der vollständigen Antwort.
    • Jeder Block kann Text, Funktionsaufrufe oder beides enthalten.
  3. Progressive Verarbeitung:
    • await for verarbeitet jeden Teil in Echtzeit, sobald er eintrifft.
    • Der Text wird sofort an die Benutzeroberfläche angehängt, wodurch der Streamingeffekt entsteht.
    • Funktionsaufrufe werden ausgeführt, sobald sie erkannt werden.
  4. Funktionsaufruf:
    • Wenn in einem Chunk ein Funktionsaufruf erkannt wird, wird er sofort ausgeführt.
    • Die Ergebnisse werden über einen weiteren Streaming-Aufruf an das LLM zurückgesendet.
    • Die Antwort des LLM auf diese Ergebnisse wird ebenfalls per Streaming verarbeitet.
  5. Fehlerbehandlung und Bereinigung:
    • try/catch bietet eine robuste Fehlerbehandlung
    • Der Block finally sorgt dafür, dass der Unterhaltungsstatus ordnungsgemäß zurückgesetzt wird.
    • Die Nachricht wird immer fertiggestellt, auch wenn Fehler auftreten

Diese Implementierung ermöglicht ein responsives, zuverlässiges Streaming und behält dabei den richtigen Konversationsstatus bei.

Hauptbildschirm aktualisieren, um den Unterhaltungsstatus zu verknüpfen

Ändern Sie die Datei lib/main.dart, um den Konversationsstatus an den Hauptbildschirm weiterzugeben:

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),
     
),
   
);
 
}
}

Die wichtigste Änderung besteht darin, dass conversationState an das MainScreen-Widget übergeben wird. Die MainScreen (vom Paket colorist_ui bereitgestellt) verwendet diesen Status, um die Texteingabe zu deaktivieren, während eine Antwort verarbeitet wird.

So entsteht eine einheitliche Nutzererfahrung, bei der die Benutzeroberfläche den aktuellen Status der Unterhaltung widerspiegelt.

Riverpod-Code generieren

Führen Sie den Befehl „build runner“ aus, um den erforderlichen Riverpod-Code zu generieren:

dart run build_runner build --delete-conflicting-outputs

Streamingantworten ausführen und testen

Führen Sie Ihre Anwendung aus:

flutter run -d DEVICE

Screenshot der Colorist App, der zeigt, wie das Gemini-LLM in Streaming-Manier antwortet

Testen Sie jetzt das Streamingverhalten mit verschiedenen Farbbeschreibungen. Probieren Sie Beschreibungen wie die folgenden aus:

  • „Zeig mir die tiefe Türkisfarbe des Ozeans bei Dämmerung“
  • „Ich möchte eine farbenfrohe Koralle sehen, die mich an tropische Blumen erinnert.“
  • „Erstelle ein gedecktes Olivgrün wie bei alten Armee-Fatiguen“

Technischer Ablauf des Streamings im Detail

Sehen wir uns an, was genau beim Streamen einer Antwort passiert:

Verbindungsaufbau

Wenn Sie sendMessageStream() aufrufen, geschieht Folgendes:

  1. Die App stellt eine Verbindung zum Vertex AI-Dienst her.
  2. Die Nutzeranfrage wird an den Dienst gesendet.
  3. Der Server beginnt mit der Verarbeitung der Anfrage.
  4. Die Streamverbindung bleibt offen und kann Blöcke übertragen.

Blockübertragung

Während Gemini Inhalte generiert, werden Chunks über den Stream gesendet:

  1. Der Server sendet Textblöcke, sobald sie generiert werden (in der Regel ein paar Wörter oder Sätze).
  2. Wenn Gemini einen Funktionsaufruf ausführt, sendet es die Informationen zum Funktionsaufruf.
  3. Auf Funktionsaufrufe können weitere Textblöcke folgen.
  4. Der Stream wird fortgesetzt, bis die Generierung abgeschlossen ist.

Progressive Verarbeitung

Ihre App verarbeitet jeden Teil inkrementell:

  1. Jeder Textblock wird an die vorhandene Antwort angehängt.
  2. Funktionsaufrufe werden ausgeführt, sobald sie erkannt werden.
  3. Die Benutzeroberfläche wird in Echtzeit mit Text- und Funktionsergebnissen aktualisiert.
  4. Der Status wird erfasst, um anzuzeigen, dass die Antwort noch gestreamt wird.

Streamende

Nach Abschluss der Generierung:

  1. Der Stream wird vom Server geschlossen
  2. Die await for-Schleife wird auf natürliche Weise beendet
  3. Die Nachricht wird als erledigt markiert.
  4. Der Unterhaltungsstatus wird wieder auf „Inaktiv“ gesetzt.
  5. Die Benutzeroberfläche wird aktualisiert, um den abgeschlossenen Status widerzuspiegeln.

Streaming im Vergleich zu nicht-streamingbasierten Daten

Um die Vorteile des Streamings besser zu verstehen, vergleichen wir den Streaming-Ansatz mit dem Ansatz ohne Streaming:

Aspekt

Nicht-Streaming

Streaming

Wahrgenommene Latenz

Der Nutzer sieht nichts, bis die vollständige Antwort bereit ist

Die ersten Wörter werden dem Nutzer innerhalb von Millisekunden angezeigt.

Auswirkungen für Nutzer

Lange Wartezeit, gefolgt von plötzlicher Textanzeige

Natürliche, progressive Textdarstellung

Statusverwaltung

Einfacher (Nachrichten sind entweder ausstehend oder abgeschlossen)

Komplexer (Nachrichten können sich im Streaming-Status befinden)

Funktionsausführung

Tritt nur nach einer vollständigen Antwort auf

Tritt während der Antwortgenerierung auf

Implementierungskomplexität

Einfacher zu implementieren

Erfordert zusätzliche Statusverwaltung

Wiederherstellung nach Fehlern

Alles-oder-Nichts-Antwort

Teilweise Antworten können trotzdem nützlich sein

Codekomplexität

Weniger komplex

Komplexere Stream-Verarbeitung

Bei einer Anwendung wie Colorist überwiegen die UX-Vorteile des Streamings die Implementierungskomplexität, insbesondere bei Farbinterpretationen, deren Generierung mehrere Sekunden dauern kann.

Best Practices für die UX von Streaming

Beachten Sie bei der Implementierung von Streaming in Ihren eigenen LLM-Anwendungen die folgenden Best Practices:

  1. Klare visuelle Hinweise: Es müssen immer klare visuelle Hinweise vorhanden sein, die zwischen gestreamten und vollständigen Nachrichten unterscheiden.
  2. Eingabeblockierung: Deaktivieren Sie die Nutzereingabe während des Streamings, um mehrere überlappende Anfragen zu vermeiden.
  3. Fehlerwiederherstellung: Die Benutzeroberfläche muss so gestaltet sein, dass bei einer Unterbrechung des Streams eine reibungslose Wiederherstellung möglich ist.
  4. Statusübergänge: Sorgen Sie für einen reibungslosen Übergang zwischen den Status „Inaktiv“, „Streaming“ und „Abgeschlossen“.
  5. Fortschrittsvisualisierung: Verwenden Sie subtile Animationen oder Indikatoren, die eine aktive Verarbeitung anzeigen.
  6. Abbruchoptionen: In einer vollständigen App müssen Nutzer die Möglichkeit haben, laufende Generierungen abzubrechen.
  7. Integration von Funktionsergebnissen: Entwerfen Sie Ihre Benutzeroberfläche so, dass Funktionsergebnisse in der Mitte des Streams angezeigt werden.
  8. Leistungsoptimierung: Minimiert die Neuaufbaue der Benutzeroberfläche bei schnellen Stream-Aktualisierungen

Im colorist_ui-Paket werden viele dieser Best Practices für Sie implementiert. Sie sind jedoch wichtige Aspekte für jede Streaming-LLM-Implementierung.

Nächste Schritte

Im nächsten Schritt implementieren Sie die LLM-Synchronisierung, indem Sie Gemini benachrichtigen, wenn Nutzer Farben aus dem Verlauf auswählen. Dadurch wird die Nutzerfreundlichkeit verbessert, da der LLM über von Nutzern initiierte Änderungen am Anwendungsstatus informiert ist.

Fehlerbehebung

Probleme bei der Streamverarbeitung

Wenn Probleme mit der Streamverarbeitung auftreten:

  • Symptome: Teilantworten, fehlender Text oder abrupte Beendigung des Streams
  • Lösung: Netzwerkverbindung prüfen und für korrekte async/await-Muster im Code sorgen
  • Diagnose: Prüfen Sie das Protokollfeld auf Fehlermeldungen oder Warnungen im Zusammenhang mit der Streamverarbeitung.
  • Problem beheben: Sorgen Sie dafür, dass bei der gesamten Streamverarbeitung die richtige Fehlerbehandlung mit try-/catch-Blöcken verwendet wird.

Fehlende Funktionsaufrufe

Wenn Funktionsaufrufe im Stream nicht erkannt werden:

  • Symptome: Text wird angezeigt, aber die Farben werden nicht aktualisiert oder das Protokoll enthält keine Funktionsaufrufe.
  • Lösung: Prüfen Sie die Anleitung in der Systemaufforderung zur Verwendung von Funktionsaufrufen.
  • Diagnose: Prüfen Sie im Logbereich, ob Funktionsaufrufe empfangen werden.
  • Problemlösung: Passen Sie die Systemaufforderung so an, dass der LLM expliziter angewiesen wird, das set_color-Tool zu verwenden.

Allgemeine Fehlerbehandlung

Bei allen anderen Problemen:

  • Schritt 1: Im Protokollbereich nach Fehlermeldungen suchen
  • Schritt 2: Vertex AI in Firebase-Verbindung prüfen
  • Schritt 3: Prüfen, ob der gesamte von Riverpod generierte Code auf dem neuesten Stand ist
  • Schritt 4: Streamingimplementierung auf fehlende await-Anweisungen prüfen

Wichtige Konzepte

  • Streamingantworten mit der Gemini API implementieren, um die Nutzerfreundlichkeit zu verbessern
  • Unterhaltungsstatus verwalten, um Streaminginteraktionen richtig zu verarbeiten
  • Echtzeittext und Funktionsaufrufe bei Eingang verarbeiten
  • Responsive Benutzeroberflächen erstellen, die während des Streamings inkrementell aktualisiert werden
  • Gleichzeitige Streams mit geeigneten asynchronen Mustern verarbeiten
  • Angemessenes visuelles Feedback beim Streamen von Antworten

Durch die Implementierung von Streaming haben Sie die Nutzerfreundlichkeit Ihrer Colorist-App erheblich verbessert und eine reaktionsschnellere, ansprechendere Benutzeroberfläche geschaffen, die wirklich dialogorientiert ist.

8. LLM-Kontextsynchronisierung

In diesem Bonusschritt implementieren Sie die LLM-Kontextsynchronisierung, indem Sie Gemini benachrichtigen, wenn Nutzer Farben aus dem Verlauf auswählen. So wird eine einheitlichere Oberfläche geschaffen, bei der der LLM nicht nur die expliziten Nachrichten der Nutzer, sondern auch ihre Aktionen auf der Benutzeroberfläche kennt.

In diesem Schritt geht es um:

  • LLM-Kontextsynchronisierung zwischen der Benutzeroberfläche und dem LLM erstellen
  • UI-Ereignisse in einen für das LLM verständlichen Kontext serialisieren
  • Unterhaltungskontext anhand von Nutzeraktionen aktualisieren
  • Für eine einheitliche Nutzung über verschiedene Interaktionsmethoden hinweg sorgen
  • Kontextbewusstsein des LLM über explizite Chatnachrichten hinaus verbessern

LLM-Kontextsynchronisierung

Traditionelle Chatbots reagieren nur auf explizite Nutzernachrichten, was zu einer Unterbrechung führt, wenn Nutzer auf andere Weise mit der App interagieren. Die LLM-Kontextsynchronisierung behebt diese Einschränkung:

Warum die LLM-Kontextsynchronisierung wichtig ist

Wenn Nutzer über UI-Elemente mit Ihrer App interagieren (z. B. eine Farbe aus dem Verlauf auswählen), kann der LLM nicht wissen, was passiert ist, es sei denn, Sie geben ihm dies explizit an. LLM-Kontextsynchronisierung:

  1. Kontext beibehalten: Der LLM wird über alle relevanten Nutzeraktionen informiert.
  2. Schafft Kohärenz: Sorgt für eine einheitliche Umgebung, in der das LLM UI-Interaktionen berücksichtigt
  3. Verbessert die Intelligenz: Ermöglicht es dem LLM, angemessen auf alle Nutzeraktionen zu reagieren.
  4. Verbesserte Nutzerfreundlichkeit: Die gesamte Anwendung wirkt integrierter und reaktionsschneller.
  5. Reduziert den Aufwand für Nutzer: Nutzer müssen ihre UI-Aktionen nicht mehr manuell erklären.

Wenn ein Nutzer in Ihrer Colorist-App eine Farbe aus dem Verlauf auswählt, soll Gemini diese Aktion bestätigen und intelligent auf die ausgewählte Farbe reagieren, um den Eindruck eines nahtlosen, aufmerksamen Assistenten zu wahren.

Gemini Chat-Dienst für Benachrichtigungen zur Farbauswahl aktualisieren

Zuerst fügen Sie der GeminiChatService eine Methode hinzu, um das LLM zu benachrichtigen, wenn ein Nutzer eine Farbe aus dem Verlauf auswählt. Aktualisieren Sie Ihre lib/services/gemini_chat_service.dart-Datei:

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);

Die wichtigste Neuerung ist die Methode notifyColorSelection, die:

  1. Ein ColorData-Objekt, das die ausgewählte Farbe darstellt
  2. Er codiert sie in ein JSON-Format, das in eine Nachricht eingefügt werden kann.
  3. Sendet eine speziell formatierte Nachricht an den LLM, die eine Nutzerauswahl angibt
  4. Die vorhandene sendMessage-Methode wird zum Bearbeiten der Benachrichtigung wiederverwendet.

Bei diesem Ansatz wird die vorhandene Infrastruktur zur Nachrichtenverarbeitung genutzt, um Duplikate zu vermeiden.

Haupt-App aktualisieren, um Benachrichtigungen zur Farbauswahl zu verknüpfen

Ändern Sie nun die Datei lib/main.dart, um die Benachrichtigungsfunktion für die Farbauswahl an den Hauptbildschirm weiterzuleiten:

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),
     
),
   
);
 
}
}

Die wichtigste Änderung ist das Hinzufügen des notifyColorSelection-Callbacks, der das UI-Ereignis (Auswahl einer Farbe aus dem Verlauf) mit dem LLM-Benachrichtigungssystem verbindet.

Systemaufforderung aktualisieren

Jetzt müssen Sie Ihren Systemprompt aktualisieren, um dem LLM anzugeben, wie es auf Benachrichtigungen zur Farbauswahl reagieren soll. Ändern Sie die Datei 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

Die wichtigste Neuerung ist der Abschnitt „Wenn Nutzer bisherige Farben auswählen“, der Folgendes enthält:

  1. Erläutert das Konzept der Benachrichtigungen zur Auswahl des Verlaufs für den LLM
  2. Ein Beispiel für diese Benachrichtigungen
  3. Ein Beispiel für eine angemessene Antwort
  4. Erklärt, wie die Auswahl bestätigt und die Farbe kommentiert werden soll

So kann das LLM besser verstehen, wie es auf diese speziellen Nachrichten reagieren soll.

Riverpod-Code generieren

Führen Sie den Befehl „build runner“ aus, um den erforderlichen Riverpod-Code zu generieren:

dart run build_runner build --delete-conflicting-outputs

LLM-Kontextsynchronisierung ausführen und testen

Führen Sie Ihre Anwendung aus:

flutter run -d DEVICE

Screenshot der Colorist App, in dem das Gemini-LLM auf eine Auswahl aus dem Farbverlauf reagiert

Zum Testen der LLM-Kontextsynchronisierung sind folgende Schritte erforderlich:

  1. Erstellen Sie zuerst einige Farben, indem Sie sie im Chat beschreiben.
    • „Zeig mir ein leuchtendes Lila“
    • „Ich hätte gerne ein dunkelgrünes.“
    • „Setzen Sie ein knallrotes Licht“
  2. Klicken Sie dann auf eine der Farbminiaturansichten im Verlaufsstreifen.

Beachten Sie Folgendes:

  1. Die ausgewählte Farbe wird auf dem Hauptdisplay angezeigt.
  2. Im Chat wird eine Nutzernachricht mit der Farbauswahl angezeigt.
  3. Das LLM antwortet, indem es die Auswahl bestätigt und die Farbe kommentiert.
  4. Die gesamte Interaktion wirkt natürlich und einheitlich.

So wird eine nahtlose Nutzung ermöglicht, bei der der LLM sowohl auf direkte Nachrichten als auch auf UI-Interaktionen aufmerksam wird und entsprechend reagiert.

So funktioniert die LLM-Kontextsynchronisierung

Sehen wir uns die technischen Details dieser Synchronisierung an:

Datenfluss

  1. Nutzeraktion: Der Nutzer klickt im Verlaufsstreifen auf eine Farbe.
  2. UI-Ereignis: Das MainScreen-Widget erkennt diese Auswahl.
  3. Callback-Ausführung: Der notifyColorSelection-Callback wird ausgelöst.
  4. Nachricht erstellen: Mit den Farbdaten wird eine speziell formatierte Nachricht erstellt.
  5. LLM-Verarbeitung: Die Nachricht wird an Gemini gesendet, das das Format erkennt.
  6. Kontextbezogene Antwort: Gemini antwortet je nach Systemprompt angemessen.
  7. Aktualisierung der Benutzeroberfläche: Die Antwort wird im Chat angezeigt, was für ein einheitliches Erscheinungsbild sorgt.

Datenserialisierung

Ein wichtiger Aspekt dieses Ansatzes ist die Serialization der Farbdaten:

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

Die Methode toLLMContextMap() (vom Paket colorist_ui bereitgestellt) wandelt ein ColorData-Objekt in eine Map mit wichtigen Eigenschaften um, die vom LLM verstanden werden können. Dazu gehören in der Regel:

  • RGB-Werte (Rot, Grün, Blau)
  • Hexadezimalcode
  • Jeder Name oder jede Beschreibung, die mit der Farbe verknüpft ist

Wenn Sie diese Daten einheitlich formatieren und in die Nachricht aufnehmen, sorgen Sie dafür, dass das LLM alle Informationen hat, die es für eine angemessene Antwort benötigt.

Weitere Anwendungsfälle für die LLM-Kontextsynchronisierung

Dieses Muster, das LLM über UI-Ereignisse zu benachrichtigen, hat neben der Farbauswahl zahlreiche Anwendungen:

Andere Anwendungsfälle

  1. Filteränderungen: Benachrichtigen Sie den LLM, wenn Nutzer Filter auf Daten anwenden.
  2. Navigationsereignisse: Informieren Sie den LLM, wenn Nutzer zu verschiedenen Bereichen wechseln.
  3. Auswahländerungen: Aktualisieren Sie die LLM, wenn Nutzer Elemente aus Listen oder Rastern auswählen.
  4. Einstellungen aktualisieren: Informieren Sie den LLM, wenn Nutzer Einstellungen ändern
  5. Datenmanipulation: Benachrichtigen Sie den LLM, wenn Nutzer Daten hinzufügen, bearbeiten oder löschen.

In jedem Fall bleibt das Muster gleich:

  1. UI-Ereignis erkennen
  2. Relevante Daten serialisieren
  3. Eine speziell formatierte Benachrichtigung an den LLM senden
  4. Das LLM durch den Systemprompt zu einer angemessenen Antwort anleiten

Best Practices für die LLM-Kontextsynchronisierung

Hier sind einige Best Practices für eine effektive LLM-Kontextsynchronisierung:

1. Einheitliches Format

Verwenden Sie ein einheitliches Format für Benachrichtigungen, damit das LLM sie leicht identifizieren kann:

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

2. Kontextreich

Geben Sie in Benachrichtigungen genügend Details an, damit das LLM intelligent reagieren kann. Bei Farben sind das RGB-Werte, Hexadezimalcodes und alle anderen relevanten Eigenschaften.

3. Eine klare Anleitung

Geben Sie im Systemprompt eine eindeutige Anleitung zum Umgang mit Benachrichtigungen, idealerweise mit Beispielen.

4. Natürliche Integration

Gestalten Sie Benachrichtigungen so, dass sie sich nahtlos in die Unterhaltung einfügen und nicht als technische Unterbrechungen wahrgenommen werden.

5. Selektive Benachrichtigung

Benachrichtigen Sie den LLM nur über Aktionen, die für die Unterhaltung relevant sind. Nicht jedes UI-Ereignis muss kommuniziert werden.

Fehlerbehebung

Probleme mit Benachrichtigungen

Wenn der LLM nicht richtig auf Farbauswahlen reagiert:

  • Prüfen, ob das Format der Benachrichtigungsnachricht mit der in der Systemaufforderung beschriebenen übereinstimmt
  • Prüfen, ob die Farbdaten richtig serialisiert werden
  • Die Systemaufforderung muss klare Anweisungen zur Auswahl enthalten.
  • Nach Fehlern im Chatdienst beim Senden von Benachrichtigungen suchen

Kontextverwaltung

Wenn der LLM den Kontext verliert:

  • Prüfen, ob die Chatsitzung ordnungsgemäß aufrechterhalten wird
  • Prüfen, ob der Konversationsstatus korrekt wechselt
  • Prüfen, ob Benachrichtigungen über dieselbe Chatsitzung gesendet werden

Allgemeine Probleme

Bei allgemeinen Problemen:

  • Protokolle auf Fehler oder Warnungen prüfen
  • Vertex AI in Firebase-Verbindung prüfen
  • Prüfen Sie, ob die Typen der Funktionsparameter übereinstimmen.
  • Prüfen, ob der gesamte von Riverpod generierte Code auf dem neuesten Stand ist

Wichtige Konzepte

  • LLM-Kontextsynchronisierung zwischen Benutzeroberfläche und LLM erstellen
  • UI-Ereignisse in einen LLM-freundlichen Kontext serialisieren
  • LLM-Verhalten für verschiedene Interaktionsmuster steuern
  • Einheitliche Nutzererfahrung bei Nachrichten und anderen Interaktionen
  • Verbesserung der LLM-Kenntnis des gesamten Anwendungsstatus

Durch die Implementierung der LLM-Kontextsynchronisierung haben Sie eine wirklich integrierte Umgebung geschaffen, in der das LLM eher wie ein aufmerksamer, responsiver Assistent als nur ein Textgenerator wirkt. Dieses Muster kann auf unzählige andere Anwendungen angewendet werden, um natürlichere, intuitivere KI-gestützte Benutzeroberflächen zu erstellen.

9. Das wars!

Sie haben das Codelab zum Thema Colorist erfolgreich abgeschlossen. 🎉

Erstellte Inhalte

Sie haben eine voll funktionsfähige Flutter-Anwendung erstellt, in die die Gemini API von Google eingebunden ist, um Farbbeschreibungen in natürlicher Sprache zu interpretieren. Ihre App kann jetzt:

  • Beschreibungen in natürlicher Sprache wie „Sonnenuntergangsorange“ oder „Tiefes Ozeanblau“ verarbeiten
  • Mit Gemini diese Beschreibungen intelligent in RGB-Werte umwandeln
  • Die erkannten Farben in Echtzeit mit Streamingantworten anzeigen
  • Nutzerinteraktionen sowohl über Chat als auch über UI-Elemente verarbeiten
  • Kontextbezogene Informationen bei verschiedenen Interaktionsmethoden berücksichtigen

So geht es weiter

Nachdem Sie die Grundlagen der Einbindung von Gemini in Flutter kennengelernt haben, können Sie sich auf die folgenden Themen konzentrieren:

Colorist-App optimieren

  • Farbpaletten: Funktion zum Generieren komplementärer oder passender Farbschemata hinzufügen
  • Spracheingabe: Spracherkennung für mündliche Farbbeschreibungen integrieren
  • Verlaufsverwaltung: Optionen zum Benennen, Organisieren und Exportieren von Farbvorlagen hinzufügen
  • Benutzerdefinierte Aufforderungen: Eine Benutzeroberfläche erstellen, über die Nutzer Systemaufforderungen anpassen können
  • Erweiterte Analysen: Sie können nachverfolgen, welche Beschreibungen am besten funktionieren oder Probleme verursachen.

Weitere Gemini-Funktionen entdecken

  • Multimodale Eingaben: Fügen Sie Bildeingaben hinzu, um Farben aus Fotos zu extrahieren.
  • Inhaltsgenerierung: Mit Gemini können Sie farbbezogene Inhalte wie Beschreibungen oder Geschichten generieren.
  • Verbesserungen beim Funktionsaufruf: Sie können komplexere Tool-Integrationen mit mehreren Funktionen erstellen.
  • Sicherheitseinstellungen: Hier erfahren Sie mehr über die verschiedenen Sicherheitseinstellungen und ihre Auswirkungen auf die Antworten.

Diese Muster auf andere Domains anwenden

  • Dokumentenanalyse: Apps erstellen, die Dokumente verstehen und analysieren können
  • Hilfe beim kreativen Schreiben: Schreibtools mit LLM-gestützten Vorschlägen erstellen
  • Aufgabenautomatisierung: Entwerfen Sie Apps, die natürliche Sprache in automatisierte Aufgaben umwandeln.
  • Wissensbasierte Anwendungen: Erstellen von Expertensystemen in bestimmten Bereichen

Ressourcen

Hier finden Sie einige nützliche Ressourcen für Ihr weiteres Lernen:

Offizielle Dokumentation

Kurs und Leitfaden zu Prompts

Community

Observable Flutter Agentic-Reihe

In Folge 59 erkunden Craig Labenz und Andrew Brogden dieses Codelab und heben interessante Teile der App-Entwicklung hervor.

In Folge 60 erweitern Craig und Andrew die Codelab-App um neue Funktionen und kämpfen darum, dass LLMs das tun, was sie ihnen sagen.

In Folge 61 wird Craig von Chris Sells unterstützt, um einen neuen Ansatz zur Analyse von Nachrichtenschlagzeilen zu entwickeln und entsprechende Bilder zu generieren.

Feedback

Wir würden uns sehr über Ihr Feedback zu diesem Codelab freuen. Sie können uns Feedback auf folgende Arten geben:

Vielen Dank, dass Sie dieses Codelab abgeschlossen haben. Wir hoffen, dass Sie die spannenden Möglichkeiten an der Schnittstelle von Flutter und KI weiter erkunden.