Gemini-gestützte Flutter-App erstellen

1. Mit Gemini eine Flutter-App erstellen

Aufgaben

In diesem Codelab entwickeln Sie Colorist, eine interaktive Flutter-Anwendung, die die Leistungsfähigkeit der Gemini API direkt in Ihre Flutter-App bringt. Wollten Sie schon immer, dass Nutzer Ihre App über natürliche Sprache steuern können, wussten aber nicht, wo Sie anfangen sollen? 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:

  • Verarbeitet diese Beschreibungen mit der Gemini API von Google
  • Interpretiert die Beschreibungen in präzise RGB-Farbwerte
  • Zeigt die Farbe in Echtzeit auf dem Bildschirm an
  • Liefert technische Farbdetails und interessanten Kontext zur Farbe
  • Verlauf der zuletzt generierten Farben

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

Die App bietet eine Split-Screen-Oberfläche mit einem Farbbereich und einem interaktiven Chatsystem auf der einen Seite und einem detaillierten Protokollbereich mit den Rohdaten der LLM-Interaktionen auf der anderen Seite. Anhand dieses Logs können Sie besser nachvollziehen, wie eine LLM-Integration wirklich funktioniert.

Warum das für Flutter-Entwickler wichtig ist

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

Ihre Lernreise

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

  1. Projekteinrichtung: Sie beginnen mit einer einfachen Flutter-App-Struktur und dem colorist_ui-Paket.
  2. Einfache Gemini-Integration: Verbinden Sie Ihre App mit Firebase AI Logic und implementieren Sie die LLM-Kommunikation.
  3. Effektive Prompts: Erstellen Sie einen System-Prompt, der das LLM anleitet, Farbbeschreibungen zu verstehen.
  4. Funktionsdeklarationen: Definieren Sie Tools, mit denen das LLM Farben in Ihrer Anwendung festlegen kann.
  5. Tool-Verarbeitung: Funktionsaufrufe des LLM verarbeiten und mit dem Status Ihrer App verbinden
  6. Streaming-Antworten: Die Nutzerfreundlichkeit durch LLM-Antworten in Echtzeit verbessern
  7. LLM-Kontextsynchronisierung: Für eine einheitliche Nutzererfahrung wird das LLM über Nutzeraktionen informiert.

Lerninhalte

  • Firebase AI Logic für Flutter-Anwendungen konfigurieren
  • Effektive System-Prompts erstellen, um das Verhalten von LLMs zu steuern
  • Funktionsdeklarationen implementieren, die natürliche Sprache und App-Funktionen verbinden
  • Streamingantworten verarbeiten, um eine reaktionsschnelle Nutzererfahrung zu ermöglichen
  • Status zwischen UI-Ereignissen und dem LLM synchronisieren
  • LLM-Konversationsstatus mit Riverpod verwalten
  • Fehler in LLM-gestützten Anwendungen ordnungsgemäß behandeln

Codevorschau: Ein Vorgeschmack auf die Implementierung

Hier sehen Sie einen Ausschnitt der Funktionsdeklaration, die Sie erstellen, damit das 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 zu diesem Codelab

Craig Labenz und Andrew Brogdon sprechen in der Observable Flutter-Folge 59 über dieses Codelab:

Vorbereitung

Für dieses Codelab benötigen Sie Folgendes:

  • Flutter-Entwicklungserfahrung: Vertrautheit mit den Grundlagen von Flutter und der Dart-Syntax
  • Kenntnisse im asynchronen Programmieren: Verständnis von Futures, async/await und Streams
  • Firebase-Konto: Sie benötigen ein Google-Konto, um Firebase einzurichten.

Legen wir los und erstellen Sie Ihre erste LLM-gestützte Flutter-App.

2. Projekteinrichtung und Echo-Dienst

In diesem ersten Schritt richten Sie die Projektstruktur ein und implementieren einen Echo-Dienst, der später durch die Gemini API-Integration ersetzt wird. So wird die Anwendungsarchitektur festgelegt und dafür gesorgt, dass die Benutzeroberfläche richtig funktioniert, bevor die Komplexität von LLM-Aufrufen hinzukommt.

Lerninhalte in diesem Schritt

  • Ein Flutter-Projekt mit den erforderlichen Abhängigkeiten einrichten
  • Mit dem colorist_ui-Paket für UI-Komponenten arbeiten
  • Einen Echo-Nachrichtendienst implementieren und mit der Benutzeroberfläche verbinden

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-App counter erstellen möchten. Die App ist für die Verwendung auf Computern, Mobilgeräten und im Web konzipiert. Linux wird auf 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

Dadurch werden die folgenden Schlüsselpakete 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 Codegenerierung und Linting

Ihre pubspec.yaml sollte in etwa so aussehen:

pubspec.yaml

name: colorist
description: "A new Flutter project."
publish_to: 'none'
version: 0.1.0

environment:
  sdk: ^3.9.2

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

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

flutter:
  uses-material-design: true

main.dart-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(chatStateProvider.notifier);
    final logStateNotifier = ref.read(logStateProvider.notifier);

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

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

Übersicht der Architektur

Sehen wir uns kurz die Architektur der colorist App an:

Das Paket colorist_ui

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

  1. MainScreen: Die Haupt-UI-Komponente, in der Folgendes angezeigt wird:
    • Ein Splitscreen-Layout auf dem Computer (Interaktionsbereich und Protokollbereich)
    • Eine tabellarische Oberfläche auf einem Mobilgerät
    • Farbdarstellung, Chatoberfläche und Verlaufsminiaturen
  2. Statusverwaltung: Die App verwendet mehrere Status-Notifier:
    • ChatStateNotifier: Verwaltet die Chatnachrichten.
    • ColorStateNotifier: Verwaltet die aktuelle Farbe und den Verlauf
    • LogStateNotifier: Verwaltet die Logeinträge für das Debugging.
  3. Nachrichtenverarbeitung: 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 Ihren einfachen Echo-Dienst. Dieser wird durch den Gemini Chat-Dienst ersetzt.
  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 sich die UI-Komponenten bereits erledigt haben.

Anwendung ausführen

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

flutter run -d DEVICE

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

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

Sie sollten jetzt die Colorist-App mit Folgendem sehen:

  1. Ein Farbbereich mit einer Standardfarbe
  2. Eine Chatoberfläche, auf der Sie Nachrichten eingeben können
  3. Ein Protokollbereich mit den Chatinteraktionen

Geben Sie eine Nachricht wie „Ich möchte eine dunkelblaue Farbe“ ein und drücken Sie auf „Senden“. Der Echo-Dienst wiederholt Ihre Nachricht einfach. In späteren Schritten ersetzen Sie dies durch die tatsächliche Farbanalyse mit Firebase AI Logic.

Nächste Schritte

Im nächsten Schritt konfigurieren Sie Firebase und implementieren die 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:

  • Achten Sie darauf, dass Sie die aktuelle Version verwenden.
  • Prüfen, ob die Abhängigkeit richtig hinzugefügt wurde
  • Auf in Konflikt stehende Paketversionen prüfen

Build-Fehler

Wenn Build-Fehler angezeigt werden:

  • Prüfen Sie, ob Sie das neueste stabile Flutter SDK installiert haben.
  • Führen Sie flutter clean und dann flutter pub get aus.
  • Konsolenausgabe auf bestimmte Fehlermeldungen prüfen

Wichtige gelernte Konzepte

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

3. Einfache Gemini Chat-Integration

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

Lerninhalte in diesem Schritt

  • Firebase in einer Flutter-Anwendung einrichten
  • Firebase AI Logic für den Gemini-Zugriff konfigurieren
  • Riverpod-Provider für Firebase- und Gemini-Dienste erstellen
  • Einen einfachen Chatdienst mit der Gemini API implementieren
  • Asynchrone API-Antworten und Fehlerstatus behandeln

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 Firebase AI Logic-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.

