构建由 Gemini 赋能的 Flutter 应用

1. 构建由 Gemini 提供支持的 Flutter 应用

构建内容

在此 Codelab 中,您将构建 Colorist,这是一个交互式 Flutter 应用,可将 Gemini API 的强大功能直接引入您的 Flutter 应用。您是否一直想让用户通过自然语言控制您的应用,但不知道从何处入手?此 Codelab 将介绍具体方法。

借助 Colorist,用户可以使用自然语言描述颜色(例如“日落的橙色”或“深海蓝”),然后该应用会:

  • 使用 Google 的 Gemini API 处理这些说明
  • 将说明解读为精确的 RGB 颜色值
  • 实时显示屏幕上的颜色
  • 提供有关颜色的技术详细信息和有趣背景信息
  • 维护最近生成的颜色的历史记录

Colorist 应用屏幕截图,显示颜色显示和聊天界面

该应用采用分屏界面,一侧是彩色显示区域和互动式聊天系统,另一侧是显示原始 LLM 互动的详细日志面板。此日志可帮助您更好地了解 LLM 集成的实际运作方式。

这对 Flutter 开发者有何重要意义

LLM 正在彻底改变用户与应用互动的方式,但将它们有效地集成到移动应用和桌面应用中会带来独特的挑战。此 Codelab 将教您一些实用的模式,这些模式不仅限于原始 API 调用。

您的学习之旅

此 Codelab 将引导您逐步完成构建 Colorist 的过程:

  1. 项目设置 - 您将从基本的 Flutter 应用结构和 colorist_ui 软件包开始
  2. 基本 Gemini 集成 - 将应用连接到 Firebase AI Logic 并实现 LLM 通信
  3. 有效的提示 - 创建一个系统提示,引导 LLM 理解颜色描述
  4. 函数声明 - 定义 LLM 可用于在应用中设置颜色的工具
  5. 工具处理 - 处理来自 LLM 的函数调用,并将其连接到应用的状态
  6. 流式传输回答 - 通过实时流式传输 LLM 回答来提升用户体验
  7. LLM 上下文同步 - 通过告知 LLM 用户操作来打造连贯一致的体验

学习内容

  • 为 Flutter 应用配置 Firebase AI Logic
  • 撰写有效的系统提示,引导 LLM 的行为
  • 实现连接自然语言和应用功能的函数声明
  • 处理流式响应,打造响应迅速的用户体验
  • 在界面事件和 LLM 之间同步状态
  • 使用 Riverpod 管理 LLM 对话状态
  • 在依托 LLM 的应用中妥善处理错误

代码预览:抢先体验您将实现的内容

以下是您将创建的函数声明,用于让 LLM 在您的应用中设置颜色:

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

此 Codelab 的视频概览

在 Observable Flutter 第 59 集中观看 Craig Labenz 和 Andrew Brogdon 讨论此 Codelab:

前提条件

为了充分利用本 Codelab,您应该具备以下条件:

  • Flutter 开发经验 - 熟悉 Flutter 基础知识和 Dart 语法
  • 异步编程知识 - 了解 Future、async/await 和流
  • Firebase 账号 - 您需要拥有 Google 账号才能设置 Firebase

让我们开始构建您的第一个由 LLM 提供支持的 Flutter 应用!

2. 项目设置和回声服务

在第一步中,您将设置项目结构并实现一个回显服务,该服务稍后将被替换为 Gemini API 集成。这样可以确定应用架构,并确保界面在添加 LLM 调用复杂性之前正常运行。

您将在此步骤中学习的内容

  • 设置具有所需依赖项的 Flutter 项目
  • 使用 colorist_ui 软件包处理界面组件
  • 实现回显消息服务并将其连接到界面

创建新的 Flutter 项目

首先,使用以下命令创建一个新的 Flutter 项目:

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

-e 标志表示您需要一个不含默认 counter 应用的空项目。该应用旨在跨桌面设备、移动设备和 Web 运行。不过,flutterfire 目前不支持 Linux。

添加依赖项

前往项目目录并添加所需的依赖项:

cd colorist
flutter pub add colorist_ui flutter_riverpod riverpod_annotation
flutter pub add --dev build_runner riverpod_generator riverpod_lint json_serializable

这会添加以下关键软件包:

  • colorist_ui:为 Colorist 应用提供界面组件的自定义软件包
  • flutter_riverpodriverpod_annotation:用于状态管理
  • logging:对于结构化日志记录
  • 用于代码生成和 linting 的开发依赖项

您的 pubspec.yaml 将如下所示:

pubspec.yaml

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

environment:
  sdk: ^3.9.2

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

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

flutter:
  uses-material-design: true

实现 main.dart 文件

lib/main.dart 的内容替换为以下内容:

lib/main.dart

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

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

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

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

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

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

这会设置一个 Flutter 应用,该应用实现了一个回显服务,通过返回用户的消息来模拟 LLM 的行为。

了解架构

让我们花点时间了解一下 colorist 应用的架构:

colorist_ui 软件包

colorist_ui 软件包提供预构建的界面组件和状态管理工具:

  1. MainScreen:显示以下内容的主要界面组件:
    • 桌面设备上的分屏布局(互动区域和日志面板)
    • 移动设备上的标签页式界面
    • 彩色显示、聊天界面和历史记录缩略图
  2. 状态管理:应用使用多个状态通知程序:
    • ChatStateNotifier:管理聊天消息
    • ColorStateNotifier:管理当前颜色和历史记录
    • LogStateNotifier:管理用于调试的日志条目
  3. 消息处理:应用使用具有不同状态的消息模型:
    • 用户消息:由用户输入
    • LLM 消息:由 LLM(或您目前的 Echo 服务)生成
    • MessageState:跟踪 LLM 消息是已完成还是仍在流式传输

应用架构

该应用遵循以下架构:

  1. 界面层:由 colorist_ui 软件包提供
  2. 状态管理:使用 Riverpod 进行响应式状态管理
  3. 服务层:目前包含简单的回声服务,之后将替换为 Gemini Chat 服务
  4. LLM 集成:将在后续步骤中添加

这种分离可让您专注于实现 LLM 集成,而无需担心界面组件。

运行应用

使用以下命令运行应用:

flutter run -d DEVICE

DEVICE 替换为目标设备,例如 macoswindowschrome 或设备 ID。

Colorist 应用屏幕截图,显示了回显服务渲染的 Markdown

您现在应该会看到 Colorist 应用,其中包含:

  1. 具有默认颜色的彩色显示区域
  2. 可输入消息的聊天界面
  3. 显示聊天互动的日志面板

尝试输入“I'd like a deep blue color”之类的消息,然后按“发送”。回声服务只会重复您的消息。在后续步骤中,您将使用 Firebase AI Logic 将其替换为实际的颜色解读。

后续操作

在下一步中,您将配置 Firebase 并实现基本的 Gemini API 集成,以使用 Gemini 对话服务替换回声服务。这样,应用便可以解读颜色描述并提供智能回答。

问题排查

界面软件包问题

如果您在安装 colorist_ui 软件包时遇到问题,请执行以下操作:

  • 确保您使用的是最新版本
  • 验证您是否已正确添加依赖项
  • 检查是否存在任何冲突的软件包版本

构建错误

如果您看到 build 错误,请执行以下操作:

  • 确保您已安装最新的稳定版 Flutter SDK
  • 运行 flutter clean,然后运行 flutter pub get
  • 检查控制台输出中是否有具体错误消息

