关于此 Codelab
1. 构建由 Gemini 赋能的 Flutter 应用
构建内容
在此 Codelab 中,您将构建 Colorist,这是一个交互式 Flutter 应用,可将 Gemini API 的强大功能直接引入您的 Flutter 应用。您是否曾希望让用户能够通过自然语言控制您的应用,但不知道从何入手?此 Codelab 将介绍具体方法。
借助 Colorist,用户可以使用自然语言(例如“日落的橙色”或“深海蓝色”)描述颜色,而该应用会:
- 使用 Google 的 Gemini API 处理这些说明
- 将描述解读为精确的 RGB 颜色值
- 实时在屏幕上显示颜色
- 提供技术性颜色详细信息和有关颜色的有趣背景信息
- 维护最近生成的颜色的历史记录
该应用采用分屏界面,一侧显示彩色显示区域和互动式聊天系统,另一侧显示显示原始 LLM 互动的详细日志面板。通过此日志,您可以更好地了解 LLM 集成在后台的实际运作方式。
这对 Flutter 开发者而言为何重要
LLM 正在彻底改变用户与应用的互动方式,但将其有效集成到移动应用和桌面应用中却面临着独特的挑战。此 Codelab 将向您介绍一些实用模式,这些模式不仅仅局限于原始 API 调用。
您的学习历程
此 Codelab 将引导您逐步构建 Colorist:
- 项目设置 - 您将从基本 Flutter 应用结构和
colorist_ui
软件包开始 - 基本 Gemini 集成 - 将您的应用连接到 Firebase 中的 Vertex AI,并实现简单的 LLM 通信
- 有效提示 - 创建系统提示,引导 LLM 理解颜色描述
- 函数声明 - 定义 LLM 可用于在应用中设置颜色的工具
- 工具处理 - 处理来自 LLM 的函数调用,并将其关联到应用的状态
- 流式响应 - 通过实时流式 LLM 响应改善用户体验
- 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_riverpod
和riverpod_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
软件包提供预构建的界面组件和状态管理工具:
- MainScreen:用于显示以下内容的主要界面组件:
- 桌面设备上的分屏布局(互动区域和日志面板)
- 移动设备上的标签页界面
- 彩色显示、聊天界面和历史记录缩略图
- 状态管理:该应用使用多个状态通知器:
- ChatStateNotifier:管理聊天消息
- ColorStateNotifier:管理当前颜色和历史记录
- LogStateNotifier:管理日志条目以进行调试
- 消息处理:应用使用具有不同状态的消息模型:
- 向用户显示的消息:由用户输入
- LLM 消息:由 LLM(或目前的回声服务)生成
- MessageState:跟踪 LLM 消息是已完整还是仍在流式传输中
应用架构
该应用遵循以下架构:
- 界面层:由
colorist_ui
软件包提供 - 状态管理:使用 Riverpod 进行响应式状态管理
- 服务层:目前包含简单的回声服务,将替换为 Gemini Chat 服务
- LLM 集成:将在后续步骤中添加
通过这种分离,您可以专注于实现 LLM 集成,而界面组件已由系统处理。
运行应用
使用以下命令运行应用:
flutter run -d DEVICE
将 DEVICE
替换为目标设备,例如 macos
、windows
、chrome
或设备 ID。
现在,您应该会看到 Colorist 应用,其中包含:
- 采用默认颜色的彩色显示区域
- 您可以输入消息的聊天界面
- 显示聊天互动的日志面板
试着输入消息,例如“我想要深蓝色”,然后按“发送”。回声服务只会重复您的消息。在后续步骤中,您将使用 Vertex AI in Firebase 通过 Gemini API 将此值替换为实际的颜色解读。
后续操作
在下一步中,您将配置 Firebase 并实现基本的 Gemini API 集成,以便将回声服务替换为 Gemini 聊天服务。这样,应用便可以解读颜色描述并提供智能回复。
问题排查
界面软件包问题
如果您在使用 colorist_ui
软件包时遇到问题,请执行以下操作:
- 确保您使用的是最新版本
- 验证您是否已正确添加依赖项
- 检查是否存在任何冲突的软件包版本
构建错误
如果您看到构建错误,请执行以下操作:
- 确保您已安装最新的稳定版渠道 Flutter SDK
- 依次运行
flutter clean
和flutter 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 项目
- 前往 Firebase 控制台,然后使用您的 Google 账号登录。
- 点击创建 Firebase 项目或选择现有项目。
- 按照设置向导创建项目。
- 创建项目后,您需要升级到 Blaze 方案(随用随付)才能使用 Vertex AI 服务。点击 Firebase 控制台左下角的升级按钮。
在 Firebase 项目中设置 Vertex AI
- 在 Firebase 控制台中,前往您的项目。
- 在左侧边栏中,选择 AI。
- 在“Vertex AI in Firebase”卡片中,选择开始使用。
- 按照提示为您的项目启用 Vertex AI in Firebase API。
安装 FlutterFire CLI
FlutterFire CLI 简化了在 Flutter 应用中设置 Firebase 的流程:
dart pub global activate flutterfire_cli
将 Firebase 添加到您的 Flutter 应用
- 将 Firebase 核心和 Vertex AI 软件包添加到您的项目:
flutter pub add firebase_core firebase_vertexai
- 运行 FlutterFire 配置命令:
flutterfire configure
此命令将执行以下操作:
- 提示您选择刚刚创建的 Firebase 项目
- 在 Firebase 中注册您的 Flutter 应用
- 使用项目配置生成
firebase_options.dart
文件
该命令会自动检测您选择的平台(iOS、Android、macOS、Windows、Web),并相应地对其进行配置。
平台专用配置
Firebase 要求的最低版本高于 Flutter 的默认版本。它还需要网络访问权限,才能与 Firebase 服务器中的 Vertex AI 通信。
配置 macOS 权限
对于 macOS,您需要在应用的使用权中启用网络访问权限:
- 打开
macos/Runner/DebugProfile.entitlements
并添加以下代码:
macos/Runner/DebugProfile.entitlements
<key>com.apple.security.network.client</key>
<true/>
- 此外,打开
macos/Runner/Release.entitlements
并添加相同的条目。 - 更新
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
时生成。
firebaseAppProvider
:使用您的项目配置初始化 FirebasegeminiModelProvider
:创建 Gemini 生成式模型实例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);
此服务:
- 接受用户消息并将其发送到 Gemini API
- 使用模型的回答更新聊天界面
- 记录所有通信,以便轻松了解真实的 LLM 流程
- 使用适当的用户反馈来处理错误
注意:此时,日志窗口看起来与聊天窗口几乎完全相同。引入函数调用和流式响应后,日志会变得更加有趣。
生成 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),
),
);
}
}
此更新的主要变更如下:
- 将回声服务替换为基于 Gemini API 的聊天服务
- 使用
when
方法通过 Riverpod 的AsyncValue
模式添加加载和错误屏幕 - 通过
sendMessage
回调将界面连接到新的聊天服务
运行应用
使用以下命令运行应用:
flutter run -d DEVICE
将 DEVICE
替换为目标设备,例如 macos
、windows
、chrome
或设备 ID。
现在,当您输入消息时,系统会将其发送到 Gemini API,您将收到 LLM 的回答,而不是回声。日志面板会显示与 API 的互动。
了解 LLM 通信
我们先来了解一下与 Gemini API 通信时会发生的情况:
通信流程
- 用户输入:用户在聊天界面中输入文本
- 请求格式设置:应用将文本格式设置为 Gemini API 的
Content
对象 - API 通信:系统会通过 Firebase 中的 Vertex AI 将文本发送到 Gemini API
- LLM 处理:Gemini 模型会处理文本并生成回答
- 响应处理:应用接收响应并更新界面
- 日志记录:为确保透明度,系统会记录所有通信
聊天会话和对话上下文
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 互动至关重要,因为它们:
- 确保一致性:引导模型以一致的格式提供回答
- 提高相关性:让模型专注于您的特定领域(在本例中为颜色)
- 建立边界:定义模型应做和不应做什么
- 提升用户体验:打造更自然、更实用的互动模式
- 减少后期处理:以更易于解析或显示的格式获取回答
对于您的 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
了解系统提示结构
我们来详细了解一下此提示的用途:
- 角色定义:将 LLM 设为“色彩专家助理”
- 任务说明:将主要任务定义为将颜色描述解读为 RGB 值
- 响应格式:精确指定 RGB 值的格式以确保一致性
- 交换示例:提供预期互动模式的具体示例
- 边缘用例处理:说明如何处理不明确的说明
- 约束条件和准则:设置边界,例如将 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
请尝试使用各种颜色描述进行测试:
- “我想要天蓝色”
- “给我一个森林绿”
- “制作艳丽的日落橙色”
- “I want the color of fresh lavender”
- “给我看看深海蓝之类的颜色”
您应该会注意到,Gemini 现在会以对话方式说明颜色,并提供格式一致的 RGB 值。系统提示有效引导了 LLM,使其提供您所需的回答类型。
此外,还可以尝试询问颜色以外的内容。例如,玫瑰战争的主要原因。您应该会注意到与上一步的不同。
针对专门任务进行提示工程的重要性
系统提示既是艺术,也是科学。它们是 LLM 集成的关键部分,可能会显著影响模型对您的特定应用的实用性。您在这里所做的是一种提示工程 - 量身定制指令,使模型的行为方式符合应用的需求。
有效的提示工程涉及以下方面:
- 明确的角色定义:确定 LLM 的用途
- 明确的指令:详细说明 LLM 应如何做出回答
- 具体示例:以展示而不是仅仅说明的方式说明什么样的回答是好的
- 极端情况处理:指示 LLM 如何处理模糊的情况
- 格式规范:确保回答的结构一致且易于使用
您创建的系统提示会将 Gemini 的通用功能转换为专门的色彩解读助理,以便根据应用的需求提供格式专属的回答。这是一种强大的模式,可应用于许多不同的领域和任务。
后续操作
在下一步中,您将在此基础上添加函数声明,以便 LLM 不仅能建议 RGB 值,还能实际调用应用中的函数来直接设置颜色。这展示了 LLM 如何在自然语言和具体应用功能之间架起桥梁。
问题排查
资源加载问题
如果您在加载系统提示时遇到错误,请执行以下操作:
- 验证您的
pubspec.yaml
是否正确列出了资源目录 - 检查
rootBundle.loadString()
中的路径是否与文件位置相符 - 依次运行
flutter clean
和flutter 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 执行以下操作:
- 识别何时调用特定函数对用户请求有益
- 生成包含该函数所需参数的结构化 JSON 对象
- 让应用使用这些参数执行函数
- 接收函数的结果并将其纳入响应中
函数调用使 LLM 能够在应用中触发具体操作,而不是仅描述要执行的操作。
函数调用对 Flutter 应用而言为何至关重要
函数调用可在自然语言和应用功能之间建立强大的桥梁:
- 直接操作:用户可以用自然语言描述所需内容,应用会以具体的操作做出回应
- 结构化输出:LLM 会生成干净的结构化数据,而不是需要解析的文本
- 复杂操作:允许 LLM 访问外部数据、执行计算或修改应用状态
- 更好的用户体验:让对话与功能无缝集成
在 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);
了解函数声明
我们来分析一下此代码的功能:
- 函数命名:您将函数命名为
set_color
,以明确说明其用途 - 函数说明:您提供清晰的说明,帮助 LLM 了解何时使用该函数
- 参数定义:您可以定义结构化参数及其说明:
red
:RGB 的红色分量,指定为介于 0.0 和 1.0 之间的数字green
:RGB 的绿色分量,以介于 0.0 和 1.0 之间的数字指定blue
:RGB 的蓝色分量,指定为介于 0.0 和 1.0 之间的数字
- 架构类型:您可以使用
Schema.number()
指明这些是数值 - 工具集合:您创建一个包含函数声明的工具列表
这种结构化方法有助于 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
系统提示的关键变更如下:
- 工具简介:现在,您无需请求格式化的 RGB 值,而是告知 LLM
set_color
工具 - 修改后的过程:您将第 3 步从“设置响应中的值的格式”更改为“使用该工具设置值”
- 更新后的示例:您展示了响应应如何包含工具调用,而不是格式化文本
- 移除了格式要求:由于您使用的是结构化函数调用,因此无需再使用特定文本格式
此更新后的提示会指示 LLM 使用函数调用,而不是仅以文本形式提供 RGB 值。
生成 Riverpod 代码
运行 build runner 命令以生成所需的 Riverpod 代码:
dart run build_runner build --delete-conflicting-outputs
运行应用
此时,Gemini 会生成尝试使用函数调用的文本,但您尚未实现函数调用的处理脚本。运行应用并描述颜色后,您会看到 Gemini 的响应,就像它调用了某个工具一样,但在下一步之前,您不会在界面中看到任何颜色变化。
运行应用:
flutter run -d DEVICE
尝试描述“深蓝色”或“森林绿”等颜色,然后观察系统的回答。LLM 会尝试调用上面定义的函数,但您的代码尚未检测到函数调用。
函数调用过程
我们来了解一下 Gemini 使用函数调用时会发生什么情况:
- 函数选择:LLM 会根据用户的请求决定是否有必要进行函数调用
- 参数生成:LLM 会生成符合函数架构的参数值
- 函数调用格式:LLM 会在响应中发送结构化函数调用对象
- 应用处理:您的应用会收到此调用并执行相关函数(在下一步中实现)
- 响应集成:在多轮对话中,LLM 会预期返回函数的结果
在应用的当前状态下,前三个步骤正在发生,但您尚未实现第 4 步或第 5 步(处理函数调用),您将在下一步中完成这些步骤。
技术详情:Gemini 如何确定何时使用函数
Gemini 会根据以下因素智能地决定何时使用函数:
- 用户意图:函数是否最适合处理用户请求
- 功能相关性:可用功能与任务的匹配程度
- 参数可用性:能否可靠地确定参数值
- 系统指令:系统提示中关于函数用法的指导
通过提供清晰的函数声明和系统说明,您已将 Gemini 设置为将颜色描述请求视为调用 set_color
函数的机会。
后续操作
在下一步中,您将为来自 Gemini 的函数调用实现处理脚本。这样一来,整个循环就完成了,用户说明可通过 LLM 的函数调用触发界面中的实际颜色变化。
问题排查
函数声明问题
如果您遇到函数声明错误,请执行以下操作:
- 检查参数名称和类型是否与预期一致
- 验证函数名称是否清晰且具有描述性
- 确保函数说明准确说明了其用途
系统提示问题
如果 LLM 未尝试使用该函数:
- 验证您的系统提示是否明确指示 LLM 使用
set_color
工具 - 检查系统提示中的示例是否演示了函数用法
- 请尝试更明确地说明使用该工具的说明
常见问题
如果您遇到其他问题,请执行以下操作:
- 检查控制台中是否存在与函数声明相关的任何错误
- 验证工具是否已正确传递给模型
- 确保所有 Riverpod 生成的代码都是最新的
学到的关键概念
- 定义函数声明以扩展 Flutter 应用中的 LLM 功能
- 为结构化数据收集创建参数架构
- 将函数声明与 Gemini 模型集成
- 更新系统提示以鼓励用户使用功能
- 了解 LLM 如何选择和调用函数
此步骤演示了 LLM 如何弥合自然语言输入和结构化函数调用之间的差距,为对话功能与应用功能之间的无缝集成奠定基础。
6. 实现工具处理
在此步骤中,您将为来自 Gemini 的函数调用实现处理脚本。这样就完成了自然语言输入和具体应用功能之间的通信循环,让 LLM 能够根据用户描述直接操控界面。
本步骤将介绍的内容
- 了解 LLM 应用中的完整函数调用流水线
- 在 Flutter 应用中处理来自 Gemini 的函数调用
- 实现用于修改应用状态的函数处理脚本
- 处理函数响应并将结果返回给 LLM
- 在 LLM 和界面之间创建完整的通信流
- 出于透明度考虑,记录函数调用和响应
了解函数调用流水线
在深入了解实现之前,我们先来了解完整的函数调用流水线:
端到端流程
- 用户输入:用户用自然语言描述颜色(例如“forest green”)
- LLM 处理:Gemini 分析说明并决定调用
set_color
函数 - 函数调用生成:Gemini 会创建包含参数(红色、绿色、蓝色值)的结构化 JSON
- 函数调用接收:您的应用从 Gemini 接收此结构化数据
- 函数执行:您的应用使用提供的参数执行函数
- 状态更新:该函数会更新应用的状态(更改显示的颜色)
- 回答生成:您的函数将结果返回给 LLM
- 回答纳入:LLM 会将这些结果纳入其最终回答
- 界面更新:界面会对状态变化做出响应,显示新颜色
完整的通信周期对于正确集成 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);
了解函数处理脚本
我们来详细了解一下这些函数处理脚本的用途:
handleFunctionCall
:一个中央调度程序,具有以下功能:- 在日志面板中记录透明度的函数调用
- 根据函数名称路由到相应的处理程序
- 返回将发回给 LLM 的结构化回答
handleSetColor
:set_color
函数的特定处理脚本,具有以下功能:- 从参数映射中提取 RGB 值
- 将它们转换为预期类型(double)
- 使用
colorStateNotifier
更新应用的颜色状态 - 创建包含成功状态和当前颜色信息的结构化响应
- 记录函数结果以进行调试
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);
}
}
以下代码:
- 检查 LLM 响应是否包含任何函数调用
- 对于每个函数调用,使用函数名称和参数调用
handleFunctionCall
方法 - 收集每次函数调用的结果
- 使用
Content.functionResponses
将这些结果发送回 LLM - 处理 LLM 对函数结果的响应
- 使用最终回答文本更新界面
这会创建一个往返流程:
- 用户 → LLM:请求颜色
- LLM → 应用:带参数的函数调用
- 应用 → 用户:显示新颜色
- 应用 → LLM:函数结果
- LLM → 用户:包含函数结果的最终回答
生成 Riverpod 代码
运行 build runner 命令以生成所需的 Riverpod 代码:
dart run build_runner build --delete-conflicting-outputs
运行并测试完整流程
现在,运行您的应用:
flutter run -d DEVICE
尝试输入各种颜色描述:
- “我想要深红色”
- “给我显示一种舒缓的天空蓝”
- “给我显示新鲜薄荷叶的颜色”
- “我想看温暖的夕阳橙色”
- “用深紫色”
现在,您应该会看到:
- 聊天界面中显示的消息
- Gemini 的回答显示在聊天中
- 日志面板中记录的函数调用
- 函数结果会在执行后立即记录
- 颜色矩形正在更新以显示所述颜色
- RGB 值更新以显示新颜色的分量
- Gemini 的最终回答显示,通常会对所设置的颜色进行评论
日志面板可让您深入了解幕后发生的情况。您会看到:
- Gemini 正在进行的确切函数调用
- 它为每个 RGB 值选择的参数
- 函数返回的结果
- Gemini 的后续回答
颜色状态通知器
您用于更新颜色的 colorStateNotifier
是 colorist_ui
软件包的一部分。它会管理:
- 界面中显示的当前颜色
- 颜色历史记录(过去 10 种颜色)
- 通知界面组件的状态变化
当您使用新的 RGB 值调用 updateColor
时,它会:
- 使用提供的值创建新的
ColorData
对象 - 更新应用状态中的当前颜色
- 将颜色添加到历史记录
- 通过 Riverpod 的状态管理触发界面更新
colorist_ui
软件包中的界面组件会监控此状态,并在状态发生变化时自动更新,从而打造响应式体验。
了解错误处理
您的实现包含强大的错误处理功能:
- try-catch 代码块:封装所有 LLM 互动以捕获任何异常
- 错误日志记录:在日志面板中记录包含堆栈轨迹的错误
- 用户反馈:在聊天中提供友好的错误消息
- 状态清理:即使发生错误,也要最终确定消息状态
这样可以确保应用保持稳定,即使 LLM 服务或函数执行出现问题,也能提供适当的反馈。
函数调用对用户体验的强大作用
您在此处完成的任务展示了 LLM 如何创建强大的自然界面:
- 自然语言接口:用户使用日常语言表达意图
- 智能解读:LLM 会将模糊的说明转换为精确的值
- 直接操控:界面会根据自然语言更新
- 上下文响应:LLM 会提供有关更改的对话上下文
- 认知负担低:用户无需了解 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 时,流式响应至关重要,因为它可以带来出色的用户体验:
改进了用户体验
流式传输回答可带来多项显著的用户体验优势:
- 缩短了感知延迟时间:用户会立即看到文本开始显示(通常在 100-300 毫秒内),而不是等待几秒钟才能看到完整的响应。这种即时感知会显著提高用户满意度。
- 自然的对话节奏:文本逐渐显示的效果模仿了人类的交流方式,从而打造更自然的对话体验。
- 逐渐处理信息:用户可以随着信息的到来开始处理信息,而不是被大量文本一下子淹没。
- 提前中断的机会:在完整应用中,如果用户发现 LLM 的运行方向不利,则可能会中断或重定向 LLM。
- 对活动进行直观确认:流式文本会立即提供系统正在运行的反馈,从而降低不确定性。
技术优势
除了改进用户体验之外,流式传输还具有以下技术优势:
- 提前函数执行:函数调用会在流中出现后立即被检测和执行,而无需等待完整响应。
- 增量界面更新:您可以随着新信息的到来逐步更新界面,从而打造更具动态性的体验。
- 对话状态管理:流式传输会提供有关回答何时完成与仍在处理的明确信号,从而实现更好的状态管理。
- 降低超时风险:使用非流式回答时,长时间生成会导致连接超时。流式传输会尽早建立连接并进行维护。
对于 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);
了解流式传输实现
我们来分析一下此代码的功能:
- 会话状态跟踪:
conversationStateProvider
用于跟踪应用当前是否正在处理响应- 状态在处理期间从
idle
转换为busy
,然后又返回idle
- 这样可以防止多个并发请求发生冲突
- 数据流初始化:
sendMessageStream()
会返回响应分块的 Stream,而不是包含完整响应的Future
- 每个分块都可能包含文本和/或函数调用
- 渐进式处理:
await for
会实时处理每个分块- 系统会立即将文本附加到界面,从而产生流式传输效果
- 函数调用会在被检测到后立即执行
- 函数调用处理:
- 当在某个分块中检测到函数调用时,系统会立即执行该调用
- 结果会通过另一个流式调用发送回 LLM
- LLM 对这些结果的响应也以流式方式处理
- 错误处理和清理:
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
现在,尝试使用不同的颜色描述测试流式传输行为。请尝试使用以下说明:
- “显示黄昏时深蓝色的海洋”
- “我想看一款让我想起热带花卉的鲜艳珊瑚色”
- “创建一个像旧军用迷彩服一样的低调橄榄绿”
详细的流式传输技术流程
我们来看看在流式传输响应时会发生什么情况:
建立连接
当您调用 sendMessageStream()
时,会发生以下情况:
- 应用建立与 Vertex AI 服务的连接
- 系统会将用户请求发送到服务
- 服务器开始处理请求
- 流连接保持打开状态,随时准备传输数据块
分块传输
随着 Gemini 生成内容,系统会通过数据流发送数据块:
- 服务器会在生成文本块时发送这些文本块(通常是几个字或几句话)
- 当 Gemini 决定进行函数调用时,它会发送函数调用信息
- 函数调用后面可能会有其他文本块
- 流式传输会持续到生成完成
渐进式处理
您的应用会增量处理每个分块:
- 每个文本块都会附加到现有回答
- 函数调用会在被检测到后立即执行
- 界面会实时更新,显示文本和函数结果
- 系统会跟踪状态,以显示响应仍在流式传输
流式传输完成
生成完成后:
- 服务器关闭了数据流
- 您的
await for
循环会自然退出 - 消息已标记为已完成
- 对话状态会恢复为空闲状态
- 界面会更新以反映已完成状态
流式传输与非流式传输的比较
为了更好地了解流式传输的好处,我们来比较一下流式传输与非流式传输方法:
方面 | 非流式传输 | 流式 |
感知延迟时间 | 在系统准备好完整回答之前,用户不会看到任何内容 | 用户在几毫秒内看到第一个字词 |
用户体验 | 长时间等待后突然显示文本 | 自然的渐进式文本显示效果 |
状态管理 | 更简单(消息为待处理或已处理) | 更复杂(消息可以处于流式传输状态) |
函数执行 | 仅在完整响应后发生 | 在生成响应期间发生 |
实现复杂性 | 更易于实现 | 需要额外的状态管理 |
错误恢复 | “一刀切”式响应 | 部分回答可能仍有用 |
代码复杂度 | 更简单 | 由于需要处理数据流,因此更为复杂 |
对于 Colorist 这样的应用,流式传输的用户体验优势大于实现复杂性,尤其是对于可能需要几秒钟才能生成的颜色解读。
有关在线播放体验的最佳实践
在您自己的 LLM 应用中实现流式传输时,请考虑以下最佳实践:
- 清晰的视觉指示:始终提供清晰的视觉提示,以区分正在播放的消息和完整消息
- 输入屏蔽:在流式传输期间停用用户输入,以防止多个重叠请求
- 错误恢复:设计界面,以便在流式传输中断时妥善恢复
- 状态转换:确保在闲置、流式传输和完成状态之间顺畅转换
- 进度可视化:考虑使用细微的动画或指示器来显示正在进行的处理
- 取消选项:在完整的应用中,为用户提供取消正在生成内容的方法
- 函数结果集成:设计界面以处理流程中显示的函数结果
- 性能优化:最大限度地减少快速数据流更新期间的界面重新构建
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 上下文同步:
- 维护上下文:让 LLM 了解所有相关的用户操作
- 创建一致性:提供协调一致的体验,其中 LLM 会确认界面互动
- 增强智能:让 LLM 能够对所有用户操作做出适当响应
- 改善用户体验:让整个应用看起来更加集成且响应更快
- 减少用户工作量:无需用户手动说明其界面操作
在 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
方法,它具有以下特点:
- 接受一个表示所选颜色的
ColorData
对象 - 将其编码为可包含在消息中的 JSON 格式
- 向 LLM 发送格式特殊的消息,指明用户的选择
- 重复使用现有的
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
其中新增了“用户选择历史配色时”部分,该部分:
- 向 LLM 介绍历史记录选择通知的概念
- 提供此类通知的示例
- 显示合适响应的示例
- 设置确认选择和对颜色发表评论的预期
这有助于 LLM 了解如何对这些特殊消息做出适当回应。
生成 Riverpod 代码
运行 build runner 命令以生成所需的 Riverpod 代码:
dart run build_runner build --delete-conflicting-outputs
运行和测试 LLM 上下文同步
运行您的应用:
flutter run -d DEVICE
测试 LLM 上下文同步涉及以下步骤:
- 首先,在聊天中描述颜色以生成一些颜色
- “Show me a vibrant purple”(显示鲜艳的紫色)
- “我想要森林绿”
- “给我一个亮红色”
- 然后,点击历史记录条中的某个色彩缩略图
您应注意以下事项:
- 所选颜色会显示在主显示屏上
- 聊天中显示一条用户消息,指明所选颜色
- LLM 会回复,确认选择并对颜色进行评论
- 整个互动过程给人以自然且协调的感觉
这样可以打造顺畅的体验,让 LLM 能够感知直接消息和界面互动,并做出适当的回应。
LLM 上下文同步的工作原理
我们来探索一下此同步的技术细节:
数据流
- 用户操作:用户点击历史记录条中的某个颜色
- 界面事件:
MainScreen
微件检测此选择 - 回调执行:触发
notifyColorSelection
回调 - 消息创建:使用颜色数据创建格式特殊的消息
- LLM 处理:系统会将消息发送到 Gemini,后者会识别格式
- 上下文响应:Gemini 会根据系统提示做出适当的响应
- 界面更新:回答会显示在聊天中,打造一致的体验
数据序列化
这种方法的一个关键方面是如何序列化颜色数据:
'User selected color from history: ${json.encode(color.toLLMContextMap())}'
toLLMContextMap()
方法(由 colorist_ui
软件包提供)会将 ColorData
对象转换为包含 LLM 可以理解的键值对的映射。这通常包括:
- RGB 值(红色、绿色、蓝色)
- 十六进制代码表示
- 与颜色相关的任何名称或说明
通过采用一致的格式设置这些数据并将其添加到消息中,您可以确保 LLM 拥有适当回复所需的所有信息。
LLM 上下文同步的更广泛应用
这种向 LLM 发送界面事件的模式除了颜色选择之外,还有许多应用场景:
其他用例
- 过滤条件更改:在用户对数据应用过滤条件时通知 LLM
- 导航事件:在用户导航到不同版块时通知 LLM
- 选择更改:在用户从列表或网格中选择项时更新 LLM
- 偏好设置更新:在用户更改设置或偏好设置时告知 LLM
- 数据操纵:在用户添加、修改或删除数据时通知 LLM
在每种情况下,模式都保持不变:
- 检测界面事件
- 序列化相关数据
- 向 LLM 发送格式特殊的通知
- 通过系统提示引导 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 的交叉领域,发掘更多令人兴奋的可能性!