1. Gemini 지원 Flutter 앱 빌드
빌드할 항목
이 Codelab에서는 Gemini API의 기능을 Flutter 앱에 직접 가져오는 대화형 Flutter 애플리케이션인 Colorist를 빌드합니다. 사용자가 자연어를 통해 앱을 제어하도록 하려고 했지만 어디서부터 시작해야 할지 몰랐던 적이 있나요? 이 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 애플리케이션용 Firebase의 Vertex AI 구성
- LLM 동작을 안내하는 효과적인 시스템 프롬프트 작성
- 자연어와 앱 기능을 연결하는 함수 선언 구현
- 반응형 사용자 환경을 위해 스트리밍 응답을 처리합니다.
- UI 이벤트와 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을 최대한 활용하려면 다음을 갖추고 있어야 합니다.
- Flutter 개발 경험 - Flutter 기본사항 및 Dart 문법에 익숙함
- 비동기 프로그래밍 지식 - Futures, async/await, 스트림 이해
- Firebase 계정 - Firebase를 설정하려면 Google 계정이 필요합니다.
- 결제가 사용 설정된 Firebase 프로젝트 - Firebase의 Vertex AI에는 결제 계정이 필요합니다.
첫 번째 LLM 기반 Flutter 앱 빌드를 시작해 보겠습니다.
2. 프로젝트 설정 및 에코 서비스
이 첫 번째 단계에서는 프로젝트 구조를 설정하고 나중에 Gemini API 통합으로 대체될 간단한 에코 서비스를 구현합니다. 이렇게 하면 LLM 호출의 복잡성을 추가하기 전에 애플리케이션 아키텍처가 설정되고 UI가 올바르게 작동하는지 확인할 수 있습니다.
이 단계에서 학습할 내용
- 필수 종속 항목이 있는 Flutter 프로젝트 설정
- UI 구성요소용
colorist_ui
패키지 작업 - 에코 메시지 서비스를 구현하고 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 앱의 UI 구성요소를 제공하는 맞춤 패키지입니다.flutter_riverpod
및riverpod_annotation
: 상태 관리logging
: 구조화된 로깅용- 코드 생성 및 린팅을 위한 개발 종속 항목
pubspec.yaml
는 다음과 같이 표시됩니다.
pubspec.yaml
name: colorist
description: "A new Flutter project."
publish_to: 'none'
version: 0.1.0
environment:
sdk: ^3.7.2
dependencies:
flutter:
sdk: flutter
colorist_ui: ^0.1.0
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.4
custom_lint: ^0.7.5
flutter:
uses-material-design: true
분석 옵션 구성
프로젝트 루트의 analysis_options.yaml
파일에 custom_lint
를 추가합니다.
include: package:flutter_lints/flutter.yaml
analyzer:
plugins:
- custom_lint
이 구성을 사용하면 Riverpod 관련 린트를 사용 설정하여 코드 품질을 유지할 수 있습니다.
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
패키지는 사전 빌드된 UI 구성요소와 상태 관리 도구를 제공합니다.
- MainScreen: 다음을 표시하는 기본 UI 구성요소입니다.
- 데스크톱의 화면 분할 레이아웃 (상호작용 영역 및 로그 패널)
- 모바일의 탭 인터페이스
- 컬러 디스플레이, 채팅 인터페이스, 기록 썸네일
- 상태 관리: 앱은 여러 상태 알림 수신자를 사용합니다.
- ChatStateNotifier: 채팅 메시지를 관리합니다.
- ColorStateNotifier: 현재 색상 및 기록을 관리합니다.
- LogStateNotifier: 디버깅을 위한 로그 항목을 관리합니다.
- 메시지 처리: 앱은 다음과 같이 다양한 상태의 메시지 모델을 사용합니다.
- 사용자 메시지: 사용자가 입력함
- LLM 메시지: LLM (또는 현재는 에코 서비스)에서 생성
- MessageState: LLM 메시지가 완료되었는지 또는 아직 스트리밍 중인지 추적합니다.
애플리케이션 아키텍처
앱은 다음 아키텍처를 따릅니다.
- UI 레이어:
colorist_ui
패키지에서 제공 - 상태 관리: 반응형 상태 관리에 Riverpod를 사용합니다.
- 서비스 레이어: 현재 간단한 에코 서비스를 포함하고 있으며, 이는 Gemini Chat 서비스로 대체될 예정입니다.
- LLM 통합: 이후 단계에서 추가됩니다.
이렇게 분리하면 UI 구성요소가 이미 처리되는 동안 LLM 통합 구현에 집중할 수 있습니다.
앱 실행
다음 명령어를 사용하여 앱을 실행합니다.
flutter run -d DEVICE
DEVICE
를 타겟 기기(예: macos
, windows
, chrome
, 기기 ID)로 바꿉니다.
이제 다음과 같은 Colorist 앱이 표시됩니다.
- 기본 색상이 적용된 컬러 디스플레이 영역
- 메시지를 입력할 수 있는 채팅 인터페이스
- 채팅 상호작용을 보여주는 로그 패널
'진한 파란색이 좋겠어요'와 같은 메시지를 입력하고 보내기를 누릅니다. 에코 서비스는 메시지를 반복할 뿐입니다. 이후 단계에서는 Firebase의 Vertex AI를 통해 Gemini API를 사용하여 이를 실제 색상 해석으로 대체합니다.
다음 단계
다음 단계에서는 Firebase를 구성하고 기본 Gemini API 통합을 구현하여 에코 서비스를 Gemini 채팅 서비스로 대체합니다. 이렇게 하면 앱이 색상 설명을 해석하고 지능적인 응답을 제공할 수 있습니다.
문제 해결
UI 패키지 문제
colorist_ui
패키지와 관련된 문제가 발생하면 다음 단계를 따르세요.
- 최신 버전을 사용하고 있는지 확인
- 종속 항목을 올바르게 추가했는지 확인
- 충돌하는 패키지 버전 확인
빌드 오류
빌드 오류가 표시되면 다음 단계를 따르세요.
- 최신 안정화 버전 채널 Flutter SDK가 설치되어 있는지 확인
flutter clean
다음에flutter pub get
를 실행합니다.- 콘솔 출력에서 특정 오류 메시지 확인
학습한 주요 개념
- 필요한 종속 항목으로 Flutter 프로젝트 설정
- 애플리케이션의 아키텍처 및 구성요소 책임 이해
- LLM의 동작을 모방하는 간단한 서비스 구현
- 서비스를 UI 구성요소에 연결
- 상태 관리에 Riverpod 사용
3. 기본 Gemini 채팅 통합
이 단계에서는 이전 단계의 에코 서비스를 Firebase의 Vertex AI를 사용하여 Gemini API 통합으로 대체합니다. Firebase를 구성하고 필요한 제공업체를 설정하고 Gemini API와 통신하는 기본 채팅 서비스를 구현합니다.
이 단계에서 학습할 내용
- Flutter 애플리케이션에서 Firebase 설정
- Gemini 액세스를 위해 Firebase에서 Vertex AI 구성
- Firebase 및 Gemini 서비스용 Riverpod 제공자 만들기
- Gemini API로 기본 채팅 서비스 구현
- 비동기 API 응답 및 오류 상태 처리
Firebase 설정하기
먼저 Flutter 프로젝트에 Firebase를 설정해야 합니다. Firebase 프로젝트를 만들고, 앱을 프로젝트에 추가하고, 필요한 Vertex AI 설정을 구성해야 합니다.
Firebase 프로젝트 만들기
- Firebase Console로 이동하여 Google 계정으로 로그인합니다.
- Firebase 프로젝트 만들기를 클릭하거나 기존 프로젝트를 선택합니다.
- 설정 마법사를 따라 프로젝트를 만듭니다.
- 프로젝트가 생성되면 Vertex AI 서비스에 액세스하려면 Blaze 요금제 (사용한 만큼만 지불)로 업그레이드해야 합니다. Firebase Console 왼쪽 하단의 업그레이드 버튼을 클릭합니다.
Firebase 프로젝트에서 Vertex AI 설정
- Firebase Console에서 프로젝트로 이동합니다.
- 왼쪽 사이드바에서 AI를 선택합니다.
- Firebase의 Vertex AI 카드에서 시작하기를 선택합니다.
- 메시지에 따라 프로젝트에 Vertex AI in Firebase API를 사용 설정합니다.
FlutterFire CLI 설치
FlutterFire CLI를 사용하면 Flutter 앱에서 Firebase 설정을 간소화할 수 있습니다.
dart pub global activate flutterfire_cli
Flutter 앱에 Firebase 추가
- 프로젝트에 Firebase Core 및 Vertex AI 패키지를 추가합니다.
flutter pub add firebase_core firebase_vertexai
- FlutterFire 구성 명령어를 실행합니다.
flutterfire configure
이 명령어는 다음을 실행합니다.
- 방금 만든 Firebase 프로젝트를 선택하라는 메시지 표시
- Firebase에 Flutter 앱 등록
- 프로젝트 구성으로
firebase_options.dart
파일 생성
이 명령어를 실행하면 선택한 플랫폼 (iOS, Android, macOS, Windows, 웹)이 자동으로 감지되고 적절하게 구성됩니다.
플랫폼별 구성
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
: 프로젝트 구성으로 Firebase를 초기화합니다.geminiModelProvider
: 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 코드 생성
빌드 러너 명령어를 실행하여 필요한 Riverpod 코드를 생성합니다.
dart run build_runner build --delete-conflicting-outputs
이렇게 하면 Riverpod이 작동하는 데 필요한 .g.dart
파일이 생성됩니다.
main.dart 파일 업데이트
새 Gemini Chat 서비스를 사용하도록 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);
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
콜백을 통해 UI를 새 채팅 서비스에 연결
앱 실행
다음 명령어를 사용하여 앱을 실행합니다.
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 모델이 텍스트를 처리하고 응답을 생성합니다.
- 응답 처리: 앱이 응답을 수신하고 UI를 업데이트합니다.
- 로깅: 투명성을 위해 모든 커뮤니케이션이 로깅됩니다.
채팅 세션 및 대화 컨텍스트
Gemini 채팅 세션은 메시지 간에 컨텍스트를 유지하여 대화 상호작용을 가능하게 합니다. 즉, LLM은 현재 세션에서 이전에 나눈 대화를 '기억'하여 더 일관된 대화를 가능하게 합니다.
채팅 세션 제공업체의 keepAlive: true
주석은 이 컨텍스트가 앱의 수명 주기 전체에 걸쳐 유지되도록 합니다. 이 영구 컨텍스트는 LLM과의 자연스러운 대화 흐름을 유지하는 데 중요합니다.
다음 단계
이 시점에서는 Gemini API가 응답할 항목에 제한이 없으므로 무엇이든 물어볼 수 있습니다. 예를 들어 색상 앱의 목적과 관련이 없는 장미 전쟁의 개요를 요청할 수 있습니다.
다음 단계에서는 Gemini가 색상 설명을 더 효과적으로 해석하도록 안내하는 시스템 프롬프트를 만듭니다. 여기에서는 애플리케이션별 요구사항에 맞게 LLM의 동작을 맞춤설정하고 앱의 도메인에 기능을 집중하는 방법을 보여줍니다.
문제 해결
Firebase 구성 문제
Firebase 초기화에 오류가 발생하면 다음 단계를 따르세요.
firebase_options.dart
파일이 올바르게 생성되었는지 확인- Vertex AI 액세스를 위해 Blaze 요금제로 업그레이드했는지 확인
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 설정
- Gemini에 액세스하도록 Firebase에서 Vertex AI 구성
- 비동기 서비스용 Riverpod 제공자 만들기
- LLM과 통신하는 채팅 서비스 구현
- 비동기 API 상태 (로드, 오류, 데이터) 처리
- LLM 커뮤니케이션 흐름 및 채팅 세션 이해
4. 효과적인 색상 설명 프롬프트
이 단계에서는 Gemini가 색상 설명을 해석하도록 안내하는 시스템 프롬프트를 만들고 구현합니다. 시스템 프롬프트는 코드를 변경하지 않고 특정 작업에 맞게 LLM 동작을 맞춤설정하는 강력한 방법입니다.
이 단계에서 학습할 내용
- LLM 애플리케이션에서 시스템 메시지 및 그 중요성 이해
- 도메인별 작업에 효과적인 프롬프트 작성
- Flutter 앱에서 시스템 메시지 로드 및 사용
- 일관된 형식의 응답을 제공하도록 LLM 안내
- 시스템 프롬프트가 LLM 동작에 미치는 영향 테스트
시스템 메시지 이해하기
구현을 시작하기 전에 시스템 메시지가 무엇인지, 왜 중요한지 알아보겠습니다.
시스템 메시지란 무엇인가요?
시스템 프롬프트는 LLM에 제공되는 특수한 유형의 안내로, LLM의 응답에 대한 컨텍스트, 동작 가이드라인, 기대치를 설정합니다. 사용자 메시지와 달리 시스템 메시지는 다음과 같은 특징이 있습니다.
- LLM의 역할 및 캐릭터 설정
- 전문 지식 또는 기능 정의
- 서식 지정 안내 제공
- 응답에 제약 조건 설정
- 다양한 시나리오를 처리하는 방법 설명
시스템 프롬프트는 LLM에 '직무 설명'을 제공하는 것으로 생각할 수 있습니다. 즉, 대화 중에 모델이 어떻게 행동해야 하는지 알려줍니다.
시스템 메시지가 중요한 이유
시스템 프롬프트는 일관되고 유용한 LLM 상호작용을 만드는 데 중요한 역할을 합니다. 다음과 같은 이유 때문입니다.
- 일관성 유지: 모델이 일관된 형식으로 응답을 제공하도록 안내합니다.
- 관련성 개선: 특정 도메인 (이 경우 색상)에 모델을 집중합니다.
- 경계 설정: 모델이 해야 할 일과 해서는 안 되는 일을 정의합니다.
- 사용자 환경 개선: 더 자연스럽고 유용한 상호작용 패턴을 만듭니다.
- 후처리 감소: 더 쉽게 파싱하거나 표시할 수 있는 형식으로 응답을 가져옵니다.
Colorist 앱의 경우 색상 설명을 일관되게 해석하고 특정 형식으로 RGB 값을 제공하는 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 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
하단을 업데이트합니다.
pubspec.yaml
flutter:
uses-material-design: true
assets:
- assets/
flutter pub get
를 실행하여 애셋 번들을 새로고침합니다.
시스템 프롬프트 제공자 만들기
시스템 프롬프트를 로드할 새 파일 lib/providers/system_prompt.dart
을 만듭니다.
lib/providers/system_prompt.dart
import 'package:flutter/services.dart';
import 'package: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 코드 생성
빌드 러너 명령어를 실행하여 필요한 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
를 실행하여 애셋 번들을 새로고침합니다.
비일관된 응답
LLM이 형식 안내를 일관되게 따르지 않는 경우 다음 단계를 따르세요.
- 시스템 프롬프트에서 형식 요구사항을 더 명시적으로 표시해 보세요.
- 예상되는 패턴을 보여주는 예시를 더 추가합니다.
- 요청하는 형식이 모델에 적합한지 확인
API 비율 제한
비율 제한과 관련된 오류가 발생하면 다음 단계를 따르세요.
- Vertex AI 서비스에는 사용량 제한이 있습니다.
- 지수 백오프로 재시도 로직을 구현해 보세요 (독자 연습문제로 남겨 둠).
- Firebase Console에서 할당량 문제를 확인합니다.
학습한 주요 개념
- LLM 애플리케이션에서 시스템 메시지의 역할 및 중요성 이해
- 명확한 안내, 예시, 제약 조건을 사용하여 효과적인 프롬프트 작성
- Flutter 애플리케이션에서 시스템 메시지 로드 및 사용
- 도메인별 작업을 위한 LLM 동작 안내
- 프롬프트 엔지니어링을 사용하여 LLM 응답 형성
이 단계에서는 시스템 프롬프트에 명확한 안내를 제공하여 코드를 변경하지 않고도 LLM 동작을 대폭 맞춤설정하는 방법을 보여줍니다.
5. LLM 도구의 함수 선언
이 단계에서는 함수 선언을 구현하여 Gemini가 앱에서 작업을 실행할 수 있도록 설정하는 작업을 시작합니다. 이 강력한 기능을 사용하면 LLM이 RGB 값을 제안하는 것뿐만 아니라 특수 도구 호출을 통해 앱의 UI에 실제로 RGB 값을 설정할 수 있습니다. 하지만 Flutter 앱에서 실행된 LLM 요청을 확인하려면 다음 단계가 필요합니다.
이 단계에서 학습할 내용
- LLM 함수 호출 및 Flutter 애플리케이션에 대한 이점 이해
- Gemini의 스키마 기반 함수 선언 정의
- 함수 선언을 Gemini 모델과 통합
- 도구 기능을 활용하도록 시스템 프롬프트 업데이트
함수 호출 이해하기
함수 선언을 구현하기 전에 함수 선언의 정의와 가치를 알아보겠습니다.
함수 호출이란 무엇인가요?
함수 호출('도구 사용'이라고도 함)은 LLM이 다음을 수행할 수 있는 기능입니다.
- 특정 함수를 호출하면 사용자 요청에 도움이 되는 경우 인식
- 해당 함수에 필요한 매개변수를 사용하여 구조화된 JSON 객체 생성
- 애플리케이션이 이러한 매개변수를 사용하여 함수를 실행하도록 허용
- 함수의 결과를 수신하고 응답에 통합
LLM이 할 일을 설명하는 대신 함수 호출을 통해 LLM이 애플리케이션에서 구체적인 작업을 트리거할 수 있습니다.
Flutter 앱에서 함수 호출이 중요한 이유
함수 호출은 자연어와 애플리케이션 기능 간에 강력한 가교를 만듭니다.
- 직접 작업: 사용자가 원하는 작업을 자연어로 설명하면 앱이 구체적인 작업으로 응답합니다.
- 구조화된 출력: LLM은 파싱이 필요한 텍스트가 아닌 깔끔하고 구조화된 데이터를 생성합니다.
- 복잡한 작업: LLM이 외부 데이터에 액세스하거나, 계산을 실행하거나, 애플리케이션 상태를 수정할 수 있도록 합니다.
- 더 나은 사용자 경험: 대화와 기능 간의 원활한 통합을 제공합니다.
Colorist 앱에서 함수를 호출하면 사용자가 '포레스트 그린이 필요해'라고 말하고 텍스트에서 RGB 값을 파싱하지 않고도 UI가 해당 색상으로 즉시 업데이트됩니다.
함수 선언 정의
새 파일 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 모델 제공업체 업데이트
이제 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 '../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 코드 생성
빌드 러너 명령어를 실행하여 필요한 Riverpod 코드를 생성합니다.
dart run build_runner build --delete-conflicting-outputs
애플리케이션 실행
이 시점에서 Gemini는 함수 호출을 사용하려는 콘텐츠를 생성하지만 아직 함수 호출 핸들러를 구현하지 않았습니다. 앱을 실행하고 색상을 설명하면 Gemini가 도구를 호출한 것처럼 응답하지만 다음 단계까지 UI에 색상 변경사항이 표시되지 않습니다.
앱을 실행합니다.
flutter run -d DEVICE
'짙은 바다색' 또는 '숲 녹색'과 같은 색상을 설명해 보고 대답을 살펴보세요. LLM이 위에 정의된 함수를 호출하려고 하지만 코드에서 아직 함수 호출을 감지하지 못하고 있습니다.
함수 호출 프로세스
Gemini에서 함수 호출을 사용하면 어떻게 되는지 알아보겠습니다.
- 함수 선택: LLM은 사용자의 요청에 따라 함수 호출이 유용할지 결정합니다.
- 매개변수 생성: LLM은 함수의 스키마에 맞는 매개변수 값을 생성합니다.
- 함수 호출 형식: LLM이 응답에 구조화된 함수 호출 객체를 전송합니다.
- 애플리케이션 처리: 앱이 이 호출을 수신하고 관련 함수 (다음 단계에서 구현됨)를 실행합니다.
- 응답 통합: 멀티턴 대화에서 LLM은 함수의 결과가 반환되기를 기대합니다.
앱의 현재 상태에서 처음 세 단계가 실행되고 있지만 아직 4단계 또는 5단계 (함수 호출 처리)는 구현하지 않았습니다. 다음 단계에서 구현할 예정입니다.
기술적 세부정보: Gemini에서 함수 사용 시기를 결정하는 방법
Gemini는 다음을 기반으로 함수를 사용할 시점에 관한 지능적인 결정을 내립니다.
- 사용자 인텐트: 함수가 사용자의 요청을 가장 잘 처리할 수 있는지 여부
- 함수 관련성: 사용 가능한 함수가 작업과 얼마나 일치하는지
- 매개변수 가용성: 매개변수 값을 확실하게 결정할 수 있는지 여부
- 시스템 안내: 함수 사용에 관한 시스템 프롬프트의 안내
명확한 함수 선언과 시스템 안내를 제공하여 색상 설명 요청을 set_color
함수를 호출할 기회로 인식하도록 Gemini를 설정했습니다.
다음 단계
다음 단계에서는 Gemini에서 발생하는 함수 호출의 핸들러를 구현합니다. 이렇게 하면 원이 완성되어 사용자 설명이 LLM의 함수 호출을 통해 UI에서 실제 색상 변경을 트리거할 수 있습니다.
문제 해결
함수 선언 문제
함수 선언에 오류가 발생하면 다음 단계를 따르세요.
- 매개변수 이름과 유형이 예상과 일치하는지 확인
- 함수 이름이 명확하고 설명적인지 확인
- 함수 설명이 용도를 정확하게 설명하는지 확인
시스템 프롬프트 문제
LLM이 함수를 사용하려고 하지 않는 경우:
- 시스템 프롬프트에서 LLM에
set_color
도구를 사용하도록 명시적으로 지시하는지 확인 - 시스템 프롬프트의 예시에서 함수 사용을 보여주는지 확인
- 도구 사용에 관한 안내를 더 명시적으로 작성해 보세요.
일반적인 문제
다른 문제가 발생하면 다음 단계를 따르세요.
- 콘솔에서 함수 선언과 관련된 오류가 있는지 확인
- 도구가 모델에 올바르게 전달되는지 확인
- 모든 Riverpod 생성 코드가 최신 상태인지 확인
학습한 주요 개념
- Flutter 앱에서 LLM 기능을 확장하기 위한 함수 선언 정의
- 구조화된 데이터 수집을 위한 매개변수 스키마 만들기
- 함수 선언을 Gemini 모델과 통합
- 기능 사용을 유도하기 위해 시스템 메시지 업데이트
- LLM이 함수를 선택하고 호출하는 방법 이해
이 단계에서는 LLM이 자연어 입력과 구조화된 함수 호출 간의 격차를 해소하여 대화와 애플리케이션 기능 간의 원활한 통합을 위한 기반을 마련하는 방법을 보여줍니다.
6. 도구 처리 구현
이 단계에서는 Gemini에서 발생하는 함수 호출의 핸들러를 구현합니다. 이렇게 하면 자연어 입력과 구체적인 애플리케이션 기능 간의 커뮤니케이션이 완료되어 LLM이 사용자 설명을 기반으로 UI를 직접 조작할 수 있습니다.
이 단계에서 학습할 내용
- LLM 애플리케이션의 전체 함수 호출 파이프라인 이해
- Flutter 애플리케이션에서 Gemini의 함수 호출 처리
- 애플리케이션 상태를 수정하는 함수 핸들러 구현
- 함수 응답 처리 및 LLM에 결과 반환
- LLM과 UI 간의 전체 통신 흐름 만들기
- 투명성을 위해 함수 호출 및 응답 로깅
함수 호출 파이프라인 이해
구현을 시작하기 전에 전체 함수 호출 파이프라인을 살펴보겠습니다.
엔드 투 엔드 흐름
- 사용자 입력: 사용자가 자연어로 색상을 설명합니다 (예: 'forest green')
- LLM 처리: Gemini가 설명을 분석하고
set_color
함수를 호출하기로 결정합니다. - 함수 호출 생성: Gemini는 매개변수 (빨간색, 초록색, 파란색 값)가 있는 구조화된 JSON을 만듭니다.
- 함수 호출 수신: 앱이 Gemini에서 이 구조화된 데이터를 수신합니다.
- 함수 실행: 앱이 제공된 매개변수를 사용하여 함수를 실행합니다.
- 상태 업데이트: 함수가 앱의 상태를 업데이트합니다 (표시된 색상 변경).
- 응답 생성: 함수가 결과를 LLM에 다시 반환합니다.
- 응답 통합: LLM은 이러한 결과를 최종 응답에 통합합니다.
- UI 업데이트: UI가 상태 변경에 반응하여 새 색상을 표시합니다.
적절한 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의 자연어 이해와 구체적인 UI 변경사항 간의 격차를 해소하므로 특히 중요합니다.
함수 호출 및 응답을 처리하도록 Gemini Chat 서비스를 업데이트합니다.
이제 LLM 응답의 함수 호출을 처리하고 결과를 LLM에 다시 전송하도록 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'; // 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의 응답을 처리합니다.
- 최종 응답 텍스트로 UI를 업데이트합니다.
이렇게 하면 왕복 흐름이 생성됩니다.
- 사용자 → LLM: 색상 요청
- LLM → 앱: 매개변수가 있는 함수 호출
- 앱 → 사용자: 새 색상이 표시됨
- 앱 → LLM: 함수 결과
- LLM → 사용자: 함수 결과를 통합한 최종 응답
Riverpod 코드 생성
빌드 러너 명령어를 실행하여 필요한 Riverpod 코드를 생성합니다.
dart run build_runner build --delete-conflicting-outputs
전체 흐름 실행 및 테스트
이제 애플리케이션을 실행합니다.
flutter run -d DEVICE
다양한 색상 설명을 입력해 보세요.
- "진한 진홍색을 원합니다."
- "차분한 하늘색 보여 줘"
- "신선한 민트 잎의 색깔 알려 줘"
- "따뜻한 노을빛을 보고 싶어요"
- "진한 로얄 퍼플로 해 줘"
이제 다음과 같이 표시됩니다.
- 채팅 인터페이스에 표시되는 메시지
- 채팅에 표시되는 Gemini의 응답
- 로그 패널에 로깅되는 함수 호출
- 함수 결과가 즉시 로깅됨
- 설명된 색상을 표시하도록 업데이트되는 색상 직사각형
- 새 색상의 구성요소를 표시하도록 업데이트되는 RGB 값
- Gemini의 최종 응답이 표시되며, 설정된 색상에 대해 언급하는 경우가 많습니다.
로그 패널은 백그라운드에서 어떤 일이 일어나고 있는지 알려줍니다. 다음 내용이 표시됩니다.
- Gemini에서 실행하는 정확한 함수 호출
- 각 RGB 값에 대해 선택하는 매개변수
- 함수가 반환하는 결과
- Gemini의 후속 응답
색상 상태 알림
색상을 업데이트하는 데 사용 중인 colorStateNotifier
는 colorist_ui
패키지의 일부입니다. 다음을 관리합니다.
- UI에 표시되는 현재 색상
- 색상 기록 (최근 10개 색상)
- UI 구성요소의 상태 변경 알림
새 RGB 값으로 updateColor
를 호출하면 다음이 실행됩니다.
- 제공된 값으로 새
ColorData
객체를 만듭니다. - 앱 상태의 현재 색상을 업데이트합니다.
- 기록에 색상을 추가합니다.
- Riverpod의 상태 관리를 통해 UI 업데이트를 트리거합니다.
colorist_ui
패키지의 UI 구성요소는 이 상태를 감시하고 상태가 변경되면 자동으로 업데이트하여 반응형 환경을 만듭니다.
오류 처리 이해
구현에 강력한 오류 처리가 포함되어 있습니다.
- try-catch 블록: 모든 LLM 상호작용을 래핑하여 예외를 포착합니다.
- 오류 로깅: 로그 패널에 스택 트레이스와 함께 오류를 기록합니다.
- 사용자 의견: 채팅에 친근한 오류 메시지를 제공합니다.
- 상태 정리: 오류가 발생하더라도 메시지 상태를 완료합니다.
이렇게 하면 LLM 서비스 또는 함수 실행에 문제가 발생하더라도 앱이 안정적으로 유지되고 적절한 의견을 제공할 수 있습니다.
사용자 환경을 위한 함수 호출의 힘
여기에서 수행한 작업은 LLM이 강력한 자연 인터페이스를 만드는 방법을 보여줍니다.
- 자연어 인터페이스: 사용자가 일상적인 언어로 의도를 표현합니다.
- 지능형 해석: LLM이 모호한 설명을 정확한 값으로 변환합니다.
- 직접 조작: 자연어에 대한 응답으로 UI가 업데이트됩니다.
- 문맥 응답: LLM이 변경사항에 관한 대화 맥락을 제공합니다.
- 인지 부하가 낮음: 사용자가 RGB 값이나 색상 이론을 이해할 필요가 없습니다.
LLM 함수 호출을 사용하여 자연어와 UI 작업을 연결하는 이 패턴은 색상 선택을 넘어 수많은 다른 도메인으로 확장될 수 있습니다.
다음 단계
다음 단계에서는 스트리밍 응답을 구현하여 사용자 환경을 개선합니다. 전체 응답을 기다리는 대신 텍스트 청크와 함수 호출이 수신될 때마다 처리하여 더 반응이 빠르고 매력적인 애플리케이션을 만들 수 있습니다.
문제 해결
함수 호출 문제
Gemini에서 함수를 호출하지 않거나 매개변수가 잘못된 경우:
- 함수 선언이 시스템 프롬프트에 설명된 내용과 일치하는지 확인
- 매개변수 이름과 유형이 일치하는지 확인
- 시스템 프롬프트에서 LLM에 도구를 사용하도록 명시적으로 지시하는지 확인
- 핸들러의 함수 이름이 선언의 함수 이름과 정확하게 일치하는지 확인합니다.
- 로그 패널에서 함수 호출에 관한 자세한 정보를 확인합니다.
함수 응답 문제
함수 결과가 LLM으로 제대로 전송되지 않는 경우:
- 함수가 올바른 형식의 맵을 반환하는지 확인
- Content.functionResponses가 올바르게 구성되는지 확인
- 로그에서 함수 응답과 관련된 오류를 찾습니다.
- 응답에 동일한 채팅 세션을 사용하고 있는지 확인
컬러 디스플레이 문제
색상이 올바르게 표시되지 않는 경우 다음 단계를 따르세요.
- RGB 값이 제대로 double로 변환되는지 확인합니다 (LLM은 정수로 전송할 수 있음).
- 값이 예상 범위 (0.0~1.0)에 있는지 확인합니다.
- 색상 상태 알림이 올바르게 호출되는지 확인
- 로그에서 함수에 전달되는 정확한 값을 검사합니다.
일반적인 문제
일반적인 문제:
- 로그에서 오류 또는 경고 확인
- Firebase 연결에서 Vertex AI 확인
- 함수 매개변수의 유형 불일치 확인
- 모든 Riverpod 생성 코드가 최신 상태인지 확인
학습한 주요 개념
- Flutter에서 전체 함수 호출 파이프라인 구현
- LLM과 애플리케이션 간의 전체 통신 만들기
- LLM 응답의 구조화된 데이터 처리
- 응답에 통합하기 위해 함수 결과를 LLM으로 다시 전송
- 로그 패널을 사용하여 LLM-애플리케이션 상호작용 확인
- 자연어 입력을 구체적인 UI 변경사항에 연결
이 단계가 완료되면 앱은 LLM 통합을 위한 가장 강력한 패턴 중 하나인 자연어 입력을 구체적인 UI 작업으로 변환하는 동시에 이러한 작업을 인식하는 일관된 대화를 유지하는 패턴을 보여줍니다. 이를 통해 사용자에게 마법 같은 느낌을 주는 직관적인 대화형 인터페이스를 만들 수 있습니다.
7. 더 나은 UX를 위한 응답 스트리밍
이 단계에서는 Gemini의 스트리밍 응답을 구현하여 사용자 환경을 개선합니다. 전체 응답이 생성될 때까지 기다리는 대신 텍스트 청크와 함수 호출이 수신될 때마다 처리하여 더 반응이 빠르고 매력적인 애플리케이션을 만들 수 있습니다.
이 단계에서 다룰 내용
- LLM 기반 애플리케이션의 스트리밍 중요성
- Flutter 애플리케이션에서 스트리밍 LLM 응답 구현
- API에서 도착하는 부분 텍스트 청크 처리
- 메시지 충돌을 방지하기 위해 대화 상태 관리
- 스트리밍 응답에서 함수 호출 처리
- 진행 중인 응답을 위한 시각적 표시기 만들기
LLM 애플리케이션에서 스트리밍이 중요한 이유
구현하기 전에 LLM으로 우수한 사용자 환경을 만드는 데 응답 스트리밍이 중요한 이유를 알아보겠습니다.
향상된 사용자 환경
응답 스트리밍은 다음과 같은 몇 가지 중요한 사용자 환경 이점을 제공합니다.
- 인지된 지연 시간 감소: 사용자는 응답이 완료될 때까지 몇 초 동안 기다리지 않고 텍스트가 즉시 (일반적으로 100~300ms 이내) 표시되는 것을 확인할 수 있습니다. 이러한 즉각적인 인식은 사용자 만족도를 크게 개선합니다.
- 자연스러운 대화 리듬: 텍스트가 점진적으로 표시되어 인간의 소통 방식을 모방하여 더 자연스러운 대화 환경을 만듭니다.
- 진행 중인 정보 처리: 사용자가 한꺼번에 대량의 텍스트를 처리하는 대신 정보가 도착할 때마다 처리를 시작할 수 있습니다.
- 조기 중단 기회: 전체 애플리케이션에서 사용자가 LLM이 유용하지 않은 방향으로 진행되는 것으로 판단되면 LLM을 중단하거나 리디렉션할 수 있습니다.
- 활동의 시각적 확인: 스트리밍 텍스트는 시스템이 작동하고 있다는 즉각적인 피드백을 제공하여 불확실성을 줄입니다.
기술적 이점
스트리밍은 UX 개선 외에도 다음과 같은 기술적 이점을 제공합니다.
- 조기 함수 실행: 함수 호출이 스트림에 표시되는 즉시 전체 응답을 기다리지 않고 감지하여 실행할 수 있습니다.
- 증분 UI 업데이트: 새 정보가 들어올 때마다 UI를 점진적으로 업데이트하여 더 역동적인 환경을 만들 수 있습니다.
- 대화 상태 관리: 스트리밍을 통해 응답이 완료되었는지 여부에 대한 명확한 신호를 제공하여 상태를 더 효과적으로 관리할 수 있습니다.
- 시간 초과 위험 감소: 스트리밍이 아닌 응답을 사용하면 장기 실행 생성 시 연결 시간 초과가 발생할 수 있습니다. 스트리밍은 연결을 조기에 설정하고 유지합니다.
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()
는 전체 응답이 포함된 Future 대신 응답 청크의 스트림을 반환합니다.- 각 청크는 텍스트, 함수 호출 또는 둘 다를 포함할 수 있습니다.
- 진행 중인 처리:
await for
는 각 청크가 실시간으로 도착할 때마다 처리합니다.- 텍스트가 즉시 UI에 추가되어 스트리밍 효과를 만듭니다.
- 함수 호출은 감지되는 즉시 실행됩니다.
- 함수 호출 처리:
- 청크에서 함수 호출이 감지되면 즉시 실행됩니다.
- 결과는 다른 스트리밍 호출을 통해 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
패키지에서 제공)는 이 상태를 사용하여 응답이 처리되는 동안 텍스트 입력을 사용 중지합니다.
이렇게 하면 UI가 대화의 현재 상태를 반영하는 일관된 사용자 환경을 만들 수 있습니다.
Riverpod 코드 생성
빌드 러너 명령어를 실행하여 필요한 Riverpod 코드를 생성합니다.
dart run build_runner build --delete-conflicting-outputs
스트리밍 응답 실행 및 테스트
애플리케이션을 실행합니다.
flutter run -d DEVICE
이제 다양한 색상 설명으로 스트리밍 동작을 테스트해 보세요. 다음과 같은 설명을 사용해 보세요.
- "해질녘에 바다의 짙은 청록색 보여 줘"
- "열대 꽃을 연상시키는 생동감 넘치는 산호를 보고 싶어요."
- "오래된 군복처럼 차분한 올리브색을 만들어 줘"
스트리밍 기술 흐름 세부정보
응답을 스트리밍할 때 정확히 어떤 일이 일어나는지 살펴보겠습니다.
연결 설정
sendMessageStream()
를 호출하면 다음과 같은 결과가 발생합니다.
- 앱이 Vertex AI 서비스에 연결
- 사용자 요청이 서비스로 전송됩니다.
- 서버에서 요청 처리를 시작합니다.
- 스트림 연결이 열려 있어 청크를 전송할 준비가 됨
청크 전송
Gemini에서 콘텐츠를 생성하면 청크가 스트림을 통해 전송됩니다.
- 서버는 텍스트 청크가 생성될 때마다 전송합니다 (일반적으로 단어 또는 문장 몇 개).
- Gemini가 함수 호출을 결정하면 함수 호출 정보를 전송합니다.
- 함수 호출 뒤에 추가 텍스트 청크가 올 수 있음
- 생성이 완료될 때까지 스트림이 계속됩니다.
점진적 처리
앱은 각 청크를 점진적으로 처리합니다.
- 각 텍스트 청크가 기존 응답에 추가됩니다.
- 함수 호출은 감지되는 즉시 실행됩니다.
- UI가 텍스트와 함수 결과 모두를 사용하여 실시간으로 업데이트됩니다.
- 응답이 계속 스트리밍 중임을 나타내기 위해 상태가 추적됨
스트림 완료
생성이 완료되면 다음 단계를 따르세요.
- 서버에서 스트림을 닫습니다.
await for
루프가 자연스럽게 종료됨- 메시지가 완료됨으로 표시됨
- 대화 상태가 유휴 상태로 다시 설정됩니다.
- 완료된 상태를 반영하도록 UI가 업데이트됩니다.
스트리밍과 비스트리밍 비교
스트리밍의 이점을 더 잘 이해하려면 스트리밍 방식과 스트리밍이 아닌 방식을 비교해 보겠습니다.
관점 | 비스트리밍 | 스트리밍 |
지각된 지연 시간 | 전체 응답이 준비될 때까지 사용자에게 아무것도 표시되지 않음 | 사용자가 밀리초 이내에 첫 단어를 확인함 |
사용자 경험 | 긴 대기 후 갑자기 텍스트가 표시됨 | 자연스럽고 점진적인 텍스트 모양 |
상태 관리 | 더 간단함 (메시지가 대기 중 또는 완료됨) | 더 복잡함 (메시지가 스트리밍 상태일 수 있음) |
함수 실행 | 전체 응답 후에만 발생 | 응답 생성 중에 발생합니다. |
구현 복잡성 | 간단한 구현 | 추가 상태 관리 필요 |
오류 복구 | 양단간의 응답 | 부분 응답도 유용할 수 있습니다. |
코드 복잡성 | 덜 복잡함 | 스트림 처리로 인해 더 복잡함 |
Colorist와 같은 애플리케이션의 경우 스트리밍의 UX 이점이 구현 복잡성보다 큽니다. 특히 생성하는 데 몇 초가 걸릴 수 있는 색상 해석의 경우 더욱 그렇습니다.
스트리밍 UX 권장사항
자체 LLM 애플리케이션에서 스트리밍을 구현할 때는 다음 권장사항을 고려하세요.
- 명확한 시각적 표시기: 항상 스트리밍 메시지와 완료 메시지를 구분하는 명확한 시각적 신호를 제공합니다.
- 입력 차단: 스트리밍 중에 사용자 입력을 사용 중지하여 여러 개의 중복 요청이 발생하지 않도록 합니다.
- 오류 복구: 스트리밍이 중단될 경우 원활한 복구를 처리하도록 UI를 설계합니다.
- 상태 전환: 유휴, 스트리밍, 완료 상태 간에 원활하게 전환합니다.
- 진행률 시각화: 활성 처리를 보여주는 섬세한 애니메이션이나 표시기를 고려하세요.
- 취소 옵션: 완성된 앱에서 사용자가 진행 중인 생성을 취소할 수 있는 방법을 제공합니다.
- 함수 결과 통합: 스트림 중간에 표시되는 함수 결과를 처리하도록 UI를 설계합니다.
- 성능 최적화: 빠른 스트림 업데이트 중에 UI 재빌드를 최소화합니다.
colorist_ui
패키지는 이러한 권장사항을 대부분 구현하지만 스트리밍 LLM 구현에서는 중요한 고려사항입니다.
다음 단계
다음 단계에서는 사용자가 기록에서 색상을 선택할 때 Gemini에 알림을 보내 LLM 동기화를 구현합니다. 이렇게 하면 LLM이 사용자가 시작한 애플리케이션 상태 변경사항을 인식하는 보다 일관된 환경이 만들어집니다.
문제 해결
스트림 처리 문제
스트림 처리에 문제가 발생하면 다음 단계를 따르세요.
- 증상: 부분 응답, 누락된 텍스트 또는 갑작스러운 스트림 종료
- 해결 방법: 네트워크 연결을 확인하고 코드에서 적절한 async/await 패턴을 사용합니다.
- 진단: 로그 패널에서 스트림 처리와 관련된 오류 메시지 또는 경고를 확인합니다.
- 해결: 모든 스트림 처리에서
try
/catch
블록을 사용하여 적절한 오류 처리를 사용하도록 합니다.
함수 호출 누락
스트림에서 함수 호출이 감지되지 않는 경우:
- 증상: 텍스트가 표시되지만 색상이 업데이트되지 않거나 로그에 함수 호출이 표시되지 않음
- 해결 방법: 함수 호출 사용에 관한 시스템 프롬프트의 안내 확인
- 진단: 로그 패널에서 함수 호출이 수신되는지 확인합니다.
- 해결 방법: LLM에
set_color
도구를 사용하도록 더 명시적으로 안내하도록 시스템 프롬프트를 조정합니다.
일반적인 오류 처리
기타 문제:
- 1단계: 로그 패널에서 오류 메시지 확인
- 2단계: Firebase 연결에서 Vertex AI 확인
- 3단계: 모든 Riverpod 생성 코드가 최신 상태인지 확인
- 4단계: 누락된 await 문이 있는지 스트리밍 구현 검토
학습한 주요 개념
- 더 반응이 빠른 UX를 위해 Gemini API로 스트리밍 응답 구현
- 스트리밍 상호작용을 올바르게 처리하기 위해 대화 상태 관리
- 실시간 문자 및 함수 호출이 도착할 때 처리
- 스트리밍 중에 점진적으로 업데이트되는 반응형 UI 만들기
- 적절한 비동기 패턴으로 동시 스트림 처리
- 대답 스트리밍 중에 적절한 시각적 피드백 제공
스트리밍을 구현하여 Colorist 앱의 사용자 환경을 크게 개선하고, 보다 반응이 빠르고 매력적인 인터페이스를 만들어 마치 대화하는 것처럼 느낄 수 있도록 했습니다.
8. LLM 컨텍스트 동기화
이 보너스 단계에서는 사용자가 기록에서 색상을 선택할 때 Gemini에 알림을 보내 LLM 컨텍스트 동기화를 구현합니다. 이렇게 하면 LLM이 명시적인 메시지뿐만 아니라 인터페이스의 사용자 작업도 인식하는 보다 일관된 환경을 만들 수 있습니다.
이 단계에서 다룰 내용
- UI와 LLM 간의 LLM 컨텍스트 동기화 만들기
- UI 이벤트를 LLM이 이해할 수 있는 컨텍스트로 직렬화
- 사용자 작업에 따라 대화 컨텍스트 업데이트
- 다양한 상호작용 방법에서 일관된 환경 만들기
- 명시적인 채팅 메시지 외에도 LLM 컨텍스트 인식 개선
LLM 컨텍스트 동기화 이해
기존 챗봇은 명시적인 사용자 메시지에만 응답하므로 사용자가 다른 방법으로 앱과 상호작용할 때 연결이 끊어집니다. LLM 컨텍스트 동기화는 이 제한사항을 해결합니다.
LLM 컨텍스트 동기화가 중요한 이유
사용자가 UI 요소 (예: 기록에서 색상 선택)를 통해 앱과 상호작용할 때는 개발자가 명시적으로 알리지 않는 한 LLM에서 어떤 일이 발생했는지 알 수 없습니다. LLM 컨텍스트 동기화:
- 문맥 유지: LLM에 모든 관련 사용자 작업에 관한 정보를 계속 제공합니다.
- 일관성 생성: LLM이 UI 상호작용을 인식하는 일관된 환경을 생성합니다.
- 인텔리전스 개선: LLM이 모든 사용자 작업에 적절하게 응답할 수 있도록 합니다.
- 사용자 환경 개선: 전체 애플리케이션이 더 통합되고 반응성이 높아집니다.
- 사용자의 노력 감소: 사용자가 UI 작업을 수동으로 설명할 필요가 없습니다.
Colorist 앱에서 사용자가 기록에서 색상을 선택하면 Gemini가 이 작업을 인식하고 선택한 색상에 대해 지능적으로 의견을 제공하여 원활하고 인식하는 어시스턴트라는 환상을 유지해야 합니다.
색상 선택 알림을 위해 Gemini Chat 서비스를 업데이트합니다.
먼저 사용자가 기록에서 색상을 선택할 때 LLM에 알리는 메서드를 GeminiChatService
에 추가합니다. 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),
),
);
}
}
주요 변경사항은 UI 이벤트 (기록에서 색상 선택)를 LLM 알림 시스템에 연결하는 notifyColorSelection
콜백을 추가하는 것입니다.
시스템 메시지 업데이트
이제 색상 선택 알림에 응답하는 방법을 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 코드 생성
빌드 러너 명령어를 실행하여 필요한 Riverpod 코드를 생성합니다.
dart run build_runner build --delete-conflicting-outputs
LLM 컨텍스트 동기화 실행 및 테스트
애플리케이션을 실행합니다.
flutter run -d DEVICE
LLM 컨텍스트 동기화를 테스트하려면 다음을 실행해야 합니다.
- 먼저 채팅에서 색상을 설명하여 몇 가지 색상을 생성합니다.
- "선명한 보라색 보여 줘"
- "숲의 녹색을 원해요"
- "밝은 빨간색으로 해 줘"
- 그런 다음 기록 스트립에서 색상 썸네일 중 하나를 클릭합니다.
다음 사항을 확인해야 합니다.
- 선택한 색상이 기본 디스플레이에 표시됩니다.
- 채팅에 색상 선택을 나타내는 사용자 메시지가 표시됨
- LLM은 선택사항을 확인하고 색상에 대해 언급하여 응답합니다.
- 전체 상호작용이 자연스럽고 일관된 느낌을 줍니다.
이렇게 하면 LLM이 채팅 메시지와 UI 상호작용을 모두 인식하고 적절하게 응답하는 원활한 환경이 만들어집니다.
LLM 컨텍스트 동기화의 작동 방식
이 동기화의 작동 방식에 관한 기술적 세부정보를 살펴보겠습니다.
데이터 흐름
- 사용자 작업: 사용자가 기록 스트립에서 색상을 클릭합니다.
- UI 이벤트:
MainScreen
위젯이 이 선택을 감지합니다. - 콜백 실행:
notifyColorSelection
콜백이 트리거됩니다. - 메시지 생성: 색상 데이터를 사용하여 특별한 형식의 메시지가 생성됩니다.
- LLM 처리: 메시지가 형식을 인식하는 Gemini로 전송됩니다.
- 문맥 응답: Gemini는 시스템 프롬프트에 따라 적절하게 응답합니다.
- UI 업데이트: 응답이 채팅에 표시되어 일관된 환경을 제공합니다.
데이터 직렬화
이 접근 방식의 핵심은 색상 데이터를 직렬화하는 방법입니다.
'User selected color from history: ${json.encode(color.toLLMContextMap())}'
toLLMContextMap()
메서드 (colorist_ui
패키지에서 제공)는 ColorData
객체를 LLM이 이해할 수 있는 키 속성이 있는 맵으로 변환합니다. 여기에는 일반적으로 다음이 포함됩니다.
- RGB 값 (빨강, 녹색, 파랑)
- 16진수 코드 표현
- 색상과 연결된 이름 또는 설명
이 데이터의 형식을 일관되게 지정하고 메일에 포함하면 LLM에 적절하게 응답하는 데 필요한 모든 정보가 포함됩니다.
LLM 컨텍스트 동기화의 광범위한 적용
UI 이벤트에 관해 LLM에 알리는 이 패턴은 색상 선택 외에도 다양한 애플리케이션에 적용할 수 있습니다.
기타 사용 사례
- 필터 변경: 사용자가 데이터에 필터를 적용하면 LLM에 알림을 보냅니다.
- 탐색 이벤트: 사용자가 다른 섹션으로 이동할 때 LLM에 알립니다.
- 선택 변경: 사용자가 목록 또는 그리드에서 항목을 선택할 때 LLM을 업데이트합니다.
- 환경설정 업데이트: 사용자가 설정 또는 환경설정을 변경하면 LLM에 알립니다.
- 데이터 조작: 사용자가 데이터를 추가, 수정 또는 삭제할 때 LLM에 알림을 보냅니다.
각 경우 패턴은 동일하게 유지됩니다.
- UI 이벤트 감지
- 관련 데이터 직렬화
- LLM에 특별한 형식의 알림 전송
- 시스템 프롬프트를 통해 LLM이 적절하게 응답하도록 안내
LLM 컨텍스트 동기화 권장사항
구현에 따라 효과적인 LLM 컨텍스트 동기화를 위한 몇 가지 권장사항은 다음과 같습니다.
1. 일관된 형식
LLM이 알림을 쉽게 식별할 수 있도록 일관된 형식을 사용합니다.
"User [action] [object]: [structured data]"
2. 풍부한 상황 정보
LLM이 지능적으로 응답할 수 있도록 알림에 충분한 세부정보를 포함합니다. 색상의 경우 RGB 값, 16진수 코드, 기타 관련 속성을 의미합니다.
3. 명확한 지침
시스템 프롬프트에 알림을 처리하는 방법에 관한 명시적인 안내를 제공합니다(예시 포함).
4. 자연스러운 통합
기술적 중단이 아닌 대화에서 자연스럽게 흐를 수 있도록 알림을 디자인합니다.
5. 선택적 알림
대화와 관련된 작업에 대해서만 LLM에 알립니다. 모든 UI 이벤트를 전달할 필요는 없습니다.
문제 해결
알림 문제
LLM이 색상 선택에 제대로 응답하지 않는 경우 다음 단계를 따르세요.
- 알림 메시지 형식이 시스템 메시지에 설명된 내용과 일치하는지 확인
- 색상 데이터가 올바르게 직렬화되는지 확인
- 시스템 프롬프트에 선택사항 처리에 관한 명확한 안내가 포함되어 있는지 확인
- 알림을 보낼 때 채팅 서비스에서 오류를 찾습니다.
컨텍스트 관리
LLM에서 컨텍스트가 손실되는 것처럼 보이면 다음을 실행합니다.
- 채팅 세션이 제대로 유지되고 있는지 확인
- 대화 상태가 올바르게 전환되는지 확인
- 동일한 채팅 세션을 통해 알림이 전송되는지 확인
일반적인 문제
일반적인 문제:
- 로그에서 오류 또는 경고 확인
- Firebase 연결에서 Vertex AI 확인
- 함수 매개변수의 유형 불일치 확인
- 모든 Riverpod 생성 코드가 최신 상태인지 확인
학습한 주요 개념
- UI와 LLM 간의 LLM 컨텍스트 동기화 만들기
- UI 이벤트를 LLM 친화적인 컨텍스트로 직렬화
- 다양한 상호작용 패턴에 맞게 LLM 동작 안내
- 메시지 및 메시지 외 상호작용 전반에서 일관된 환경 만들기
- 더 광범위한 애플리케이션 상태에 대한 LLM 인식 향상
LLM 컨텍스트 동기화를 구현하여 LLM이 단순한 텍스트 생성기가 아닌 인식하고 반응하는 어시스턴트처럼 느껴지는 완전히 통합된 환경을 만들었습니다. 이 패턴은 수많은 다른 애플리케이션에 적용하여 더 자연스럽고 직관적인 AI 기반 인터페이스를 만들 수 있습니다.
9. 축하합니다.
Colorist Codelab을 완료했습니다. 🎉
빌드한 항목
Google의 Gemini API를 통합하여 자연어 색상 설명을 해석하는 완전한 기능을 갖춘 Flutter 애플리케이션을 만들었습니다. 이제 앱에서 다음 작업을 할 수 있습니다.
- '석양 오렌지' 또는 '깊은 바다색 블루'와 같은 자연어 설명을 처리합니다.
- Gemini를 사용하여 이러한 설명을 RGB 값으로 스마트하게 변환합니다.
- 스트리밍 응답을 사용하여 해석된 색상을 실시간으로 표시
- 채팅과 UI 요소를 모두 통해 사용자 상호작용 처리
- 다양한 상호작용 방법에서 문맥 인식 유지
다음 단계
이제 Gemini와 Flutter를 통합하는 기본사항을 숙지했으므로 다음과 같은 방법으로 학습을 계속할 수 있습니다.
Colorist 앱 개선하기
- 색상 팔레트: 보완 또는 일치하는 색 구성표를 생성하는 기능을 추가합니다.
- 음성 입력: 음성 색상 설명을 위한 음성 인식 통합
- 기록 관리: 색상 세트에 이름을 지정하고, 정리하고, 내보낼 수 있는 옵션을 추가합니다.
- 맞춤 프롬프트: 사용자가 시스템 프롬프트를 맞춤설정할 수 있는 인터페이스를 만듭니다.
- 고급 분석: 가장 효과적인 설명이나 문제를 일으키는 설명 추적
Gemini 기능 더보기
- 멀티모달 입력: 이미지 입력을 추가하여 사진에서 색상을 추출합니다.
- 콘텐츠 생성: Gemini를 사용하여 설명이나 스토리와 같은 색상 관련 콘텐츠를 생성합니다.
- 함수 호출 개선: 여러 함수로 더 복잡한 도구 통합을 만듭니다.
- 안전 설정: 다양한 안전 설정과 응답에 미치는 영향을 살펴봅니다.
다른 도메인에 이러한 패턴 적용
- 문서 분석: 문서를 이해하고 분석할 수 있는 앱을 만듭니다.
- 창의적 글쓰기 지원: LLM 기반 제안을 사용하여 작성 도구 빌드
- 작업 자동화: 자연어를 자동 작업으로 변환하는 앱을 설계합니다.
- 지식 기반 애플리케이션: 특정 도메인에서 전문가 시스템을 만듭니다.
리소스
다음은 학습을 계속할 수 있는 유용한 리소스입니다.
공식 문서
프롬프트 과정 및 가이드
커뮤니티
의견
이 Codelab 사용 경험에 관한 의견을 들려주세요. 다음을 통해 의견을 보내주세요.
이 Codelab을 완료해 주셔서 감사합니다. Flutter와 AI의 교차점에서 흥미로운 가능성을 계속 탐색하시기 바랍니다.