学到的主要概念

  • 设置具有必要依赖项的 Flutter 项目
  • 了解应用的架构和组件职责
  • 实现一个模仿 LLM 行为的简单服务
  • 将服务连接到界面组件
  • 使用 Riverpod 进行状态管理

3. 基本 Gemini Chat 集成

在此步骤中,您将使用 Firebase AI Logic 将上一步中的 echo 服务替换为 Gemini API 集成。您将配置 Firebase、设置必要的提供程序,并实现与 Gemini API 通信的基本聊天服务。

您将在此步骤中学习的内容

  • 在 Flutter 应用中设置 Firebase
  • 配置 Firebase AI Logic 以访问 Gemini
  • 为 Firebase 和 Gemini 服务创建 Riverpod 提供程序
  • 使用 Gemini API 实现基本聊天服务
  • 处理异步 API 响应和错误状态

设置 Firebase

首先,您需要为 Flutter 项目设置 Firebase。这包括创建 Firebase 项目、将应用添加到该项目,以及配置必要的 Firebase AI Logic 设置。

创建 Firebase 项目

  1. 前往 Firebase 控制台,然后使用您的 Google 账号登录。
  2. 点击创建 Firebase 项目或选择现有项目。
  3. 按照设置向导创建项目。

在 Firebase 项目中设置 Firebase AI Logic

  1. 在 Firebase 控制台中,前往您的项目。
  2. 在左侧边栏中,选择 AI
  3. 在 AI 下拉菜单中,选择 AI 逻辑
  4. 在 Firebase AI Logic 卡片中,选择开始
  5. 按照提示为您的项目启用 Gemini Developer API。

安装 FlutterFire CLI

FlutterFire CLI 可简化 Flutter 应用中的 Firebase 设置:

dart pub global activate flutterfire_cli

将 Firebase 添加到您的 Flutter 应用

  1. 将 Firebase 核心和 Firebase AI Logic 软件包添加到您的项目中:
flutter pub add firebase_core firebase_ai
  1. 运行 FlutterFire 配置命令:
flutterfire configure

此命令将执行以下操作:

  • 要求您选择刚刚创建的 Firebase 项目
  • 在 Firebase 中注册您的 Flutter 应用
  • 生成包含项目配置的 firebase_options.dart 文件

该命令会自动检测您选择的平台(iOS、Android、macOS、Windows、Web),并对其进行相应配置。

针对具体平台的配置

Firebase 要求的最低版本高于 Flutter 的默认版本。此外,它还需要网络访问权限,以便与 Firebase AI Logic 服务器通信。

配置 macOS 权限

对于 macOS,您需要在应用的授权中启用网络访问权限:

  1. 打开 macos/Runner/DebugProfile.entitlements 并添加以下内容:

macos/Runner/DebugProfile.entitlements

<key>com.apple.security.network.client</key>
<true/>
  1. 同时打开 macos/Runner/Release.entitlements 并添加相同的条目。

配置 iOS 设置

对于 iOS,请更新 ios/Podfile 顶部的最低版本:

ios/Podfile

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

创建 Gemini 模型提供方

现在,您将为 Firebase 和 Gemini 创建 Riverpod 提供程序。创建新文件 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();
}

此文件定义了三个密钥提供程序的基础。这些提供程序是在您通过 Riverpod 代码生成器运行 dart run build_runner 时生成的。

  1. firebaseAppProvider:使用项目配置初始化 Firebase
  2. geminiModelProvider:创建 Gemini 生成式模型实例
  3. chatSessionProvider:创建并维护与 Gemini 模型的聊天会话

聊天会话上的 keepAlive: true 注释可确保聊天会话在整个应用生命周期内保持不变,从而维持对话上下文。

实现 Gemini Chat 服务

创建新文件 lib/services/gemini_chat_service.dart 以实现聊天服务:

lib/services/gemini_chat_service.dart

import 'dart:async';

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

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

part 'gemini_chat_service.g.dart';

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

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

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

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

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

此服务:

  1. 接受用户消息并将其发送到 Gemini API
  2. 使用模型的回答更新聊天界面
  3. 记录所有通信,以便轻松了解真实的 LLM 流
  4. 通过适当的用户反馈处理错误

注意:此时,“日志”窗口看起来与聊天窗口几乎完全相同。引入函数调用和流式响应后,日志会变得更有趣。

生成 Riverpod 代码

运行 build runner 命令以生成必要的 Riverpod 代码:

dart run build_runner build --delete-conflicting-outputs

这将创建 Riverpod 正常运行所需的 .g.dart 文件。

更新 main.dart 文件

更新 lib/main.dart 文件以使用新的 Gemini Chat 服务:

lib/main.dart

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

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

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

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

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

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

此更新中的主要变更如下:

  1. 将回显服务替换为基于 Gemini API 的聊天服务
  2. 使用 Riverpod 的 AsyncValue 模式和 when 方法添加加载和错误界面
  3. 通过 sendMessage 回调将界面连接到新的聊天服务

运行应用

使用以下命令运行应用:

flutter run -d DEVICE

DEVICE 替换为目标设备,例如 macoswindowschrome 或设备 ID。

Colorist 应用的屏幕截图,显示 Gemini LLM 对“阳光黄色”颜色请求的响应

现在,当您输入消息时,系统会将其发送到 Gemini API,您将收到 LLM 的回答,而不是回声。日志面板将显示与 API 的互动。

了解 LLM 通信

让我们花点时间了解一下与 Gemini API 通信时会发生什么情况:

通信流程

  1. 用户输入:用户在聊天界面中输入文本
  2. 请求格式设置:应用将文本格式设置为 Gemini API 的 Content 对象
  3. API 通信:文本通过 Firebase AI Logic 发送到 Gemini API
  4. LLM 处理:Gemini 模型处理文本并生成回答
  5. 处理响应:应用接收响应并更新界面
  6. 日志记录:所有通信都会记录在日志中,以确保透明度

聊天会话和对话上下文

Gemini 对话会话会在消息之间保留上下文,从而实现对话式互动。这意味着 LLM 会“记住”当前会话中的先前交流内容,从而实现更连贯的对话。

聊天会话提供程序上的 keepAlive: true 注释可确保此上下文在整个应用生命周期内保持不变。这种持久性上下文对于保持与 LLM 的自然对话流程至关重要。

后续操作

此时,您可以向 Gemini API 询问任何问题,它不会限制回答的内容。例如,您可以让它总结玫瑰战争,这与您的颜色应用的目的无关。

在下一步中,您将创建一个系统提示,以引导 Gemini 更有效地解读颜色描述。本教程将演示如何根据应用特定需求自定义 LLM 的行为,并将其功能重点放在应用的网域上。

问题排查

Firebase 配置问题

如果您在 Firebase 初始化时遇到错误,请执行以下操作:

  • 确保 firebase_options.dart 文件已正确生成
  • 确认您已升级到 Blaze 方案,以便访问 Firebase AI Logic

API 访问错误

如果您在访问 Gemini API 时收到错误,请执行以下操作:

  • 确认您的 Firebase 项目已正确设置结算信息
  • 检查 Firebase 项目中是否已启用 Firebase AI Logic 和 Cloud AI API
  • 检查网络连接和防火墙设置
  • 验证模型名称 (gemini-2.0-flash) 是否正确且可用

对话上下文问题