Firebase AI Logic in Ihrem Firebase-Projekt einrichten

  1. Rufen Sie in der Firebase Console Ihr Projekt auf.
  2. Wählen Sie in der linken Seitenleiste KI aus.
  3. Wählen Sie im Drop-down-Menü für KI die Option KI-Logik aus.
  4. Wählen Sie auf der Karte „Firebase AI Logic“ die Option Jetzt starten aus.
  5. Folgen Sie der Anleitung, um die Gemini Developer API für Ihr Projekt zu aktivieren.

FlutterFire CLI installieren

Die FlutterFire CLI vereinfacht die Firebase-Einrichtung in Flutter-Apps:

dart pub global activate flutterfire_cli

Firebase zu Ihrer Flutter-App hinzufügen

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

Mit diesem Befehl wird Folgendes ausgeführt:

  • Sie werden aufgefordert, das gerade erstellte Firebase-Projekt auszuwählen.
  • 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 Mindestversionen erforderlich, die höher sind als die Standardversionen für Flutter. Außerdem ist Netzwerkzugriff erforderlich, um mit den Firebase AI Logic-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.

iOS-Einstellungen konfigurieren

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

ios/Podfile

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

Gemini-Modellanbieter erstellen

Jetzt erstellen Sie die Riverpod-Provider für Firebase und Gemini. Erstellen Sie eine neue Datei lib/providers/gemini.dart:

lib/providers/gemini.dart

import 'dart:async';

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

import '../firebase_options.dart';

part 'gemini.g.dart';

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

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

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

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

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

  1. firebaseAppProvider: Initialisiert Firebase mit Ihrer Projektkonfiguration.
  2. geminiModelProvider: Erstellt eine Gemini-Instanz für generative Modelle.
  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 Lebenszyklus der App erhalten bleibt und der Kontext der Unterhaltung beibehalten wird.

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

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

part 'gemini_chat_service.g.dart';

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

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

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

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

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

Dieser Dienst:

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

Hinweis:Das Logfenster sieht an dieser Stelle fast genauso aus wie das Chatfenster. Das Protokoll wird interessanter, wenn Sie Funktionsaufrufe und dann Streaming-Antworten einführen.

Riverpod-Code generieren

Führen Sie den Build-Runner-Befehl aus, um den erforderlichen Riverpod-Code zu generieren:

dart run build_runner build --delete-conflicting-outputs

Dadurch werden die .g.dart-Dateien erstellt, die Riverpod für die Funktion benötigt.

Datei „main.dart“ aktualisieren

Aktualisieren Sie die Datei lib/main.dart, um den neuen Gemini Chat-Dienst 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 sind:

  1. Ersetzen des Echo-Dienstes durch den auf der Gemini API basierenden Chatdienst
  2. Lade- und Fehlerbildschirme mit dem AsyncValue-Muster von Riverpod und der when-Methode hinzufügen
  3. Benutzeroberfläche ü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 Ihr Zielgerät, z. B. macos, windows, chrome oder eine Geräte-ID.

Screenshot der Colorist App, auf dem das Gemini LLM auf eine Anfrage nach einer sonnigen gelben Farbe reagiert

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

LLM-Kommunikation

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

Kommunikationsablauf

  1. Nutzereingabe: 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 Firebase AI Logic 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. Protokollierung: Alle Kommunikationsvorgänge werden zur Transparenz protokolliert.

Chatsitzungen und Unterhaltungskontext

In der Gemini Chat-Sitzung wird der Kontext zwischen den Nachrichten beibehalten, sodass Unterhaltungen möglich sind. Das bedeutet, dass sich das LLM an frühere Interaktionen in der aktuellen Sitzung „erinnert“, was zu kohärenteren Unterhaltungen führt.

Die Annotation keepAlive: true für den Anbieter Ihrer Chatsitzung sorgt dafür, dass dieser Kontext während des gesamten Lebenszyklus der App erhalten bleibt. Dieser dauerhafte Kontext ist entscheidend, um einen natürlichen Gesprächsfluss mit dem LLM aufrechtzuerhalten.

Nächste Schritte

An diesem Punkt können Sie Gemini API alles fragen, da es keine Einschränkungen gibt, worauf sie antwortet. Sie könnten beispielsweise nach einer Zusammenfassung der Rosenkriege fragen, die nichts mit dem Zweck Ihrer Farbanwendung zu tun haben.

Im nächsten Schritt erstellen Sie einen System-Prompt, um Gemini bei der Interpretation von Farbbeschreibungen zu unterstützen. Hier wird gezeigt, wie Sie das Verhalten eines LLM an anwendungsspezifische Anforderungen anpassen und seine Funktionen auf die Domain Ihrer App konzentrieren.

Fehlerbehebung

Probleme bei der Firebase-Konfiguration

Wenn bei der Firebase-Initialisierung Fehler auftreten, können Sie Folgendes versuchen:

  • Prüfen Sie, ob die Datei firebase_options.dart korrekt generiert wurde.
  • Prüfen Sie, ob Sie ein Upgrade auf den Blaze-Tarif für den Zugriff auf Firebase AI Logic 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 Firebase AI Logic 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 ist und verfügbar ist.

Probleme mit dem Kontext von Unterhaltungen

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

  • Prüfen Sie, ob die Funktion chatSession mit @Riverpod(keepAlive: true) annotiert ist.
  • Prüfen Sie, ob Sie für alle Nachrichten denselben Chat verwenden.
  • Prüfen Sie, ob die Chatsitzung richtig initialisiert wurde, bevor Sie Nachrichten senden.

Plattformspezifische Probleme

Bei plattformspezifischen Problemen:

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

Wichtige gelernte Konzepte

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

4. Effektive Prompts für Farbbeschreibungen

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

Lerninhalte in diesem Schritt

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

Systemaufforderungen

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

Was sind Systemprompts?

Ein Systemprompt ist eine spezielle Art von Anweisung, die einem LLM gegeben wird und den Kontext, die Verhaltensrichtlinien und die Erwartungen für seine Antworten festlegt. Im Gegensatz zu Nutzernachrichten gilt für Systemprompts Folgendes:

  • Rolle und Persona des LLM festlegen
  • Spezialwissen oder ‑fähigkeiten definieren
  • Formatierungsanweisungen bereitstellen
  • Einschränkungen für Antworten festlegen
  • Beschreiben, wie in verschiedenen Szenarien vorgegangen werden soll

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

Warum Systemprompts wichtig sind

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

  1. Für Konsistenz sorgen: Das Modell anweisen, Antworten in einem einheitlichen Format zu liefern
  2. Relevanz verbessern: Richten Sie das Modell auf Ihren spezifischen Bereich aus (in Ihrem Fall Farben).
  3. Grenzen festlegen: Definieren Sie, was das Modell tun darf und was nicht.
  4. Nutzerfreundlichkeit verbessern: Ein natürlicheres, hilfreiches Interaktionsmuster schaffen
  5. Nachbearbeitung reduzieren: Antworten in Formaten erhalten, die leichter zu parsen oder darzustellen sind

Für Ihre Colorist-App muss das LLM Farbbeschreibungen einheitlich interpretieren und RGB-Werte in einem bestimmten Format bereitstellen.

Systemprompt-Asset erstellen

Zuerst erstellen Sie eine Systemprompt-Datei, 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 des System-Prompts

Sehen wir uns an, was dieser Prompt bewirkt:

  1. Definition der Rolle: Das LLM wird als „Farbexperten-Assistent“ festgelegt.
  2. Aufgabenerklärung: Definiert die primäre Aufgabe als Interpretation von Farbbeschreibungen in RGB-Werte.
  3. Antwortformat: Gibt genau an, wie RGB-Werte für Konsistenz formatiert werden sollen.
  4. Beispiel für Austausch: Bietet ein konkretes Beispiel für das erwartete Interaktionsmuster.
  5. Umgang mit Grenzfall: Hier wird beschrieben, wie mit unklaren Beschreibungen umzugehen ist.
  6. Einschränkungen und Richtlinien: Legt Grenzen fest, 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 und informativ sind und so formatiert werden, dass sie sich leicht parsen lassen, wenn Sie die RGB-Werte programmatisch extrahieren möchten.

