构建由 Gemini 赋能的 Flutter 应用

构建由 Gemini 赋能的 Flutter 应用

关于此 Codelab

subject上次更新时间:6月 3, 2025
account_circleBrett Morgan 编写

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 的视频概览

观看 Craig Labenz 和 Andrew Brogdon 在 Observable Flutter 播客第 59 集中讨论此 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 应用的空项目。该应用可在桌面设备、移动设备和网站上运行。不过,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 custom_lint

这会添加以下密钥软件包:

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

您的 pubspec.yaml 将如下所示:

pubspec.yaml

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

environment:
  sdk: ^3.8.0

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

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

flutter:
  uses-material-design: true

配置分析选项

custom_lint 添加到项目根目录下的 analysis_options.yaml 文件中:

include: package:flutter_lints/flutter.yaml

analyzer:
  plugins:
    - custom_lint

此配置启用了 Riverpod 专用 lint,有助于维护代码质量。

实现 main.dart 文件

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

lib/main.dart

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

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

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

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

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

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

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

了解架构

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

colorist_ui 软件包

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

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

应用架构

该应用遵循以下架构:

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

通过这种分离,您可以专注于实现 LLM 集成,而界面组件已由系统处理。

运行应用

使用以下命令运行应用:

flutter run -d DEVICE

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

显示“Colorist”应用的屏幕截图,其中显示了“Echo”服务正在渲染 Markdown

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

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

试着输入消息,例如“我想要深蓝色”,然后按“发送”。回声服务只会重复您的消息。在后续步骤中,您将使用 Firebase AI Logic 将此值替换为实际的颜色解读。

后续操作

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

问题排查

界面软件包问题

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

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

构建错误

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

  • 确保您已安装最新的稳定版渠道 Flutter SDK
  • 依次运行 flutter cleanflutter pub get
  • 检查控制台输出,查找具体错误消息

学到的关键概念

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

3. 基本 Gemini Chat 集成

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

本步骤将介绍的内容

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

设置 Firebase

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

创建 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 并添加相同的条目。
  2. 更新 macos/Podfile 顶部的最低 macOS 版本:

macos/Podfile

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

配置 iOS 权限

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

ios/Podfile

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

配置 Android 设置

对于 Android,请更新 android/app/build.gradle.kts

android/app/build.gradle.kts

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

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

创建 Gemini 模型提供程序

现在,您将为 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:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

import '../firebase_options.dart';

part 'gemini.g.dart';

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

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

 
final model = 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:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

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

part 'gemini_chat_service.g.dart';

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

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

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

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

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

此服务:

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

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

生成 Riverpod 代码

运行 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。

显示 Gemini LLM 响应阳光黄色请求的 Colorist 应用屏幕截图

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

了解 LLM 通信

我们先来了解一下与 Gemini API 通信时会发生的情况:

通信流程

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

聊天会话和对话上下文

Gemini Chat 会话会在消息之间保留上下文,从而实现对话式互动。这意味着 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 以刷新资源 bundle。

创建系统提示提供程序

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

lib/providers/system_prompt.dart

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

part 'system_prompt.g.dart';

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

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

更新 Gemini 模型提供程序

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

lib/providers/gemini.dart

import 'dart:async';

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

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

part 'gemini.g.dart';

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

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

 
final model = 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

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

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

  • “我想要天蓝色”
  • “给我一个森林绿”
  • “制作艳丽的日落橙色”
  • “I want the color of fresh lavender”
  • “给我看看深海蓝之类的颜色”

您应该会注意到,Gemini 现在会以对话方式说明颜色,并提供格式一致的 RGB 值。系统提示有效引导了 LLM,使其提供您所需的回答类型。

此外,还可以尝试询问颜色以外的内容。例如,玫瑰战争的主要原因。您应该会注意到与上一步的差异。

针对专门任务进行提示工程的重要性

系统提示既是艺术,也是科学。它们是 LLM 集成的关键部分,可能会极大地影响模型对您的特定应用的实用性。您在这里所做的是一种提示工程 - 量身定制指令,使模型的行为方式符合应用的需求。

有效的提示工程涉及以下方面:

  1. 明确的角色定义:确定 LLM 的用途
  2. 明确的指令:详细说明 LLM 应如何做出回答
  3. 具体示例:以展示而不是仅仅说明的方式说明什么样的回答是好的
  4. 极端情况处理:指示 LLM 如何处理模糊的情况
  5. 格式规范:确保回答的结构一致且易于使用

您创建的系统提示会将 Gemini 的通用功能转换为专门的色彩解读助理,以便根据应用的需求提供格式专属的回答。这是一种强大的模式,可应用于许多不同的领域和任务。

后续操作

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

问题排查

资源加载问题

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

  • 验证 pubspec.yaml 是否正确列出了资源目录
  • 检查 rootBundle.loadString() 中的路径是否与文件位置相符
  • 依次运行 flutter cleanflutter pub get 以刷新资源 bundle

回复不一致

如果 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:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'gemini_tools.g.dart';