如果您发现 Gemini 无法记住对话中的先前上下文,请执行以下操作:

  • 确认 chatSession 函数带有 @Riverpod(keepAlive: true) 注释
  • 检查您是否在所有消息交换中重复使用同一聊天会话
  • 验证聊天会话是否已正确初始化,然后再发送消息

平台特有的问题

对于特定于平台的问题:

  • iOS/macOS:确保设置了适当的授权,并配置了最低版本
  • Android:验证最低 SDK 版本是否已正确设置
  • 在控制台中查看特定于平台的错误消息

学到的主要概念

  • 在 Flutter 应用中设置 Firebase
  • 配置 Firebase AI Logic 以访问 Gemini
  • 为异步服务创建 Riverpod 提供程序
  • 实现与 LLM 通信的聊天服务
  • 处理异步 API 状态(加载、错误、数据)
  • 了解 LLM 通信流程和聊天会话

4. 针对颜色描述的有效提示

在此步骤中,您将创建并实现一个系统提示,以指导 Gemini 解读颜色描述。系统提示是一种强大的方式,可用于针对特定任务自定义 LLM 行为,而无需更改代码。

您将在此步骤中学习的内容

  • 了解系统提示及其在 LLM 应用中的重要性
  • 针对特定领域任务精心设计有效的提示
  • 在 Flutter 应用中加载和使用系统提示
  • 引导 LLM 提供格式一致的回答
  • 测试系统提示如何影响 LLM 行为

了解系统提示

在深入了解实现之前,我们先来了解一下什么是系统提示以及它们为何如此重要:

什么是系统提示?

系统提示是一种特殊的指令,用于为 LLM 设置上下文、行为准则和回答预期。与用户消息不同,系统提示:

  • 确定 LLM 的角色和角色设定
  • 定义专业知识或能力
  • 提供格式设置说明
  • 设置回答限制
  • 描述如何处理各种场景

您可以将系统提示视为为 LLM 提供“职位说明”,它会告知模型在整个对话中的行为方式。

为什么系统提示很重要

系统提示对于创建一致且实用的 LLM 互动至关重要,因为它们:

  1. 确保一致性:引导模型以一致的格式提供回答
  2. 提高相关性:让模型专注于您的特定领域(在本例中为颜色)
  3. 设定界限:明确模型应该做什么和不应该做什么
  4. 提升用户体验:打造更自然、更实用的互动模式
  5. 减少后处理:以更易于解析或显示的格式获取回答

对于 Colorist 应用,您需要 LLM 以一致的方式解读颜色描述,并以特定格式提供 RGB 值。

创建系统提示素材资源

首先,您将创建一个在运行时加载的系统提示文件。这种方法可让您修改提示,而无需重新编译应用。

创建一个包含以下内容的新文件 assets/system_prompt.md

assets/system_prompt.md

# Colorist System Prompt

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

## Your Capabilities

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

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

## How to Respond to User Inputs

When users describe a color:

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

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

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

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

## When Descriptions are Unclear

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

## Important Guidelines

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

了解系统提示结构

我们来详细了解一下此提示的作用:

  1. 角色定义:将 LLM 确立为“色彩专家助理”
  2. 任务说明:将主要任务定义为将颜色描述解释为 RGB 值
  3. 响应格式:确切指定了 RGB 值的格式,以确保一致性
  4. 对话示例:提供预期互动模式的具体示例
  5. 边缘情况处理:说明如何处理不明确的说明
  6. 限制和准则:设置边界,例如将 RGB 值保持在 0.0 到 1.0 之间

这种结构化方法可确保 LLM 的回答一致、翔实,并且采用易于解析的格式,以便您以程序化方式提取 RGB 值。

更新 pubspec.yaml

现在,更新 pubspec.yaml 的底部,使其包含 assets 目录:

pubspec.yaml

flutter:
  uses-material-design: true

  assets:
    - assets/

运行 flutter pub get 以刷新资源包。

创建系统提示提供程序

创建新文件 lib/providers/system_prompt.dart 以加载系统提示:

lib/providers/system_prompt.dart

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

part 'system_prompt.g.dart';

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

此提供程序使用 Flutter 的资源加载系统在运行时读取提示文件。

更新 Gemini 模型提供方

现在,修改 lib/providers/gemini.dart 文件以包含系统提示:

lib/providers/gemini.dart

import 'dart:async';

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

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

part 'gemini.g.dart';

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

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

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

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

关键更改是在创建生成式模型时添加 systemInstruction: Content.system(systemPrompt)。这会指示 Gemini 将您的指令用作此聊天会话中所有互动的系统提示。

生成 Riverpod 代码

运行 build runner 命令以生成所需的 Riverpod 代码:

dart run build_runner build --delete-conflicting-outputs

运行并测试应用

现在运行您的应用:

flutter run -d DEVICE

Colorist 应用屏幕截图:显示 Gemini LLM 针对颜色选择应用以角色身份做出的回答

尝试使用各种颜色描述进行测试:

  • “I'd like a sky blue”
  • “给我一个森林绿”
  • “制作鲜艳的日落橙色”
  • “I want the color of fresh lavender”
  • “Show me something like a deep ocean blue”

您会发现,Gemini 现在会以对话方式说明颜色,并提供格式一致的 RGB 值。系统提示有效地引导 LLM 提供了您需要的回答类型。

您还可以尝试让它生成与颜色无关的内容。例如,玫瑰战争的主要原因。您应该会注意到与上一步的不同之处。

提示工程对专业任务的重要性

系统提示既是艺术,也是科学。它们是 LLM 集成的重要组成部分,可以极大地影响模型对特定应用的实用性。您在此处所做的操作是一种提示工程,即调整指令,使模型以适合应用需求的方式运行。

有效的提示工程包括:

  1. 明确的角色定义:确定 LLM 的用途
  2. 明确的指令:详细说明 LLM 应如何回答
  3. 具体示例:展示而非仅仅说明出色的回答是什么样的
  4. 极端情况处理:指示 LLM 如何处理模棱两可的情况
  5. 格式规范:确保回答以一致且实用的方式呈现

您创建的系统提示会将 Gemini 的通用功能转变为专业的色彩解读助理,该助理会提供专门针对您的应用需求而设置格式的回答。这是一种强大的模式,可应用于许多不同的领域和任务。

后续操作

在下一步中,您将在此基础上添加函数声明,使 LLM 不仅可以建议 RGB 值,还可以实际调用应用中的函数来直接设置颜色。这表明 LLM 如何弥合自然语言与具体应用功能之间的差距。

问题排查

素材资源加载问题

如果您在加载系统提示时遇到错误,请执行以下操作:

  • 验证您的 pubspec.yaml 是否正确列出了资源目录
  • 检查 rootBundle.loadString() 中的路径是否与您的文件位置一致
  • 运行 flutter clean,然后运行 flutter pub get 以刷新资源包

回答不一致

如果 LLM 未始终遵循您的格式说明,请尝试以下操作:

  • 尝试在系统提示中更明确地说明格式要求
  • 添加更多示例来展示预期模式
  • 确保您请求的格式对于模型而言是合理的

API 速率限制

如果您遇到与速率限制相关的错误,请执行以下操作:

  • 请注意,Firebase AI Logic 服务有使用限制
  • 考虑实现具有指数退避算法的重试逻辑
  • 在 Firebase 控制台中检查是否存在任何配额问题

学到的主要概念

  • 了解系统提示在 LLM 应用中的作用和重要性
  • 通过清晰的指令、示例和限制条件打造有效的提示
  • 在 Flutter 应用中加载和使用系统提示
  • 引导 LLM 针对特定领域任务的行为
  • 使用提示工程来调整 LLM 回答