pubspec.yaml aktualisieren

Aktualisieren Sie nun das Ende der Datei pubspec.yaml, um das Assets-Verzeichnis einzuschließen:

pubspec.yaml

flutter:
  uses-material-design: true

  assets:
    - assets/

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

Systemprompt-Anbieter erstellen

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

lib/providers/system_prompt.dart

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

part 'system_prompt.g.dart';

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

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

Gemini-Modellanbieter aktualisieren

Ändern Sie nun die Datei lib/providers/gemini.dart, um den Systemprompt einzufügen:

lib/providers/gemini.dart

import 'dart:async';

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

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

part 'gemini.g.dart';

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

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

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

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

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

Riverpod-Code generieren

Führen Sie den Build-Runner-Befehl 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, auf dem das Gemini LLM mit einer Antwort reagiert, die für eine Farbauswahl-App charakteristisch ist

Testen Sie die Funktion mit verschiedenen Farbbeschreibungen:

  • „I'd like a sky blue“ (Ich möchte einen himmelblauen)
  • „Gib mir ein Waldgrün“
  • „Erstelle ein leuchtendes Sonnenuntergangsorange.“
  • „Ich möchte die Farbe von frischem Lavendel“
  • „Show me something like a deep ocean blue“ (Zeig mir etwas wie ein tiefes Ozeanblau)

Gemini antwortet jetzt mit Erklärungen zu den Farben in einem natürlichen Gesprächston und gibt einheitlich formatierte RGB-Werte an. Der System-Prompt hat das LLM effektiv dazu angeleitet, die Art von Antworten zu liefern, die Sie benötigen.

Sie können auch nach Inhalten fragen, die nichts mit Farben zu tun haben. Nenne 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 die Nützlichkeit des Modells für Ihre spezielle Anwendung erheblich beeinflussen. Sie haben hier eine Form des Prompt-Engineerings angewendet, indem Sie Anweisungen so angepasst haben, dass das Modell sich so verhält, wie es für Ihre Anwendung erforderlich ist.

Effektives Prompt Engineering umfasst:

  1. Klare Rollendefinition: Festlegen des Zwecks des LLM
  2. Explizite Anweisungen: Detaillierte Angaben dazu, wie das LLM reagieren soll
  3. Konkrete Beispiele: Zeigen statt nur sagen, wie gute Antworten aussehen
  4. Umgang mit Sonderfällen: Das LLM anweisen, wie mit mehrdeutigen Szenarien umzugehen ist
  5. Spezifikationen zur Formatierung: Antworten müssen einheitlich und nutzerfreundlich strukturiert sein.

Der von Ihnen erstellte Systemprompt wandelt die allgemeinen Funktionen von Gemini in einen spezialisierten Assistenten für die Farbanalyse 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 das LLM nicht nur RGB-Werte vorschlagen, sondern auch Funktionen in Ihrer App aufrufen, um die Farbe direkt festzulegen. Hier wird gezeigt, 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 in pubspec.yaml das Asset-Verzeichnis richtig 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 das LLM Ihre Formatierungsanweisungen nicht konsistent befolgt:

  • Formatanforderungen im System-Prompt expliziter formulieren
  • Fügen Sie weitere Beispiele hinzu, um das erwartete Muster zu veranschaulichen.
  • Das angeforderte Format muss für das Modell angemessen sein.

API-Ratenbegrenzung

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

  • Der Firebase AI Logic-Dienst hat Nutzungslimits.
  • Wiederholungslogik mit exponentiellem Backoff implementieren
  • In der Firebase Console nach Kontingentproblemen suchen

Wichtige gelernte Konzepte

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

In diesem Schritt wird gezeigt, wie Sie das Verhalten von LLMs erheblich anpassen können, ohne Ihren Code zu ändern. Dazu müssen Sie lediglich klare Anweisungen im Systemprompt angeben.

5. Funktionsdeklarationen für LLM-Tools

In diesem Schritt beginnen Sie mit der Implementierung von Funktionsdeklarationen, um Gemini zu ermöglichen, Aktionen in Ihrer App auszuführen. Mit dieser leistungsstarken Funktion kann das LLM nicht nur RGB-Werte vorschlagen, sondern sie auch über spezielle Tool-Aufrufe in der Benutzeroberfläche Ihrer App festlegen. Dazu ist jedoch der nächste Schritt erforderlich, um die in der Flutter-App ausgeführten LLM-Anfragen zu sehen.

Lerninhalte in diesem Schritt

  • Funktionsaufrufe mit LLMs 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 sind Funktionsaufrufe?

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

  1. Erkennen, wann die Ausführung einer bestimmten Funktion für eine Nutzeranfrage sinnvoll wäre
  2. Generieren Sie ein strukturiertes JSON-Objekt mit den für diese Funktion erforderlichen Parametern.
  3. Anwendung die Funktion mit diesen Parametern ausführen lassen
  4. Das Ergebnis der Funktion empfangen und in die Antwort einfügen

Beim Funktionsaufruf beschreibt das LLM nicht nur, was zu tun ist, sondern kann auch konkrete Aktionen in Ihrer Anwendung auslösen.

Warum Funktionsaufrufe für Flutter-Apps wichtig sind

Funktionsaufrufe schaffen eine leistungsstarke Verbindung 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 gibt saubere, strukturierte Daten aus, anstatt Text, der geparst werden muss.
  3. Komplexe Vorgänge: Ermöglicht dem LLM, auf externe Daten zuzugreifen, Berechnungen durchzuführen oder den Anwendungsstatus zu ändern.
  4. Bessere Nutzerfreundlichkeit: Ermöglicht eine nahtlose Integration zwischen Konversation und Funktion

In Ihrer Colorist-App können Nutzer durch die Funktion „Funktionsaufruf“ beispielsweise „Ich möchte ein Waldgrün“ sagen und die Benutzeroberfläche wird 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_ai/firebase_ai.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'gemini_tools.g.dart';

class GeminiTools {
  GeminiTools(this.ref);

  final Ref ref;

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

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

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

Funktionsdeklarationen

Sehen wir uns an, was dieser Code bewirkt:

  1. Funktionsbenennung: Sie nennen Ihre Funktion set_color, um ihren Zweck klar anzugeben.
  2. Funktionsbeschreibung: Sie geben eine klare Beschreibung an, damit das LLM weiß, wann die Funktion verwendet werden 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 von RGB, 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 müssen angegeben werden?
  • 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_ai/firebase_ai.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

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

part 'gemini.g.dart';

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

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

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

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

Die wichtigste Änderung ist das Hinzufügen des Parameters tools: geminiTools.tools beim Erstellen des generativen Modells. So weiß Gemini, welche Funktionen aufgerufen werden können.

Systemaufforderung aktualisieren

Jetzt müssen Sie den Systemprompt so anpassen, dass das LLM angewiesen wird, das neue set_color-Tool zu verwenden. assets/system_prompt.md aktualisieren:

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 am Systemprompt sind:

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

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

Riverpod-Code generieren

Führen Sie den Build-Runner-Befehl aus, um den erforderlichen Riverpod-Code zu generieren:

dart run build_runner build --delete-conflicting-outputs

Anwendung ausführen

An diesem Punkt 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, antwortet Gemini so, als ob ein Tool aufgerufen wurde. Sie sehen jedoch erst im nächsten Schritt Farbänderungen in der Benutzeroberfläche.

Führen Sie Ihre App aus:

flutter run -d DEVICE

Screenshot der Colorist App, auf dem das Gemini LLM mit einer unvollständigen Antwort reagiert

Beschreiben Sie eine Farbe wie „tiefes Ozeanblau“ oder „Waldgrün“ und sehen Sie sich die Antworten an. Das LLM versucht, die oben definierten Funktionen aufzurufen, aber Ihr Code erkennt noch keine Funktionsaufrufe.

Der Prozess für Funktionsaufrufe

So funktioniert die Funktionsaufruffunktion von Gemini:

  1. Funktionsauswahl: Das LLM entscheidet anhand der Nutzeranfrage, ob ein Funktionsaufruf hilfreich wäre.
  2. Parametergenerierung: Das LLM generiert Parameterwerte, die zum Schema der Funktion passen.
  3. Format für Funktionsaufrufe: Das LLM sendet in seiner Antwort ein strukturiertes Funktionsaufrufobjekt.
  4. Anwendungsbearbeitung: Ihre App würde diesen Aufruf empfangen und die entsprechende Funktion ausführen (die im nächsten Schritt implementiert wird).
  5. Antwortintegration: Bei Unterhaltungen in 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 Schritt 4 oder 5 (Verarbeiten der Funktionsaufrufe) noch nicht implementiert. Das werden Sie im nächsten Schritt tun.

Technische Details: So entscheidet Gemini, wann Funktionen verwendet werden

Gemini trifft intelligente Entscheidungen darüber, wann Funktionen verwendet werden sollen. Dabei werden folgende Faktoren berücksichtigt:

  1. Nutzerabsicht: Gibt an, ob die Anfrage des Nutzers am besten durch eine Funktion erfüllt werden kann.
  2. Funktionsrelevanz: Wie gut die verfügbaren Funktionen zur Aufgabe passen
  3. Verfügbarkeit von Parametern: Gibt an, ob Parameterwerte zuverlässig ermittelt werden können.
  4. Systemanweisungen: Anweisungen aus Ihrem System-Prompt zur Verwendung von Funktionen

Durch die Bereitstellung klarer Funktionsdeklarationen und Systemanweisungen haben Sie Gemini so eingerichtet, dass Farbbeschreibungsanfragen als Möglichkeiten zum Aufrufen der set_color-Funktion erkannt werden.

Nächste Schritte

Im nächsten Schritt implementieren Sie Handler für die Funktionsaufrufe, die von Gemini kommen. So schließt sich der Kreis 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, ob der Funktionsname klar und aussagekräftig ist
  • Die Funktionsbeschreibung muss den Zweck der Funktion genau erläutern.

Probleme mit System-Prompts

Wenn das LLM nicht versucht, die Funktion zu verwenden:

  • Prüfen Sie, ob der Systemprompt das LLM eindeutig anweist, das Tool set_color zu verwenden.
  • Prüfen Sie, ob im Beispiel im Systemprompt die Verwendung von Funktionen demonstriert wird.
  • Versuchen Sie, die Anweisung zur Verwendung des Tools expliziter zu formulieren.

Allgemeine Probleme

Wenn andere Probleme auftreten:

  • Konsole auf Fehler im Zusammenhang mit Funktionsdeklarationen prüfen
  • Prüfen, ob die Tools richtig an das Modell übergeben werden
  • Achten Sie darauf, dass der gesamte von Riverpod generierte Code auf dem neuesten Stand ist.

Wichtige gelernte Konzepte

  • Funktionsdeklarationen definieren, um die LLM-Funktionen in Flutter-Apps zu erweitern
  • Parameterschemas für die Erhebung strukturierter Daten erstellen
  • Funktionsdeklarationen in das Gemini-Modell einbinden
  • Systemaufforderungen aktualisieren, um die Funktionsnutzung zu fördern
  • Auswahl und Aufruf von Funktionen durch LLMs

In diesem Schritt wird gezeigt, wie LLMs die Lücke zwischen Eingaben in natürlicher Sprache und strukturierten Funktionsaufrufen schließen können. Das ist die Grundlage für eine nahtlose Integration zwischen Konversations- und Anwendungsfunktionen.

6. Umgang mit dem Tool implementieren

In diesem Schritt implementieren Sie Handler für die Funktionsaufrufe, die von Gemini kommen. Damit wird der Kommunikationszyklus zwischen Eingaben in natürlicher Sprache und konkreten Anwendungsfunktionen geschlossen. Das LLM kann Ihre Benutzeroberfläche basierend auf Nutzerbeschreibungen direkt bearbeiten.

Lerninhalte in diesem Schritt

  • Die vollständige Pipeline für Funktionsaufrufe in LLM-Anwendungen
  • Funktionsaufrufe von Gemini in einer Flutter-Anwendung verarbeiten
  • Funktionshandler implementieren, die den Anwendungsstatus ändern
  • Funktionsantworten verarbeiten und Ergebnisse an das LLM zurückgeben
  • Vollständigen Kommunikationsfluss zwischen LLM und Benutzeroberfläche erstellen
  • Funktionsaufrufe und Antworten zur Transparenz protokollieren

Informationen zur Pipeline für Funktionsaufrufe

Bevor wir uns mit der Implementierung befassen, sehen wir uns die gesamte Pipeline für Funktionsaufrufe an:

End-to-End-Ablauf