class GeminiTools {
 
GeminiTools(this.ref);

 
final Ref ref;

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

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

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

了解函数声明

我们来分析一下此代码的功能:

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

这种结构化方法有助于 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:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

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

part 'gemini.g.dart';

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

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

 
final model = 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. 工具简介:现在,您可以告知 LLM set_color 工具,而不是请求格式化的 RGB 值
  2. 修改后的过程:您将第 3 步从“设置响应中的值的格式”更改为“使用该工具设置值”
  3. 更新后的示例:您展示了响应应如何包含工具调用,而不是格式化文本
  4. 移除了格式要求:由于您使用的是结构化函数调用,因此无需再使用特定文本格式

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

生成 Riverpod 代码

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

dart run build_runner build --delete-conflicting-outputs

运行应用

此时,Gemini 会生成尝试使用函数调用的文本,但您尚未实现函数调用的处理脚本。运行应用并描述颜色后,您会看到 Gemini 的响应,就像它调用了某个工具一样,但在下一步之前,您不会在界面中看到任何颜色变化。

运行应用:

flutter run -d DEVICE

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

尝试描述“深蓝色”或“森林绿”等颜色,然后观察系统的回答。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. 用户输入:用户用自然语言描述颜色(例如“forest green”)
  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:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'gemini_tools.g.dart';

class GeminiTools {
 
GeminiTools(this.ref);

 
final Ref ref;

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

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

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

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

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

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

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

了解函数处理脚本

我们来详细了解一下这些函数处理脚本的用途:

  1. handleFunctionCall:一个中央调度程序,具有以下功能:
    • 在日志面板中记录透明度的函数调用
    • 根据函数名称路由到相应的处理程序
    • 返回将发回给 LLM 的结构化回答
  2. handleSetColorset_color 函数的特定处理脚本,具有以下功能:
    • 从参数映射中提取 RGB 值
    • 将它们转换为预期类型(double)
    • 使用 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:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

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

part 'gemini_chat_service.g.dart';

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

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

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

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

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

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

了解通信流

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

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

以下代码:

  1. 检查 LLM 响应是否包含任何函数调用
  2. 对于每次函数调用,使用函数名称和参数调用 handleFunctionCall 方法
  3. 收集每次函数调用的结果
  4. 使用 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

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

尝试输入各种颜色描述:

  • “我想要深红色”
  • “给我显示舒缓的天空蓝”
  • “给我显示新鲜薄荷叶的颜色”
  • “我想看温暖的夕阳橙色”
  • “用深紫色”

现在,您应该会看到:

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

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

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

颜色状态通知器

您用于更新颜色的 colorStateNotifiercolorist_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/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

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

part 'gemini_chat_service.g.dart';

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

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

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

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

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

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

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

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

了解流式传输实现

我们来分析一下此代码的功能:

  1. 会话状态跟踪
    • conversationStateProvider 用于跟踪应用当前是否正在处理响应
    • 状态在处理期间从 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 微件。MainScreen(由 colorist_ui 软件包提供)将使用此状态在处理响应时停用文本输入。

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

生成 Riverpod 代码

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

dart run build_runner build --delete-conflicting-outputs

运行和测试流式回答

运行您的应用:

flutter run -d DEVICE

显示 Gemini LLM 以流式方式响应的 Colorist 应用屏幕截图

现在,尝试使用不同的颜色描述测试流式传输行为。请尝试使用以下描述:

  • “显示黄昏时深蓝色的海洋”
  • “我想看一款让我想起热带花卉的鲜艳珊瑚色”
  • “创建一个像旧军用迷彩服一样的低调橄榄绿”

详细的流式传输技术流程

我们来看看在流式传输响应时会发生什么情况:

建立连接

当您调用 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 上下文同步

在此额外步骤中,您将在用户从历史记录中选择颜色时通知 Gemini,以实现 LLM 上下文同步。这样可以打造更具一致性的体验,让 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/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

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

part 'gemini_chat_service.g.dart';

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

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

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

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

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

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

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

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

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

新增的关键方法是 notifyColorSelection 方法,其用途如下:

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

这种方法通过利用现有的邮件处理基础架构来避免重复。

更新了主应用,以关联颜色选择通知

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

lib/main.dart

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

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

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

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

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

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

主要更改是添加了 notifyColorSelection 回调,该回调会将界面事件(从历史记录中选择颜色)与 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”(显示鲜艳的紫色)
    • “我想要森林绿”
    • “给我一个亮红色”
  2. 然后,点击历史记录条中的某个色彩缩略图

您应注意以下事项:

  1. 所选颜色会显示在主显示屏上
  2. 聊天中显示一条用户消息,指明所选颜色
  3. LLM 会回复,确认选择并对颜色进行评论
  4. 整个互动过程给人以自然且协调的感觉

这样可以打造顺畅的体验,让 LLM 能够感知直接消息和界面互动,并做出适当的回应。

LLM 上下文同步的工作原理

我们来探索一下此同步的技术细节:

数据流

  1. 用户操作:用户点击历史记录条中的某个颜色
  2. 界面事件MainScreen 微件检测此选择
  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,重点介绍了应用 build 的有趣部分。

在第 60 集,Craig 和 Andrew 将继续为您讲解如何通过添加新功能来扩展 Codelab 应用,并努力让 LLM 按照指示执行操作。

在第 61 集中,Craig 邀请了 Chris Sells 一起,以全新的方式分析新闻标题并生成相应的图片。

反馈

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

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