此步骤演示了如何在不更改代码的情况下,仅通过在系统提示中提供明确的指令,实现对 LLM 行为的显著自定义。

5. LLM 工具的函数声明

在此步骤中,您将通过实现函数声明来开始启用 Gemini 在应用中执行操作。借助这项强大的功能,LLM 不仅可以建议 RGB 值,还可以通过专门的工具调用在应用的界面中实际设置这些值。不过,您需要执行下一步才能看到在 Flutter 应用中执行的 LLM 请求。

您将在此步骤中学习的内容

  • 了解 LLM 函数调用及其对 Flutter 应用的益处
  • 为 Gemini 定义基于架构的函数声明
  • 将函数声明与 Gemini 模型集成
  • 更新系统提示以利用工具功能

了解函数调用

在实现函数声明之前,我们先来了解一下函数声明是什么以及为什么它们很有价值:

什么是函数调用?

函数调用(有时称为“工具使用”)是一项功能,可让 LLM 执行以下操作:

  1. 识别用户请求何时会因调用特定函数而受益
  2. 生成一个结构化 JSON 对象,其中包含该函数所需的参数
  3. 让您的应用使用这些参数执行该函数
  4. 接收函数的结果并将其纳入响应中

函数调用功能可让 LLM 在您的应用中触发具体操作,而不仅仅是描述要做什么。

为什么函数调用对 Flutter 应用至关重要

函数调用在自然语言和应用功能之间架起了一座强大的桥梁:

  1. 直接操作:用户可以使用自然语言描述自己的需求,应用会以具体操作做出响应
  2. 结构化输出:LLM 生成的是干净的结构化数据,而不是需要解析的文本
  3. 复杂操作:使 LLM 能够访问外部数据、执行计算或修改应用状态
  4. 更出色的用户体验:在对话和功能之间实现无缝集成

在 Colorist 应用中,函数调用功能可让用户说出“我想要森林绿”,然后界面会立即更新为该颜色,而无需从文本中解析 RGB 值。

定义函数声明

创建新文件 lib/services/gemini_tools.dart 以定义函数声明:

lib/services/gemini_tools.dart

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

part 'gemini_tools.g.dart';

class GeminiTools {
  GeminiTools(this.ref);

  final Ref ref;

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

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

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

了解函数声明

我们来分析一下此代码的作用:

  1. 函数命名:您将函数命名为 set_color,以清晰表明其用途
  2. 函数说明:您需要提供清晰的说明,帮助 LLM 了解何时使用该函数
  3. 参数定义:您可以定义具有各自说明的结构化参数:
    • red:RGB 的红色分量,指定为介于 0.0 和 1.0 之间的数字
    • green:RGB 的绿色分量,指定为介于 0.0 和 1.0 之间的数字
    • blue:RGB 的蓝色分量,指定为介于 0.0 和 1.0 之间的数字
  4. 架构类型:您可以使用 Schema.number() 来指明这些是数值
  5. 工具集合:您创建一个包含函数声明的工具列表

这种结构化方法有助于 Gemini LLM 了解:

  • 何时应调用此函数
  • 需要提供哪些参数
  • 这些参数适用的限制(例如值范围)

更新 Gemini 模型提供方

现在,修改 lib/providers/gemini.dart 文件,以便在初始化 Gemini 模型时包含函数声明:

lib/providers/gemini.dart

import 'dart:async';

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

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

part 'gemini.g.dart';

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

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

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

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

关键变化是在创建生成式模型时添加了 tools: geminiTools.tools 参数。这样一来,Gemini 就能知道哪些函数可供其调用。

更新系统提示

现在,您需要修改系统提示,以指示 LLM 使用新的 set_color 工具。更新 assets/system_prompt.md

assets/system_prompt.md

# Colorist System Prompt

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

## Your Capabilities

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

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

## How to Respond to User Inputs

When users describe a color:

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

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

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

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

## When Descriptions are Unclear

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

## Important Guidelines

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

系统提示的主要变化如下:

  1. 工具介绍:现在,您无需请求格式化的 RGB 值,而是向 LLM 介绍 set_color 工具
  2. 修改后的流程:您将第 3 步从“设置回答中的值格式”更改为“使用该工具设置值”
  3. 更新后的示例:您展示了响应应如何包含工具调用,而不是格式化文本
  4. 移除了格式要求:由于您使用的是结构化函数调用,因此不再需要特定的文本格式

更新后的提示会指示 LLM 使用函数调用,而不是仅以文本形式提供 RGB 值。

生成 Riverpod 代码

运行 build runner 命令以生成所需的 Riverpod 代码:

dart run build_runner build --delete-conflicting-outputs

运行应用

此时,Gemini 将生成尝试使用函数调用的内容,但您尚未实现函数调用的处理程序。运行应用并描述颜色时,您会看到 Gemini 做出响应,就好像它已调用工具一样,但在执行下一步之前,您不会看到界面有任何颜色变化。

运行应用:

flutter run -d DEVICE

Colorist 应用的屏幕截图,显示 Gemini LLM 以部分响应进行回答

尝试描述一种颜色,例如“深海蓝”或“森林绿”,然后观察回答。LLM 正在尝试调用上面定义的函数,但您的代码尚未检测到函数调用。

函数调用流程

我们来了解一下 Gemini 使用函数调用时会发生什么情况:

  1. 函数选择:LLM 根据用户的请求决定函数调用是否有帮助
  2. 参数生成:LLM 生成符合函数架构的参数值
  3. 函数调用格式:LLM 会在其响应中发送结构化函数调用对象
  4. 应用处理:您的应用会收到此调用并执行相关函数(在下一步中实现)
  5. 响应集成:在多轮对话中,LLM 希望返回函数的结果

在应用的当前状态下,前三个步骤正在发生,但您尚未实现第 4 步或第 5 步(处理函数调用),这将在下一步中完成。

技术细节:Gemini 如何决定何时使用函数

Gemini 会根据以下因素智能决定何时使用函数:

  1. 用户意图:用户请求是否最好通过函数来满足
  2. 功能相关性:可用功能与任务的匹配程度
  3. 参数可用性:是否可以确定参数值
  4. 系统指令:系统提示中有关函数使用情况的指导信息

通过提供清晰的函数声明和系统指令,您已设置 Gemini,使其能够将颜色描述请求识别为调用 set_color 函数的机会。

后续操作

在下一步中,您将为来自 Gemini 的函数调用实现处理程序。这样就形成了一个完整的循环,让用户说明能够通过 LLM 的函数调用触发界面中的实际颜色变化。

问题排查

函数声明问题

如果您在函数声明方面遇到错误,请执行以下操作:

  • 检查参数名称和类型是否与预期一致
  • 验证函数名称是否清晰且具有描述性
  • 确保函数说明准确阐明了函数用途

系统提示问题

如果 LLM 未尝试使用该函数,请执行以下操作:

  • 验证系统提示是否明确指示 LLM 使用 set_color 工具
  • 检查系统提示中的示例是否演示了函数用法
  • 尝试让工具使用说明更明确

常见问题

如果您遇到其他问题,请执行以下操作:

  • 检查控制台中是否存在与函数声明相关的任何错误
  • 验证工具是否已正确传递给模型
  • 确保所有 Riverpod 生成的代码都是最新版本

学到的主要概念