  1. Nutzereingabe: Der Nutzer beschreibt eine Farbe in natürlicher Sprache (z.B. „waldgrü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 von Funktionsaufrufen: 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 (die angezeigte Farbe wird geändert).
  7. Antwortgenerierung: Ihre Funktion gibt Ergebnisse an das LLM zurück.
  8. Einbeziehung der Antwort: Das LLM bezieht diese Ergebnisse in seine endgültige Antwort ein.
  9. UI-Aktualisierung: Die Benutzeroberfläche reagiert auf die Statusänderung und zeigt die neue Farbe an.

Der vollständige Kommunikationszyklus ist für die richtige LLM-Integration unerlässlich. Wenn ein LLM einen Funktionsaufruf ausführt, wird die Anfrage nicht einfach gesendet und dann fortgefahren. Stattdessen wird gewartet, bis Ihre Anwendung die Funktion ausführt und Ergebnisse zurückgibt. Das LLM verwendet diese Ergebnisse dann, um die endgültige Antwort zu formulieren. So entsteht ein natürlicher Gesprächsfluss, in dem die ergriffenen Maßnahmen berücksichtigt werden.

Funktions-Handler implementieren

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

lib/services/gemini_tools.dart

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

part 'gemini_tools.g.dart';

class GeminiTools {
  GeminiTools(this.ref);

  final Ref ref;

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

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

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

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

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

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

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

Funktionshandler

Sehen wir uns an, was diese Funktionshandler tun:

  1. handleFunctionCall: Ein zentraler Dispatcher, der Folgendes bietet:
    • Protokolliert den Funktionsaufruf zur Transparenz im Logbereich
    • Leitet Anfragen 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 Folgendes tut:
    • Extrahiert RGB-Werte aus der Argumentzuordnung.
    • Konvertiert sie in die erwarteten Typen (Doubles).
    • Aktualisiert den Farbstatus der Anwendung mit colorStateNotifier
    • Erstellt eine strukturierte Antwort mit dem Erfolgsstatus und den aktuellen Farbinformationen.
    • Protokolliert die Funktionsergebnisse für das Debugging
  3. handleUnknownFunction: Ein Fallback-Handler für unbekannte Funktionen, der Folgendes ausführt:
    • Protokolliert eine Warnung zur nicht unterstützten Funktion
    • Gibt eine Fehlerantwort an das LLM zurück

Die Funktion handleSetColor ist besonders wichtig, da sie die Lücke zwischen dem Natural Language Understanding des LLM und konkreten Änderungen an der Benutzeroberfläche schließt.

Gemini Chat-Dienst aktualisieren, um Funktionsaufrufe und Antworten zu verarbeiten

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

lib/services/gemini_chat_service.dart

import 'dart:async';

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

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

part 'gemini_chat_service.g.dart';

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

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

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

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

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

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

Kommunikationsablauf

Die wichtigste Neuerung 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. Bei jedem Funktionsaufruf wird die Methode handleFunctionCall mit dem Funktionsnamen und den Argumenten aufgerufen.
  3. Sammelt die Ergebnisse jedes Funktionsaufrufs
  4. Diese Ergebnisse werden mit Content.functionResponses an das LLM zurückgesendet.
  5. Verarbeitet die Antwort des LLM auf die Funktionsergebnisse
  6. Aktualisiert die Benutzeroberfläche mit dem endgültigen Antworttext

Dadurch wird ein Roundtrip-Ablauf erstellt:

  • Nutzer → LLM: Fordert eine Farbe an
  • 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 Build-Runner-Befehl 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, auf dem das Gemini LLM mit einem Funktionsaufruf antwortet

Geben Sie verschiedene Farbbeschreibungen ein:

  • „Ich möchte ein tiefes Karmesinrot.“
  • „Zeig mir ein beruhigendes Himmelblau.“
  • „Gib mir die Farbe frischer Minzblätter.“
  • „Ich möchte einen warmen Sonnenuntergang in Orange sehen.“
  • „Make it a rich royal purple“ (Mach es zu einem satten, königlichen Lila)

Jetzt sollten Sie Folgendes sehen:

  1. Ihre Nachricht wird in der Chatoberfläche angezeigt
  2. Die Antwort von Gemini wird im Chat angezeigt
  3. Funktionsaufrufe werden im Logfeld protokolliert
  4. Funktionsergebnisse werden unmittelbar danach protokolliert.
  5. Das Farbrechteck wird aktualisiert und zeigt die beschriebene Farbe an.
  6. Die RGB-Werte werden aktualisiert und zeigen die Komponenten der neuen Farbe an.
  7. Die endgültige Antwort von Gemini wird angezeigt, oft mit einem Kommentar zur festgelegten Farbe.

Das Log-Steuerfeld bietet Einblicke in die Vorgänge im Hintergrund. Sie sehen hier Folgendes:

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

Benachrichtigung zum Farbstatus

Die colorStateNotifier, die Sie zum Aktualisieren von Farben verwenden, ist Teil des colorist_ui-Pakets. Folgendes wird verwaltet:

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

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

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

Die UI-Komponenten im colorist_ui-Paket beobachten diesen Status und werden automatisch aktualisiert, wenn er sich ändert. So entsteht eine reaktive 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 mit Stacktraces im Log-Bereich aufgezeichnet.
  3. Nutzerfeedback: Gibt eine freundliche Fehlermeldung im Chat aus.
  4. Statusbereinigung: Der Nachrichtenstatus wird auch bei einem Fehler abgeschlossen.

So bleibt die App stabil und gibt auch dann angemessenes Feedback, wenn Probleme mit dem LLM-Dienst oder der Funktionsausführung auftreten.

Funktionsaufrufe für eine bessere Nutzererfahrung

Sie haben hier gesehen, wie leistungsstarke natürliche Schnittstellen mit LLMs erstellt werden können:

  1. Schnittstelle für natürliche Sprache: Nutzer drücken ihre Absicht in Alltagssprache aus.
  2. Intelligente Interpretation: Das LLM übersetzt vage Beschreibungen in genaue Werte.
  3. Direkte Bearbeitung: Die Benutzeroberfläche wird als Reaktion auf natürliche Sprache aktualisiert.
  4. Kontextbezogene Antworten: Das LLM liefert Kontextinformationen zu den Änderungen.
  5. Geringe kognitive Belastung: Nutzer müssen keine RGB-Werte oder Farbtheorie verstehen.

Dieses Muster, bei dem LLM-Funktionsaufrufe verwendet werden, um natürliche Sprache und UI-Aktionen zu verbinden, kann auf unzählige andere Bereiche als die Farbauswahl ausgeweitet werden.

Nächste Schritte

Im nächsten Schritt verbessern Sie die Nutzerfreundlichkeit, indem Sie Streaming-Antworten implementieren. Anstatt auf die vollständige Antwort zu warten, verarbeiten Sie Textblöcke und Funktionsaufrufe, sobald sie eingehen. So entsteht eine reaktionsschnellere und ansprechendere Anwendung.

Fehlerbehebung

Probleme mit Funktionsaufrufen

Wenn Gemini Ihre Funktionen nicht aufruft oder Parameter falsch sind, gehen Sie so vor:

  • Prüfen Sie, ob Ihre Funktionsdeklaration der Beschreibung im Systemprompt entspricht.
  • Prüfen, ob Parameternamen und -typen übereinstimmen
  • Achten Sie darauf, dass der Systemprompt das LLM explizit anweist, das Tool zu verwenden.
  • Prüfen Sie, ob der Funktionsname in Ihrem Handler genau mit dem Namen in der Deklaration übereinstimmt.
  • Logbereich auf detaillierte Informationen zu Funktionsaufrufen prüfen

Probleme mit Funktionsantworten

Wenn Funktionsergebnisse nicht richtig an das LLM zurückgesendet werden:

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

Probleme mit der Farbdarstellung

Wenn Farben nicht richtig angezeigt werden:

  • Achten Sie darauf, dass RGB-Werte richtig in Gleitkommazahlen konvertiert werden (das 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 die Benachrichtigung über den Farbstatus korrekt aufgerufen wird
  • Prüfen Sie das Log auf die genauen Werte, die an die Funktion übergeben werden.

Allgemeine Probleme

Bei allgemeinen Problemen:

  • Protokolle auf Fehler oder Warnungen prüfen
  • Firebase AI Logic-Verbindung prüfen
  • Prüfen Sie, ob es Typkonflikte bei Funktionsparametern gibt.
  • Achten Sie darauf, dass der gesamte von Riverpod generierte Code auf dem neuesten Stand ist.

Wichtige gelernte Konzepte

  • Vollständige Pipeline für Funktionsaufrufe in Flutter implementieren
  • Vollständige Kommunikation zwischen einem LLM und Ihrer Anwendung herstellen
  • Verarbeiten strukturierter Daten aus LLM-Antworten
  • Funktionsergebnisse an das LLM zurücksenden, damit sie in Antworten einbezogen werden können
  • Logbereich verwenden, um Einblick in die Interaktionen zwischen LLM und Anwendung zu erhalten
  • Verknüpfen von Eingaben in natürlicher Sprache mit konkreten Änderungen an der Benutzeroberfläche

Nachdem Sie diesen Schritt abgeschlossen haben, demonstriert Ihre App 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 Konversation, in der diese Aktionen berücksichtigt werden. So entsteht eine intuitive, dialogorientierte Oberfläche, die für Nutzer wie Magie wirkt.

7. Streamingantworten für eine bessere Nutzerfreundlichkeit

In diesem Schritt verbessern Sie die Nutzerfreundlichkeit, indem Sie Streaming-Antworten 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 reaktionsschnellere und ansprechendere Anwendung.

Inhalte dieses Schritts

  • Die Bedeutung von Streaming für LLM-basierte Anwendungen
  • Streaming von LLM-Antworten in einer Flutter-Anwendung implementieren
  • Verarbeiten von Textausschnitten, sobald sie von der API empfangen werden
  • Unterhaltungsstatus verwalten, um Nachrichtenkonflikte zu vermeiden
  • Funktionsaufrufe in Streaming-Antworten verarbeiten
  • Visuelle Hinweise für laufende Antworten erstellen

Warum Streaming für LLM-Anwendungen wichtig ist

Bevor wir mit der Implementierung beginnen, wollen wir uns ansehen, warum das Streamen von Antworten so wichtig ist, um mit LLMs eine hervorragende User Experience zu schaffen:

Verbesserte Nutzerfreundlichkeit

Das Streamen von Antworten bietet mehrere wichtige Vorteile für die Nutzerfreundlichkeit:

  1. Geringere wahrgenommene Latenz: Nutzer sehen Text sofort (in der Regel innerhalb von 100 bis 300 ms), anstatt mehrere Sekunden auf eine vollständige Antwort zu warten. Diese Wahrnehmung der Unmittelbarkeit verbessert die Nutzerzufriedenheit erheblich.
  2. Natürlicher Gesprächsrhythmus: Der Text wird nach und nach eingeblendet, was der menschlichen Kommunikation nachempfunden ist und für einen natürlicheren Dialog sorgt.
  3. Progressive Informationsverarbeitung: Nutzer können mit der Verarbeitung von Informationen beginnen, sobald sie eintreffen, anstatt von einem großen Textblock auf einmal überfordert zu werden.
  4. Möglichkeit für frühe Unterbrechung: In einer vollständigen Anwendung können Nutzer die LLM möglicherweise unterbrechen oder umleiten, wenn sie feststellen, dass sie in eine nicht hilfreiche Richtung geht.
  5. Visuelle Bestätigung der Aktivität: Der Streaming-Text gibt sofortiges Feedback, dass das System funktioniert, wodurch die Unsicherheit verringert wird.

Technische Vorteile

Neben UX-Verbesserungen bietet Streaming auch technische Vorteile:

  1. Frühe 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-Updates: Sie können die Benutzeroberfläche nach und nach aktualisieren, wenn neue Informationen eingehen, um eine dynamischere Nutzererfahrung zu schaffen.
  3. Verwaltung des Unterhaltungsstatus: Durch das Streaming werden klare Signale dafür gesendet, wann Antworten abgeschlossen sind und wann sie noch laufen. So lässt sich der Status besser verwalten.
  4. Geringeres Risiko von Zeitüberschreitungen: Bei Nicht-Streaming-Antworten besteht bei lang andauernden Generierungen das Risiko von Verbindungszeitüberschreitungen. Beim Streaming wird die Verbindung frühzeitig hergestellt und aufrechterhalten.

Wenn Sie Streaming in Ihre Colorist-App implementieren, sehen Nutzer sowohl Textantworten als auch Farbänderungen schneller, was die Reaktionsfähigkeit deutlich verbessert.

Verwaltung des Unterhaltungsstatus hinzufügen

Fügen wir zuerst einen Statusanbieter hinzu, um zu verfolgen, ob die App derzeit eine Streaming-Antwort 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_ai/firebase_ai.dart';
import 'package:flutter_riverpod/legacy.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

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

part 'gemini_chat_service.g.dart';

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

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

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

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

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

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

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

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

Streaming-Implementierung

Sehen wir uns an, was dieser Code bewirkt:

  1. Tracking des Konversationsstatus:
    • Eine conversationStateProvider gibt an, ob die App derzeit eine Antwort verarbeitet.
    • Der Status wechselt während der Verarbeitung von idle → busy und dann zurück zu idle.
    • So wird verhindert, dass mehrere gleichzeitige Anfragen zu Konflikten führen.
  2. Stream-Initialisierung:
    • sendMessageStream() gibt einen Stream von Antwort-Chunks anstelle eines Future mit der vollständigen Antwort zurück.
    • Jeder Chunk kann Text, Funktionsaufrufe oder beides enthalten.
  3. Progressive Verarbeitung:
    • await for verarbeitet jeden Chunk in Echtzeit, sobald er eintrifft.
    • Text wird sofort an die Benutzeroberfläche angehängt, wodurch der Streaming-Effekt entsteht.
    • Funktionsaufrufe werden ausgeführt, sobald sie erkannt werden.
  4. Verarbeitung von Funktionsaufrufen:
    • 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 Reaktion des LLM auf diese Ergebnisse wird ebenfalls im Streaming-Modus verarbeitet.
  5. Fehlerbehandlung und Bereinigung:
    • try/catch bietet eine robuste Fehlerbehandlung
    • Der finally-Block sorgt dafür, dass der Unterhaltungsstatus richtig zurückgesetzt wird.
    • Die Nachricht wird immer fertiggestellt, auch wenn Fehler auftreten.

Diese Implementierung sorgt für ein reaktionsschnelles und zuverlässiges Streaming und erhält gleichzeitig den richtigen Konversationsstatus bei.

Hauptbildschirm aktualisieren, um den Unterhaltungsstatus zu verbinden

Ändern Sie die Datei lib/main.dart, um den Unterhaltungsstatus an den Hauptbildschirm zu übergeben:

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. Der MainScreen (bereitgestellt vom Paket colorist_ui) verwendet diesen Status, um die Texteingabe zu deaktivieren, während eine Antwort verarbeitet wird.

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

Riverpod-Code generieren

Führen Sie den Build-Runner-Befehl aus, um den erforderlichen Riverpod-Code zu generieren:

dart run build_runner build --delete-conflicting-outputs

Streaming-Antworten ausführen und testen

Führen Sie Ihre Anwendung aus:

flutter run -d DEVICE

Screenshot der Colorist App, auf dem zu sehen ist, wie das Gemini LLM eine Antwort streamt

Testen Sie nun das Streamingverhalten mit verschiedenen Farbbeschreibungen. Versuchen Sie es mit Beschreibungen wie:

  • „Show me the deep teal color of the ocean at twilight“ (Zeig mir die tiefblaue Farbe des Ozeans in der Dämmerung)
  • „Ich möchte ein leuchtendes Korallenriff sehen, das mich an tropische Blumen erinnert.“
  • „Erstelle ein gedämpftes Olivgrün wie bei alten Armeeuniformen.“

Der technische Ablauf des Streamings im Detail

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

Verbindung herstellen

Wenn Sie sendMessageStream() aufrufen, geschieht Folgendes:

  1. Die App stellt eine Verbindung zum Firebase AI Logic-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 ist bereit, Chunks zu übertragen.

Chunk-Ü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 einige Wörter oder Sätze).
  2. Wenn Gemini sich für einen Funktionsaufruf entscheidet, werden die Informationen zum Funktionsaufruf gesendet.
  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 Chunk 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 verfolgt, um anzuzeigen, dass die Antwort noch gestreamt wird.

Stream abgeschlossen

Wenn die Generierung abgeschlossen ist:

  1. Der Stream wird vom Server geschlossen.
  2. Die await for-Schleife wird auf natürliche Weise beendet
  3. Die Nachricht ist als erledigt markiert
  4. Der Unterhaltungsstatus wird auf „Inaktiv“ zurückgesetzt.
  5. Die Benutzeroberfläche wird aktualisiert und zeigt den Status „Abgeschlossen“ an.

Vergleich von Streaming- und Nicht-Streaming-Antworten

Um die Vorteile des Streamings besser zu verstehen, vergleichen wir Streaming- und Nicht-Streaming-Ansätze:

Aspekt

Nicht-Streaming

Streaming

Wahrgenommene Latenz

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

Nutzer sehen die ersten Wörter innerhalb von Millisekunden

Auswirkungen für Nutzer

Lange Wartezeit, gefolgt von plötzlichem Erscheinen des Texts

Natürliches, progressives Einblenden von Text

Statusverwaltung

Einfacher (Nachrichten sind entweder ausstehend oder abgeschlossen)

Komplexer (Nachrichten können im Streaming-Status sein)

Funktionsausführung

Tritt erst nach einer vollständigen Antwort auf

Tritt während der Antwortgenerierung auf

Komplexität der Implementierung

Einfachere Implementierung

Erfordert zusätzliche Statusverwaltung

Wiederherstellung nach Fehlern

Alles-oder-Nichts-Reaktion

Teilweise Antworten können trotzdem nützlich sein

Komplexität des Codes

Weniger komplex

Komplexer aufgrund der Streamverarbeitung

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

Best Practices für die Streaming-Benutzerfreundlichkeit

Beachten Sie beim Implementieren von Streaming in Ihren eigenen LLM-Anwendungen die folgenden Best Practices:

  1. Deutliche visuelle Hinweise: Sorgen Sie immer für deutliche visuelle Hinweise, die zwischen Streaming- und vollständigen Nachrichten unterscheiden.
  2. Eingabe blockieren: Deaktivieren Sie die Nutzereingabe während des Streamings, um mehrere sich überschneidende Anfragen zu verhindern.
  3. Fehlerbehebung: Gestalte deine Benutzeroberfläche so, dass sie eine reibungslose Wiederherstellung ermöglicht, wenn das Streaming unterbrochen wird.
  4. Statusübergänge: Sorge für reibungslose Übergänge zwischen den Status „Leerlauf“, „Streaming“ und „Abgeschlossen“.
  5. Visualisierung des Fortschritts: Verwenden Sie subtile Animationen oder Anzeigen, um zu zeigen, dass die Verarbeitung aktiv ist.
  6. Optionen zum Abbrechen: In einer vollständigen App sollten Nutzer laufende Generierungen abbrechen können.
  7. Integration von Funktionsergebnissen: Gestalten Sie die Benutzeroberfläche so, dass Funktionsergebnisse während der Ausführung angezeigt werden können.
  8. Leistungsoptimierung: Minimieren Sie das Neuerstellen der Benutzeroberfläche bei schnellen Stream-Updates.

Das colorist_ui-Paket implementiert viele dieser Best Practices für Sie. Sie sind jedoch wichtige Überlegungen 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. So wird eine kohärentere Nutzererfahrung geschaffen, bei der das LLM über vom Nutzer initiierte Änderungen am Anwendungsstatus informiert ist.

Fehlerbehebung

Probleme bei der Streamverarbeitung

Wenn Probleme bei der Streamverarbeitung auftreten:

  • Symptome: Teilantworten, fehlender Text oder abruptes Beenden des Streams
  • Lösung: Netzwerkverbindung prüfen und für korrekte async/await-Muster im Code sorgen
  • Diagnose: Prüfen Sie das Log-Feld auf Fehlermeldungen oder Warnungen im Zusammenhang mit der Streamverarbeitung.
  • Beheben: Achten Sie darauf, dass bei der gesamten Streamverarbeitung eine ordnungsgemäße 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 im Log sind keine Funktionsaufrufe zu sehen.
  • Lösung: Überprüfen Sie die Anweisungen im System-Prompt zur Verwendung von Funktionsaufrufen.
  • Diagnose: Prüfen Sie im Logbereich, ob Funktionsaufrufe empfangen werden.
  • Lösung: Passen Sie den System-Prompt an, um das LLM expliziter anzuweisen, das Tool set_color zu verwenden.

Allgemeine Fehlerbehandlung

Bei allen anderen Problemen:

  • Schritt 1: Im Logbereich nach Fehlermeldungen suchen
  • Schritt 2: Verbindung zu Firebase AI Logic überprüfen
  • Schritt 3: Sicherstellen, dass der gesamte von Riverpod generierte Code auf dem neuesten Stand ist
  • Schritt 4: Streaming-Implementierung auf fehlende „await“-Anweisungen prüfen

Wichtige gelernte Konzepte

  • Streaming-Antworten mit der Gemini API für eine reaktionsschnellere Benutzeroberfläche implementieren
  • Unterhaltungsstatus verwalten, um Streaming-Interaktionen richtig zu verarbeiten
  • Echtzeittext und Funktionsaufrufe werden verarbeitet, sobald sie eingehen.
  • Reaktionsfähige UIs erstellen, die während des Streamings inkrementell aktualisiert werden
  • Gleichzeitige Streams mit geeigneten asynchronen Mustern verarbeiten
  • Angemessenes visuelles Feedback während des Streamings von Antworten geben

Durch die Implementierung von Streaming haben Sie die Nutzerfreundlichkeit Ihrer Colorist-App erheblich verbessert und eine reaktionsschnellere, ansprechendere Oberfläche geschaffen, die sich wirklich dialogorientiert anfühlt.

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 kohärentere Erfahrung geschaffen, bei der das LLM nicht nur die expliziten Nachrichten, sondern auch die Nutzeraktionen in der Benutzeroberfläche kennt.

Inhalte dieses Schritts

  • LLM-Kontextsynchronisierung zwischen Ihrer Benutzeroberfläche und dem LLM erstellen
  • UI-Ereignisse in einen Kontext serialisieren, den das LLM verstehen kann
  • Konversationskontext auf Grundlage von Nutzeraktionen aktualisieren
  • Einheitliche Nutzererfahrung bei verschiedenen Interaktionsmethoden
  • LLM-Kontextbewusstsein über explizite Chatnachrichten hinaus verbessern

LLM-Kontextsynchronisierung

Herkömmliche Chatbots reagieren nur auf explizite Nutzernachrichten. Das kann zu Problemen führen, wenn Nutzer auf andere Weise mit der App interagieren. Durch die LLM-Kontextsynchronisierung wird diese Einschränkung behoben:

Warum die Synchronisierung des LLM-Kontexts wichtig ist

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

  1. Kontext beibehalten: Das LLM wird über alle relevanten Nutzeraktionen auf dem Laufenden gehalten.
  2. Schafft Kohärenz: Das LLM reagiert auf UI-Interaktionen und sorgt so für ein stimmiges Nutzererlebnis.
  3. Verbessert die Intelligenz: Das LLM kann angemessen auf alle Nutzeraktionen reagieren.
  4. Verbesserte Nutzerfreundlichkeit: Die gesamte Anwendung wirkt integrierter und reaktionsschneller.
  5. Weniger Aufwand für Nutzer: Nutzer müssen ihre UI-Aktionen nicht manuell erläutern.

Wenn ein Nutzer in Ihrer Colorist-App eine Farbe aus dem Verlauf auswählt, soll Gemini diese Aktion bestätigen und intelligent zur ausgewählten Farbe kommentieren, um den Eindruck eines nahtlosen, aufmerksamen Assistenten zu erwecken.

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

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

part 'gemini_chat_service.g.dart';

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

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

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

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

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

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

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

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

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

Die wichtigste Ergänzung ist die Methode notifyColorSelection, die folgende Funktionen bietet:

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

So wird eine doppelte Verarbeitung vermieden, da Ihre vorhandene Infrastruktur zur Nachrichtenverarbeitung genutzt wird.

Haupt-App aktualisieren, um Benachrichtigungen zur Farbauswahl zu erhalten

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

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 (Auswählen einer Farbe aus dem Verlauf) mit dem LLM-Benachrichtigungssystem verbindet.

Systemaufforderung aktualisieren

Jetzt müssen Sie den System-Prompt aktualisieren, um das LLM anzuweisen, 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 Ergänzung ist der Abschnitt „Wenn Nutzer bisherige Farben auswählen“, in dem Folgendes beschrieben wird:

  1. Das Konzept der Benachrichtigungen zur Auswahl des Verlaufs für das LLM erläutern
  2. Beispiel für diese Benachrichtigungen
  3. Beispiel für eine angemessene Antwort
  4. Erwartungen an die Bestätigung der Auswahl und die Kommentierung der Farbe

So kann das LLM angemessen auf diese speziellen Nachrichten reagieren.

Riverpod-Code generieren

Führen Sie den Build-Runner-Befehl 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, auf dem das Gemini LLM auf eine Auswahl aus dem Farbverlauf reagiert

Das Testen der LLM-Kontextsynchronisierung umfasst Folgendes:

  1. Generieren Sie zuerst einige Farben, indem Sie sie im Chat beschreiben.
    • „Show me a vibrant purple“ (Zeig mir ein kräftiges Lila)
    • „Ich möchte ein Waldgrün.“
    • „Gib mir ein helles Rot“
  2. Klicken Sie dann auf eines der Farbmineaturen 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 reagiert, indem es die Auswahl bestätigt und die Farbe kommentiert.
  4. Die gesamte Interaktion fühlt sich natürlich und zusammenhängend an.

So entsteht eine nahtlose Erfahrung, bei der das LLM sowohl auf Direktnachrichten als auch auf UI-Interaktionen reagiert.

So funktioniert die Synchronisierung des LLM-Kontexts

Sehen wir uns die technischen Details der 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. Nachrichtenerstellung: Eine speziell formatierte Nachricht wird mit den Farbdaten erstellt.
  5. LLM-Verarbeitung: Die Nachricht wird an Gemini gesendet, das das Format erkennt.
  6. Kontextbezogene Antwort: Gemini antwortet angemessen basierend auf dem System-Prompt.
  7. Aktualisierung der Benutzeroberfläche: Die Antwort wird im Chat angezeigt, was für eine einheitliche Nutzererfahrung sorgt.

Datenserialisierung

Ein wichtiger Aspekt dieses Ansatzes ist, wie Sie die Farbdaten serialisieren:

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

Die Methode toLLMContextMap() (bereitgestellt vom Paket colorist_ui) konvertiert ein ColorData-Objekt in eine Map mit Schlüsseleigenschaften, die das LLM verstehen kann. Dazu gehören in der Regel:

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

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

Breitere Anwendung der LLM-Kontextsynchronisierung

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

Andere Anwendungsfälle

  1. Änderungen filtern: Die LLM benachrichtigen, wenn Nutzer Filter auf Daten anwenden
  2. Navigationsereignisse: Informieren das LLM, wenn Nutzer zu verschiedenen Bereichen navigieren.
  3. Auswahländerungen: Aktualisieren Sie das LLM, wenn Nutzer Elemente aus Listen oder Rastern auswählen.
  4. Aktualisierungen der Einstellungen: LLM informieren, wenn Nutzer Einstellungen ändern
  5. Datenbearbeitung: Das LLM benachrichtigen, 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 das LLM senden
  4. LLM durch den Systemprompt zu einer angemessenen Reaktion anleiten

Best Practices für die LLM-Kontextsynchronisierung

Hier sind einige Best Practices für eine effektive LLM-Kontextsynchronisierung, die auf Ihrer Implementierung basieren:

1. Einheitliches Format

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

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

2. Umfangreicher Kontext

Benachrichtigungen müssen genügend Details enthalten, damit das LLM intelligent reagieren kann. Bei Farben sind das RGB-Werte, Hexadezimalcodes und alle anderen relevanten Eigenschaften.

3. Klare Anweisungen

Geben Sie im System-Prompt explizite Anweisungen dazu, wie Benachrichtigungen behandelt werden sollen, idealerweise mit Beispielen.

4. Natürliche Integration

Benachrichtigungen sollten sich natürlich in die Unterhaltung einfügen und nicht als technische Unterbrechungen wahrgenommen werden.

5. Selektive Benachrichtigung

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

Fehlerbehebung

Probleme mit Benachrichtigungen

Wenn das LLM nicht richtig auf Farbauswahlen reagiert:

  • Prüfen, ob das Format der Benachrichtigungsmeldung dem entspricht, was im Systemprompt beschrieben wird
  • Prüfen, ob die Farbdaten richtig serialisiert werden
  • Sorgen Sie dafür, dass die Systemanweisung klare Anweisungen für die Verarbeitung von Auswahlen enthält.
  • Suchen Sie im Chatdienst nach Fehlern beim Senden von Benachrichtigungen.

Kontextverwaltung

Wenn das LLM den Kontext zu verlieren scheint:

  • Prüfen, ob die Chatsitzung ordnungsgemäß aufrechterhalten wird
  • Prüfen, ob die Konversationsstatus korrekt übergehen
  • Benachrichtigungen müssen über dieselbe Chatsitzung gesendet werden.

Allgemeine Probleme

Bei allgemeinen Problemen:

  • Protokolle auf Fehler oder Warnungen prüfen
  • Firebase AI Logic-Verbindung prüfen
  • Prüfen Sie, ob es Typkonflikte bei Funktionsparametern gibt.
  • Achten Sie darauf, dass der gesamte von Riverpod generierte Code auf dem neuesten Stand ist.

Wichtige gelernte Konzepte

  • LLM-Kontextsynchronisierung zwischen UI und LLM erstellen
  • UI-Ereignisse in LLM-freundlichen Kontext serialisieren
  • LLM-Verhalten für verschiedene Interaktionsmuster steuern
  • Einheitliche Nutzererfahrung bei Interaktionen mit und ohne Nachrichten
  • LLM-Bewusstsein für den allgemeinen Anwendungsstatus verbessern

Durch die Implementierung der LLM-Kontextsynchronisierung haben Sie eine wirklich integrierte Lösung geschaffen, bei der sich das LLM wie ein aufmerksamer, reaktionsschneller Assistent und nicht nur wie ein Textgenerator anfühlt. Dieses Muster kann auf unzählige andere Anwendungen angewendet werden, um natürlichere, intuitivere KI-basierte Benutzeroberflächen zu schaffen.

9. Glückwunsch!

Sie haben das Colorist-Codelab erfolgreich abgeschlossen. 🎉

Was Sie erstellt haben

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

  • Verarbeiten von Beschreibungen in natürlicher Sprache wie „Sonnenuntergangsorange“ oder „Tiefseeblau“
  • Gemini verwenden, um diese Beschreibungen intelligent in RGB-Werte zu übersetzen
  • Interpretierte Farben in Echtzeit mit Streaming-Antworten anzeigen
  • Nutzerinteraktionen über Chat und UI-Elemente verarbeiten
  • Kontextbezug bei verschiedenen Interaktionsmethoden beibehalten

So geht es weiter

Nachdem Sie nun die Grundlagen der Integration von Gemini in Flutter beherrschen, haben Sie folgende Möglichkeiten, um weiterzumachen:

Colorist-App optimieren

  • Farbpaletten: Funktion zum Generieren von komplementären oder passenden Farbschemas hinzufügen
  • Spracheingabe: Spracherkennung für verbale Farbbeschreibungen einbinden
  • Verlauf verwalten: Optionen zum Benennen, Organisieren und Exportieren von Farbsets hinzugefügt
  • Benutzerdefinierte Prompts: Erstellen Sie eine Schnittstelle, über die Nutzer System-Prompts anpassen können.
  • Erweiterte Analysen: Sie können nachvollziehen, welche Beschreibungen am besten funktionieren oder Schwierigkeiten verursachen.

Weitere Gemini-Funktionen

  • Multimodale Eingaben: Mit Bildeingaben können Sie Farben aus Fotos extrahieren.
  • Inhaltsgenerierung: Mit Gemini farbbezogene Inhalte wie Beschreibungen oder Geschichten generieren
  • Verbesserungen bei Funktionsaufrufen: Komplexere Tool-Integrationen mit mehreren Funktionen erstellen
  • Sicherheitseinstellungen: Hier können Sie verschiedene Sicherheitseinstellungen und ihre Auswirkungen auf Antworten ausprobieren.

Diese Muster auf andere Domains anwenden

  • Dokumentanalyse: Apps erstellen, die Dokumente verstehen und analysieren können
  • Unterstützung beim kreativen Schreiben: Schreibtools mit LLM-gestützten Vorschlägen erstellen
  • Aufgabenautomatisierung: Entwickeln Sie Apps, die natürliche Sprache in automatisierte Aufgaben umwandeln.
  • Wissensbasierte Anwendungen: Expertensysteme in bestimmten Bereichen erstellen

Ressourcen

Hier sind einige hilfreiche Ressourcen, die Ihnen den Einstieg erleichtern:

Offizielle Dokumentation

Kurs und Anleitung zu Prompts

Community

Observable Flutter Agentic-Serie

In Folge 59 sehen Sie, wie Craig Labenz und Andrew Brogden dieses Codelab durchgehen und interessante Aspekte der App-Entwicklung hervorheben.

In Folge 60 sind Craig und Andrew wieder dabei. Sie erweitern die Codelab-App um neue Funktionen und versuchen, LLMs dazu zu bringen, das zu tun, was sie sollen.

In Folge 61 analysiert Craig gemeinsam mit Chris Sells Nachrichtenüberschriften und generiert entsprechende Bilder.

Feedback

Wir freuen uns über Ihr Feedback zu diesem Codelab. Sie können Feedback auf folgende Weise geben:

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