构建由 Gemini 赋能的 Flutter 应用

构建由 Gemini 赋能的 Flutter 应用

关于此 Codelab

subject上次更新时间:5月 19, 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 中的 Vertex AI,并实现简单的 LLM 通信
  3. 有效提示 - 创建系统提示,引导 LLM 理解颜色描述
  4. 函数声明 - 定义 LLM 可用于在应用中设置颜色的工具
  5. 工具处理 - 处理来自 LLM 的函数调用,并将其关联到应用的状态
  6. 流式响应 - 通过实时流式 LLM 响应改善用户体验
  7. LLM 上下文同步 - 通过告知 LLM 用户操作来打造协调一致的体验

学习内容

  • 为 Flutter 应用配置 Vertex AI in Firebase
  • 撰写有效的系统提示,引导 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
  • 已启用结算功能的 Firebase 项目 - Vertex AI in 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.3
  flutter_riverpod: ^2.6.1
  riverpod_annotation: ^2.6.1

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

flutter:
  uses-material-design: true

配置分析选项

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. 显示聊天互动的日志面板

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

后续操作

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

问题排查

界面软件包问题

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

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

构建错误

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

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

学到的关键概念

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

3. 基本 Gemini Chat 集成

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

本步骤将介绍的内容

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

设置 Firebase

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

创建 Firebase 项目

  1. 前往 Firebase 控制台,然后使用您的 Google 账号登录。
  2. 点击创建 Firebase 项目或选择现有项目。
  3. 按照设置向导创建项目。
  4. 创建项目后,您需要升级到 Blaze 方案(随用随付)才能使用 Vertex AI 服务。点击 Firebase 控制台左下角的升级按钮。

在 Firebase 项目中设置 Vertex AI

  1. 在 Firebase 控制台中,前往您的项目。
  2. 在左侧边栏中,选择 AI
  3. 在“Vertex AI in Firebase”卡片中,选择开始使用
  4. 按照提示为您的项目启用 Vertex AI in Firebase API。

安装 FlutterFire CLI

FlutterFire CLI 简化了在 Flutter 应用中设置 Firebase 的流程:

dart pub global activate flutterfire_cli

将 Firebase 添加到您的 Flutter 应用

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

此命令将执行以下操作:

  • 提示您选择刚刚创建的 Firebase 项目
  • 在 Firebase 中注册您的 Flutter 应用
  • 使用项目配置生成 firebase_options.dart 文件

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

平台专用配置

Firebase 要求的最低版本高于 Flutter 的默认版本。它还需要网络访问权限,才能与 Firebase 服务器中的 Vertex AI 通信。

配置 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_core/firebase_core.dart';
import 'package:firebase_vertexai/firebase_vertexai.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

import '../firebase_options.dart';

part 'gemini.g.dart';

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

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

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

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

此文件定义了三个密钥提供程序的基础。这些提供程序由 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_vertexai/firebase_vertexai.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

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

part 'gemini_chat_service.g.dart';

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

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

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

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

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

此服务:

  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. 使用 when 方法通过 Riverpod 的 AsyncValue 模式添加加载和错误屏幕
  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 中的 Vertex AI 将文本发送到 Gemini API
  4. LLM 处理:Gemini 模型会处理文本并生成回答
  5. 响应处理:应用接收响应并更新界面
  6. 日志记录:为确保透明度,系统会记录所有通信

聊天会话和对话上下文

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

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

后续操作

目前,您可以向 Gemini API 询问任何问题,因为它对可回复的内容没有限制。例如,您可以让它提供玫瑰战争的摘要,这与您的配色应用的用途无关。

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

问题排查

Firebase 配置问题

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

  • 确保 firebase_options.dart 文件已正确生成
  • 验证您是否已升级到 Blaze 方案以获得 Vertex AI 访问权限

API 访问错误

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

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

对话上下文问题

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

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

平台专用问题

对于特定于平台的问题:

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

学到的关键概念

  • 在 Flutter 应用中设置 Firebase
  • 在 Firebase 中配置 Vertex AI 以访问 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_core/firebase_core.dart';
import 'package:firebase_vertexai/firebase_vertexai.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

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

part 'gemini.g.dart';

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

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

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

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

关键更改是在创建生成式模型时添加 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 速率限制

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

  • 请注意,Vertex AI 服务有使用限制
  • 考虑实现具有指数退避算法的重试逻辑
  • 查看 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_vertexai/firebase_vertexai.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'gemini_tools.g.dart';

class GeminiTools {
 
GeminiTools(this.ref);

 
final Ref ref;

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

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

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

了解函数声明

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

  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_core/firebase_core.dart';
import 'package:firebase_vertexai/firebase_vertexai.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

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

part 'gemini.g.dart';

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

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

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

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

主要变化是在创建生成式模型时添加了 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

显示 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_vertexai/firebase_vertexai.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'gemini_tools.g.dart';

class GeminiTools {
 
GeminiTools(this.ref);

 
final Ref ref;

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

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

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

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

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

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

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

了解函数处理脚本

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

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

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

part 'gemini_chat_service.g.dart';

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

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

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

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

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

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

了解通信流

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

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)
  • 检查是否正确调用了颜色状态通知器
  • 检查日志,了解传递给函数的确切值

一般问题

对于常见问题:

  • 检查日志是否存在错误或警告
  • 验证 Vertex AI in Firebase 的连接性
  • 检查函数参数中是否存在任何类型不匹配的情况
  • 确保所有 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_vertexai/firebase_vertexai.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

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

part 'gemini_chat_service.g.dart';

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

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

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

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

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

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

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

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

了解流式传输实现

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

  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. 应用建立与 Vertex AI 服务的连接
  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 步:验证 Vertex AI 与 Firebase 的连接性
  • 第 3 步:确保所有 Riverpod 生成的代码都是最新的
  • 第 4 步:检查流式传输实现,确保没有缺少 await 语句

学到的关键概念

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

通过实现流式传输,您显著提升了 Colorist 应用的用户体验,打造了响应更快、互动性更强的界面,让用户有真正对话的感觉。

8. LLM 上下文同步

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

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

part 'gemini_chat_service.g.dart';

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

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

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

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

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

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

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

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

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

新增的关键方法是 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

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

测试 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 似乎丢失了上下文:

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

一般问题

对于常见问题:

  • 检查日志是否存在错误或警告
  • 验证 Vertex AI in Firebase 的连接性
  • 检查函数参数中是否存在任何类型不匹配的情况
  • 确保所有 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 的交叉领域,发掘更多令人兴奋的可能性!