  • 定义函数声明以扩展 Flutter 应用中的 LLM 功能
  • 为结构化数据收集创建参数架构
  • 将函数声明与 Gemini 模型集成
  • 更新了系统提示,以鼓励用户使用函数
  • 了解 LLM 如何选择和调用函数

此步骤演示了 LLM 如何弥合自然语言输入与结构化函数调用之间的差距,为对话与应用功能之间的无缝集成奠定基础。

6. 实现工具处理

在此步骤中,您将为来自 Gemini 的函数调用实现处理程序。这样就完成了自然语言输入与具体应用功能之间的通信闭环,使 LLM 能够根据用户描述直接操作界面。

您将在此步骤中学习的内容

  • 了解 LLM 应用中的完整函数调用流水线
  • 在 Flutter 应用中处理来自 Gemini 的函数调用
  • 实现可修改应用状态的函数处理程序
  • 处理函数响应并将结果返回给 LLM
  • 在 LLM 和界面之间创建完整的通信流
  • 记录函数调用和响应以提高透明度

了解函数调用流水线

在深入了解实现之前,我们先来了解完整的函数调用流水线:

端到端流程

  1. 用户输入:用户以自然语言描述颜色(例如,“森林绿”)
  2. LLM 处理:Gemini 分析说明并决定调用 set_color 函数
  3. 函数调用生成:Gemini 创建一个包含参数(红色、绿色、蓝色值)的结构化 JSON
  4. 函数调用接收:您的应用会从 Gemini 接收此结构化数据
  5. 函数执行:您的应用使用提供的参数执行函数
  6. 状态更新:该函数会更新应用的状态(更改显示的颜色)
  7. 生成回答:函数将结果返回给 LLM
  8. 纳入回答:LLM 会将这些结果纳入其最终回答中
  9. 界面更新:界面会对状态变化做出反应,显示新颜色

完整的通信周期对于正确集成 LLM 至关重要。当 LLM 进行函数调用时,它不会只是发送请求然后继续执行。相反,它会等待您的应用执行该函数并返回结果。然后,LLM 会使用这些结果来制定最终回答,从而形成自然的对话流程,并确认已采取的操作。

实现函数处理程序

让我们更新 lib/services/gemini_tools.dart 文件,以添加函数调用处理程序:

lib/services/gemini_tools.dart

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

part 'gemini_tools.g.dart';

class GeminiTools {
  GeminiTools(this.ref);

  final Ref ref;

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

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

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

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

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

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

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

了解函数处理程序

下面我们来详细了解一下这些函数处理程序的作用:

  1. handleFunctionCall:一个中央调度程序,用于:
    • 在日志面板中记录函数调用,以实现透明度
    • 根据函数名称将请求路由到相应的处理程序
    • 返回将发送回 LLM 的结构化响应
  2. handleSetColorset_color 函数的特定处理脚本,用于:
    • 从实参映射中提取 RGB 值
    • 将其转换为预期类型(双精度浮点数)
    • 使用 colorStateNotifier 更新应用的颜色状态
    • 创建包含成功状态和当前颜色信息的结构化响应
    • 记录函数结果以进行调试
  3. handleUnknownFunction:未知函数的后备处理程序,可执行以下操作:
    • 记录有关不受支持的函数的警告
    • 向 LLM 返回错误响应

handleSetColor 函数尤为重要,因为它弥合了 LLM 的自然语言理解能力与具体的界面更改之间的差距。

更新 Gemini Chat 服务以处理函数调用和响应

现在,我们来更新 lib/services/gemini_chat_service.dart 文件,以处理来自 LLM 响应的函数调用并将结果发送回 LLM:

lib/services/gemini_chat_service.dart

import 'dart:async';

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

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

part 'gemini_chat_service.g.dart';

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

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

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

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

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

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

了解沟通流程

这里新增的关键功能是可完整处理函数调用和响应:

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

此代码:

  1. 检查 LLM 回答是否包含任何函数调用
  2. 对于每次函数调用,都会使用函数名称和实参调用您的 handleFunctionCall 方法
  3. 收集每次函数调用的结果
  4. 使用 Content.functionResponses 将这些结果发送回 LLM
  5. 处理 LLM 对函数结果的响应
  6. 使用最终回答文本更新界面

这会创建一个往返流程:

  • 用户 → LLM:请求颜色
  • LLM → 应用:带参数的函数调用
  • 应用 → 用户:显示新颜色
  • 应用 → LLM:函数结果
  • LLM → 用户:包含函数结果的最终回答

生成 Riverpod 代码

运行 build runner 命令以生成所需的 Riverpod 代码:

dart run build_runner build --delete-conflicting-outputs

运行并测试完整流程

现在运行您的应用:

flutter run -d DEVICE

Colorist 应用的屏幕截图,显示了 Gemini LLM 通过函数调用进行响应

尝试输入各种颜色描述:

  • “我想要深红色”
  • “显示令人平静的天蓝色”
  • “给我新鲜薄荷叶的颜色”
  • “我想看看暖日落橙色”
  • “将颜色设为浓郁的皇家紫色”

现在,您应该会看到:

  1. 您的消息显示在聊天界面中
  2. Gemini 在对话中显示的回答
  3. 日志面板中记录的函数调用
  4. 在函数执行后立即记录函数结果
  5. 颜色矩形更新为显示所描述的颜色
  6. RGB 值更新为显示新颜色的分量
  7. 显示 Gemini 的最终回答,通常会评论设置的颜色

日志面板可帮助您了解幕后发生的情况。您会看到:

  • Gemini 正在进行的具体函数调用
  • 它为每个 RGB 值选择的形参
  • 函数返回的结果
  • Gemini 的后续回答

颜色状态通知程序

您用于更新颜色的 colorStateNotifier 属于 colorist_ui 软件包。它会管理:

  • 界面中显示的当前颜色
  • 颜色历史记录(最近 10 种颜色)
  • 向界面组件发送状态变化通知

当您使用新的 RGB 值调用 updateColor 时,它会:

  1. 使用提供的值创建新的 ColorData 对象
  2. 更新应用状态中的当前颜色
  3. 将颜色添加到历史记录中
  4. 通过 Riverpod 的状态管理触发界面更新

colorist_ui 软件包中的界面组件会监控此状态,并在状态发生变化时自动更新,从而打造出响应式体验。

了解错误处理

您的实现包含强大的错误处理功能:

  1. try-catch 代码块:封装所有 LLM 互动以捕获任何异常
  2. 错误记录:在日志面板中记录包含堆栈轨迹的错误
  3. 用户反馈:在对话中提供友好的错误消息
  4. 状态清理:即使发生错误,也会完成消息状态

这可确保应用保持稳定,即使 LLM 服务或函数执行出现问题,也能提供适当的反馈。

函数调用对用户体验的强大作用

您在此处完成的操作展示了 LLM 如何创建强大的自然接口:

  1. 自然语言界面:用户使用日常语言表达意图
  2. 智能解读:LLM 将模糊的描述转换为精确的值
  3. 直接操作:界面会根据自然语言进行更新
  4. 情境化回答:LLM 提供有关更改的对话情境
  5. 认知负荷低:用户无需了解 RGB 值或色彩理论

这种使用 LLM 函数调用来连接自然语言和界面操作的模式可以扩展到颜色选择之外的无数其他领域。

后续操作

在下一步中,您将通过实现流式响应来提升用户体验。您无需等待完整的响应,而是会在收到文本块和函数调用时立即进行处理,从而打造更具响应性和吸引力的应用。

问题排查

函数调用问题

如果 Gemini 未调用您的函数或参数不正确,请执行以下操作:

  • 验证函数声明是否与系统提示中的描述一致
  • 检查参数名称和类型是否一致
  • 确保系统提示明确指示 LLM 使用该工具
  • 验证处理程序中的函数名称是否与声明中的名称完全一致
  • 检查日志面板,了解有关函数调用的详细信息

函数响应问题

如果函数结果未正确发送回 LLM,请执行以下操作:

  • 检查函数是否返回格式正确的 Map
  • 验证 Content.functionResponses 是否正在正确构建
  • 在日志中查找与函数响应相关的任何错误
  • 确保您在同一聊天会话中生成回答

色彩显示问题

如果颜色显示异常,请执行以下操作:

  • 确保 RGB 值已正确转换为双精度浮点数(LLM 可能会将其作为整数发送)
  • 验证值是否在预期范围内(0.0 到 1.0)
  • 检查颜色状态通知程序是否被正确调用
  • 检查日志,了解传递给函数的具体值

一般问题

对于一般问题:

  • 检查日志中是否存在错误或警告
  • 验证 Firebase AI Logic 连接
  • 检查函数参数中是否存在任何类型不匹配的情况
  • 确保所有 Riverpod 生成的代码都是最新版本

学到的主要概念

  • 在 Flutter 中实现完整的函数调用流水线
  • 在 LLM 与应用之间建立完整的通信
  • 处理 LLM 回答中的结构化数据
  • 将函数结果发送回 LLM 以纳入回答中
  • 使用日志面板深入了解 LLM-应用互动
  • 将自然语言输入与具体的界面更改相关联

完成此步骤后,您的应用现在展示了 LLM 集成最强大的模式之一:将自然语言输入转换为具体的界面操作,同时保持连贯的对话来确认这些操作。这会打造出直观的对话式界面,让用户感觉非常神奇。

7. 流式响应,可改善用户体验

在此步骤中,您将通过实现来自 Gemini 的流式回答来提升用户体验。您将处理收到的文本块和函数调用,而不是等待生成整个响应,从而打造更具响应性和吸引力的应用。

您将在本步骤中学习的内容

  • 流式传输对于依托 LLM 的应用的重要性
  • 在 Flutter 应用中实现流式 LLM 回答
  • 处理从 API 接收的部分文本块
  • 管理对话状态以防止消息冲突
  • 处理流式传输响应中的函数调用
  • 为正在进行的回答创建视觉指示器

为什么流式传输对 LLM 应用至关重要

在实现之前,我们先来了解一下为什么流式传输响应对于使用 LLM 打造出色的用户体验至关重要:

改善用户体验

流式回答可带来多项显著的用户体验优势:

  1. 缩短了感知延迟时间:用户会立即看到文本开始显示(通常在 100-300 毫秒内),而无需等待几秒钟才能看到完整响应。这种即时性感知可大幅提高用户满意度。
  2. 自然的对话节奏:文字的逐步显示模仿了人类的交流方式,营造出更自然的对话体验。
  3. 渐进式信息处理:用户可以在信息到达时开始处理,而不是一次性处理大量文本,以免不知所措。
  4. 可提前中断:在完整应用中,如果用户发现 LLM 的回答方向不正确,可能会中断或重定向 LLM。
  5. 直观的活动确认:流式文本可提供有关系统正在运行的即时反馈,从而减少不确定性。

技术优势

除了改进用户体验之外,流式传输还具有以下技术优势:

  1. 提前执行函数:系统可以在函数调用出现在流中时立即检测并执行这些函数调用,而无需等待完整响应。
  2. 增量界面更新:您可以随着新信息的到来逐步更新界面,从而打造更动态的体验。
  3. 对话状态管理:流式传输可清晰指示响应何时完成,何时仍在进行中,从而实现更好的状态管理。
  4. 降低了超时风险:对于非流式回答,长时间运行的生成操作可能会导致连接超时。流式传输会提前建立连接并保持连接。

对于 Colorist 应用,实现流式传输意味着用户将能更快地看到文本回答和颜色变化,从而获得更快的响应速度。

添加对话状态管理

首先,我们添加一个状态提供程序,以跟踪应用当前是否正在处理流式传输响应。更新 lib/services/gemini_chat_service.dart 文件:

lib/services/gemini_chat_service.dart

import 'dart:async';

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

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

part 'gemini_chat_service.g.dart';

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

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

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

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

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

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

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

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

了解流式实现

我们来分析一下此代码的作用:

  1. 对话状态跟踪
    • conversationStateProvider 用于跟踪应用当前是否正在处理响应
    • 在处理过程中,状态从 idle 转换为 busy,然后返回到 idle
    • 这样可以防止可能发生冲突的多个并发请求
  2. 数据流初始化
    • sendMessageStream() 返回响应块的 Stream,而不是包含完整响应的 Future
    • 每个块可能包含文本、函数调用或两者兼而有之
  3. 渐进式处理
    • await for 会在每个数据块到达时实时处理
    • 文本会立即附加到界面,从而产生流式效果
    • 检测到函数调用后,系统会立即执行
  4. 函数调用处理
    • 当在代码块中检测到函数调用时,系统会立即执行该函数
    • 结果通过另一个流式调用发送回 LLM
    • LLM 对这些结果的回答也会以流式方式进行处理
  5. 错误处理和清理
    • try/catch 提供强大的错误处理功能
    • finally 代码块可确保对话状态得到正确重置
    • 即使发生错误,消息也始终会最终确定

此实现可创建响应迅速、可靠的流式传输体验,同时保持适当的对话状态。

更新主屏幕以连接对话状态

修改 lib/main.dart 文件,以将对话状态传递给主屏幕:

lib/main.dart

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

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

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

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

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

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

此处的关键变化是将 conversationState 传递给 MainScreen widget。MainScreen(由 colorist_ui 软件包提供)将使用此状态在处理响应时停用文本输入。

这样可以打造一致的用户体验,让界面反映对话的当前状态。

生成 Riverpod 代码

运行 build runner 命令以生成所需的 Riverpod 代码:

dart run build_runner build --delete-conflicting-outputs

运行并测试流式回答

运行您的应用:

flutter run -d DEVICE

Colorist 应用屏幕截图,显示 Gemini LLM 以流式方式回答问题

现在,尝试使用各种颜色描述来测试流式传输行为。尝试使用以下描述:

  • “Show me the deep teal color of the ocean at twilight”
  • “我想看看色彩鲜艳的珊瑚,让我想起热带花卉”
  • “创建一种像旧军装一样的柔和橄榄绿”

流式传输技术流程详情

我们来详细了解一下在流式传输回答时会发生什么情况:

连接建立

当您调用 sendMessageStream() 时,系统会执行以下操作:

  1. 应用与 Firebase AI Logic 服务建立连接
  2. 用户请求已发送给服务
  3. 服务器开始处理请求
  4. 流连接保持打开状态,随时可以传输块

块传输

当 Gemini 生成内容时,系统会通过数据流发送块:

  1. 服务器在生成文本块时会立即发送(通常是几个字或几句话)
  2. 当 Gemini 决定进行函数调用时,它会发送函数调用信息
  3. 函数调用后可能会有其他文本块
  4. 该流会一直持续,直到生成完成

渐进式处理

您的应用会以增量方式处理每个数据块:

  1. 每个文本块都会附加到现有回答中
  2. 检测到函数调用后,系统会立即执行
  3. 界面会实时更新,显示文本和函数结果
  4. 跟踪状态以显示响应仍在流式传输

流式传输完成

生成完成后:

  1. 服务器关闭了流
  2. 您的 await for 循环自然退出
  3. 消息标记为已完成
  4. 对话状态已重新设置为闲置
  5. 界面会更新以反映已完成状态

流式回答与非流式回答的比较

为了更好地了解流式传输的优势,我们来比较一下流式传输与非流式传输方法:

方面

非流式传输

流式

感知延迟时间

在完整响应准备就绪之前,用户看不到任何内容

用户在几毫秒内即可看到第一个字词

用户体验

长时间等待后突然显示文字

自然、渐进的文字显示效果

状态管理

更简单(消息要么处于待处理状态,要么处于已完成状态)

更复杂(消息可以处于流式传输状态)

函数执行

仅在完整响应后发生

在生成回答期间发生

实现复杂性

更易于实现

需要额外的状态管理

错误恢复

全有或全无响应

部分回答可能仍然有用

代码复杂度

更简单

由于数据流处理而更加复杂

对于 Colorist 这类应用,流式传输带来的用户体验优势远胜于实现复杂性,尤其是对于可能需要几秒钟才能生成的色彩解读。

流式传输用户体验最佳实践

在您自己的 LLM 应用中实现流式传输时,请考虑以下最佳实践:

  1. 清晰的视觉指示器:始终提供清晰的视觉提示,以区分流式消息和完整消息
  2. 输入屏蔽:在流式传输期间停用用户输入,以防止出现多个重叠的请求
  3. 错误恢复:设计界面以在流式传输中断时妥善恢复
  4. 状态转换:确保在空闲、流式传输和完成状态之间顺畅转换
  5. 进度可视化:考虑使用细微的动画或指示器来显示正在进行的处理
  6. 取消选项:在完整的应用中,提供让用户取消正在进行生成的选项
  7. 函数结果集成:设计界面以处理中途出现的函数结果
  8. 性能优化:在快速更新流期间最大限度地减少界面重建

colorist_ui 软件包为您实现了许多此类最佳实践,但对于任何流式 LLM 实现,这些都是重要的考虑因素。

后续操作

在下一步中,您将通过在用户从历史记录中选择颜色时通知 Gemini 来实现 LLM 同步。这样一来,LLM 就能感知用户发起的应用状态更改,从而打造更具凝聚力的体验。

问题排查

流处理问题

如果您在流处理方面遇到问题,请执行以下操作:

  • 症状:部分响应、文本缺失或流突然终止
  • 解决方案:检查网络连接,并确保代码中使用了正确的异步/等待模式
  • 诊断:检查日志面板中是否存在与视频流处理相关的错误消息或警告
  • 修复:确保所有流处理都使用包含 try/catch 块的适当错误处理

缺少函数调用

如果系统未在数据流中检测到函数调用,请执行以下操作:

  • 症状:显示文本,但颜色未更新;或者日志显示没有函数调用
  • 解决方案:验证系统提示中有关使用函数调用的说明
  • 诊断:检查日志面板,看看是否收到了函数调用
  • 修复:调整系统提示,更明确地指示 LLM 使用 set_color 工具

常规错误处理

对于任何其他问题:

  • 第 1 步:查看日志面板中是否有错误消息
  • 第 2 步:验证 Firebase AI Logic 连接
  • 第 3 步:确保所有 Riverpod 生成的代码都是最新版本
  • 第 4 步:检查流式传输实现中是否有任何缺失的 await 语句

学到的主要概念

  • 使用 Gemini API 实现流式传输响应,以提供更快的用户体验
  • 管理对话状态以正确处理流式互动
  • 实时处理文本和函数调用
  • 创建在流式传输期间以增量方式更新的自适应界面
  • 使用适当的异步模式处理并发声音流
  • 在流式传输响应期间提供适当的视觉反馈

通过实现流式传输,您显著提升了 Colorist 应用的用户体验,打造了响应速度更快、更具吸引力的界面,让用户感觉像是在进行真正的对话。

8. LLM 上下文同步

在此奖励步骤中,您将实现 LLM 上下文同步,即在用户从历史记录中选择颜色时通知 Gemini。这样一来,LLM 不仅能了解用户的明确消息,还能感知用户在界面中的操作,从而打造更具凝聚力的体验。

您将在本步骤中学习的内容

  • 在界面和 LLM 之间创建 LLM 上下文同步
  • 将界面事件序列化为 LLM 可理解的上下文
  • 根据用户操作更新对话上下文
  • 通过不同的互动方式打造一致的体验
  • 增强 LLM 上下文感知能力,使其不仅限于明确的聊天消息

了解 LLM 上下文同步

传统聊天机器人仅响应明确的用户消息,当用户通过其他方式与应用互动时,聊天机器人无法响应。LLM 上下文同步功能可解决此限制:

LLM 上下文同步的重要性

当用户通过界面元素(例如从历史记录中选择颜色)与应用互动时,除非您明确告知 LLM,否则它无法知道发生了什么。LLM 上下文同步:

  1. 保持上下文:让 LLM 了解所有相关的用户操作
  2. 打造连贯性:打造连贯的体验,让 LLM 能够识别界面互动
  3. 增强智能性:使 LLM 能够针对所有用户操作做出适当的响应
  4. 提升用户体验:让整个应用感觉更加集成和响应迅速
  5. 减少用户操作:无需用户手动说明其界面操作

在 Colorist 应用中,当用户从历史记录中选择颜色时,您希望 Gemini 能够识别此操作并针对所选颜色做出智能评论,从而营造出流畅且智能的助理体验。

更新了 Gemini Chat 服务,以显示颜色选择通知

首先,您将向 GeminiChatService 添加一个方法,以便在用户从历史记录中选择颜色时通知 LLM。更新 lib/services/gemini_chat_service.dart 文件:

lib/services/gemini_chat_service.dart

import 'dart:async';
import 'dart:convert';                                               // Add this import

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

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

part 'gemini_chat_service.g.dart';

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

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

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

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

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

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

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

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

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

新增的关键方法是 notifyColorSelection,该方法:

  1. 接受表示所选颜色的 ColorData 对象
  2. 将其编码为可包含在消息中的 JSON 格式
  3. 向 LLM 发送一条特殊格式的消息,表明用户做出了选择
  4. 重用现有的 sendMessage 方法来处理通知

此方法利用现有的消息处理基础架构来避免重复。

更新主应用以连接颜色选择通知

现在,修改您的 lib/main.dart 文件,以将颜色选择通知函数传递给主屏幕:

lib/main.dart

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

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

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

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

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

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

关键更改是添加了 notifyColorSelection 回调,该回调将界面事件(从历史记录中选择颜色)连接到 LLM 通知系统。

更新系统提示

现在,您需要更新系统提示,以指示 LLM 如何响应颜色选择通知。修改 assets/system_prompt.md 文件:

assets/system_prompt.md

# Colorist System Prompt

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

## Your Capabilities

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

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

## How to Respond to User Inputs

When users describe a color:

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

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

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

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

## When Descriptions are Unclear

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

## When Users Select Historical Colors

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

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

## Important Guidelines

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

新增的关键部分是“当用户选择历史颜色时”,其中:

  1. 向 LLM 解释历史记录选择通知的概念
  2. 提供这些通知的显示示例
  3. 显示适当响应的示例
  4. 就确认选择和评论颜色设定预期

这有助于 LLM 了解如何恰当回应这些特殊消息。

生成 Riverpod 代码

运行 build runner 命令以生成所需的 Riverpod 代码:

dart run build_runner build --delete-conflicting-outputs

运行并测试 LLM 上下文同步

运行您的应用:

flutter run -d DEVICE

Colorist 应用的屏幕截图,显示 Gemini LLM 对从颜色历史记录中选择的颜色做出的响应

测试 LLM 上下文同步涉及以下步骤:

  1. 首先,在聊天中描述几种颜色,生成这些颜色
    • “Show me a vibrant purple”(显示鲜艳的紫色)
    • “I'd like a forest green”
    • “Give me a bright red”
  2. 然后,点击历史记录栏中的某个颜色缩略图

您应观察:

  1. 所选颜色会显示在主显示屏上
  2. 聊天中会显示一条用户消息,指明所选颜色
  3. LLM 会通过确认所选颜色并对该颜色发表评论来做出回应
  4. 整个互动过程自然流畅

这样一来,LLM 就能感知并适当响应直接消息和界面互动,从而打造顺畅的体验。

LLM 上下文同步的运作方式

我们来详细了解一下此同步功能的运作方式:

数据流

  1. 用户操作:用户点击历史记录条带中的颜色
  2. 界面事件MainScreen widget 会检测到此选择
  3. 回调执行:触发 notifyColorSelection 回调
  4. 消息创建:系统会使用颜色数据创建特殊格式的消息
  5. LLM 处理:消息会发送给 Gemini,后者会识别格式
  6. 情境化回答:Gemini 会根据系统提示给出适当的回答
  7. 界面更新:回答会显示在聊天中,从而打造连贯的体验

数据序列化

此方法的关键方面在于如何序列化颜色数据:

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

toLLMContextMap() 方法(由 colorist_ui 软件包提供)将 ColorData 对象转换为 LLM 可以理解的具有键属性的映射。这通常包括:

  • RGB 值(红色、绿色、蓝色)
  • 十六进制代码表示形式
  • 与颜色关联的任何名称或说明

通过以一致的方式设置这些数据的格式并将其包含在消息中,您可以确保 LLM 拥有做出适当回答所需的所有信息。

LLM 上下文同步的更广泛应用

这种通知 LLM 界面事件的模式除了颜色选择之外,还有许多其他应用:

其他用例

  1. 过滤条件更改:在用户对数据应用过滤条件时通知 LLM
  2. 导航事件:在用户导航到不同部分时通知 LLM
  3. 选择发生变化:当用户从列表或网格中选择项时,更新 LLM
  4. 偏好设置更新:告知 LLM 用户何时更改了设置或偏好设置
  5. 数据操纵:在用户添加、修改或删除数据时通知 LLM

在每种情况下,模式都保持不变:

  1. 检测界面事件
  2. 序列化相关数据
  3. 向 LLM 发送特殊格式的通知
  4. 通过系统提示引导 LLM 做出适当的回答

LLM 上下文同步的最佳实践

根据您的实现情况,以下是一些可有效实现 LLM 上下文同步的最佳实践:

1. 采用风格一致的内容形式

使用一致的通知格式,以便 LLM 轻松识别:

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

2. 丰富的上下文

在通知中包含足够的详细信息,以便 LLM 做出智能响应。对于颜色,这意味着 RGB 值、十六进制代码和任何其他相关属性。

3. 清晰的说明

在系统提示中提供有关如何处理通知的明确说明,最好附上示例。

4. 自然集成

设计通知时应使其在对话中自然显示,而不是以技术中断的形式显示。

5. 选择性通知

仅将与对话相关的操作通知 LLM。并非每个界面事件都需要进行通信。

问题排查

通知问题

如果 LLM 未正确响应颜色选择,请执行以下操作:

  • 检查通知消息格式是否与系统提示中所述的格式一致
  • 验证颜色数据是否已正确序列化
  • 确保系统提示包含有关如何处理选择的明确说明
  • 在发送通知时查找聊天服务中的任何错误

上下文管理

如果 LLM 似乎丢失了上下文:

  • 检查聊天会话是否得到妥善维护
  • 验证对话状态是否正确转换
  • 确保通知通过同一聊天会话发送

一般问题

对于一般问题:

  • 检查日志中是否存在错误或警告
  • 验证 Firebase AI Logic 连接
  • 检查函数参数中是否存在任何类型不匹配的情况
  • 确保所有 Riverpod 生成的代码都是最新版本

学到的主要概念

  • 在界面和 LLM 之间创建 LLM 上下文同步
  • 将界面事件序列化为 LLM 友好型上下文
  • 针对不同的互动模式指导 LLM 行为
  • 在消息互动和非消息互动中打造一致的体验
  • 增强 LLM 对更广泛应用状态的感知能力

通过实现 LLM 上下文同步,您打造了真正集成化的体验,让 LLM 感觉像是一个有感知能力、响应迅速的助理,而不仅仅是一个文本生成器。此模式可应用于无数其他应用,以创建更自然、更直观的 AI 赋能界面。

9. 恭喜!

您已成功完成 Colorist Codelab!🎉

您构建的内容

您已创建一款功能完备的 Flutter 应用,该应用集成了 Google 的 Gemini API,可用于解读自然语言颜色说明。您的应用现在可以:

  • 处理“日落橙”或“深海蓝”等自然语言描述
  • 使用 Gemini 将这些说明智能翻译为 RGB 值
  • 通过流式响应实时显示解读的颜色
  • 通过聊天和界面元素处理用户互动
  • 在不同互动方法之间保持情境感知

后续步骤

现在,您已掌握将 Gemini 与 Flutter 集成的基础知识,不妨通过以下方式继续学习:

增强 Colorist 应用

  • 调色板:添加生成互补或匹配配色方案的功能
  • 语音输入:集成语音识别功能,用于识别口头颜色描述
  • 历史记录管理:添加了用于命名、整理和导出颜色集的选项
  • 自定义提示:创建界面,供用户自定义系统提示
  • 高级分析:跟踪哪些说明效果最好或导致了困难

探索更多 Gemini 功能

  • 多模态输入:添加图片输入以从照片中提取颜色
  • 内容生成:使用 Gemini 生成与颜色相关的内容,例如说明或故事
  • 函数调用增强功能:创建具有多个功能的更复杂的工具集成
  • 安全设置:了解不同的安全设置及其对回答的影响

将这些模式应用到其他网域

  • 文档分析:创建可理解和分析文档的应用
  • 创意写作辅助:构建具有 LLM 赋能的建议功能的写作工具
  • 任务自动化:设计可将自然语言转换为自动化任务的应用
  • 基于知识的应用:在特定领域创建专家系统

资源

以下是一些宝贵资源,可帮助您继续学习:

官方文档

提示课程和指南

社区

Observable Flutter Agentic 系列

在第 59 集中,Craig Labenz 和 Andrew Brogden 探索了此 Codelab,重点介绍了应用构建的有趣部分。

在第 60 集中,再次加入 Craig 和 Andrew 的行列,了解他们如何通过新功能扩展 Codelab 应用,以及如何让 LLM 按照他们的指示执行操作。

在第 61 集节目中,Craig 与 Chris Sells 一起以全新视角分析新闻标题并生成相应的图片。

反馈

我们衷心期待您与我们分享学习此 Codelab 的体验!请考虑通过以下方式提供反馈:

感谢您完成此 Codelab,我们希望您继续探索 Flutter 和 AI 交叉领域令人兴奋的可能性!