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 AI Logic에 연결하고 LLM 통신 구현
- 효과적인 프롬프트 - LLM이 색상 설명을 이해하도록 안내하는 시스템 프롬프트 만들기
- 함수 선언 - LLM이 애플리케이션에서 색상을 설정하는 데 사용할 수 있는 도구를 정의합니다.
- 도구 처리 - LLM의 함수 호출을 처리하고 앱의 상태에 연결합니다.
- 스트리밍 응답 - 실시간 스트리밍 LLM 응답으로 사용자 환경 개선
- LLM 컨텍스트 동기화 - 사용자 작업에 대해 LLM에 알려 일관된 환경 만들기
학습할 내용
- Flutter 애플리케이션용 Firebase AI Logic 구성
- 효과적인 시스템 프롬프트 작성으로 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의 동영상 개요
Observable Flutter 에피소드 #59에서 Craig Labenz와 Andrew Brogdon이 이 Codelab에 관해 논의하는 내용을 시청하세요.
기본 요건
이 Codelab을 최대한 활용하려면 다음이 필요합니다.
- Flutter 개발 경험 - Flutter 기본사항 및 Dart 문법에 대한 이해
- 비동기 프로그래밍 지식 - Future, async/await, 스트림 이해
- Firebase 계정 - Firebase를 설정하려면 Google 계정이 필요합니다.
첫 번째 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.9.2
dependencies:
flutter:
sdk: flutter
colorist_ui: ^0.3.0
flutter_riverpod: ^3.0.0
riverpod_annotation: ^3.0.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^6.0.0
build_runner: ^2.7.1
riverpod_generator: ^3.0.0
riverpod_lint: ^3.0.0
json_serializable: ^6.11.1
flutter:
uses-material-design: true
분석 옵션 구성
프로젝트 루트의 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(chatStateProvider.notifier);
final logStateNotifier = ref.read(logStateProvider.notifier);
chatStateNotifier.addUserMessage(message);
logStateNotifier.logUserText(message);
chatStateNotifier.addLlmMessage(message, MessageState.complete);
logStateNotifier.logLlmText(message);
}
}
이렇게 하면 사용자의 메시지를 반환하여 LLM의 동작을 모방하는 에코 서비스를 구현하는 Flutter 앱이 설정됩니다.
아키텍처 이해
잠시 시간을 내어 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 AI Logic을 사용하여 실제 색상 해석으로 대체합니다.
다음 단계
다음 단계에서는 Firebase를 구성하고 기본 Gemini API 통합을 구현하여 에코 서비스를 Gemini 채팅 서비스로 대체합니다. 이렇게 하면 앱이 색상 설명을 해석하고 지능형 응답을 제공할 수 있습니다.
문제 해결
UI 패키지 문제
colorist_ui
패키지에 문제가 있는 경우 다음 단계를 따르세요.
- 최신 버전을 사용하고 있는지 확인하세요.
- 종속 항목을 올바르게 추가했는지 확인
- 충돌하는 패키지 버전이 있는지 확인
빌드 오류
빌드 오류가 표시되면 다음 단계를 따르세요.
- 최신 안정화 버전 채널 Flutter SDK가 설치되어 있어야 합니다.
flutter clean
을 실행한 후flutter pub get
을 실행합니다.- 콘솔 출력에서 특정 오류 메시지 확인
학습한 주요 개념
- 필요한 종속 항목으로 Flutter 프로젝트 설정
- 애플리케이션의 아키텍처 및 구성요소 책임 이해
- LLM의 동작을 모방하는 간단한 서비스 구현
- 서비스를 UI 구성요소에 연결
- 상태 관리에 Riverpod 사용
3. 기본 Gemini Chat 통합
이 단계에서는 이전 단계의 에코 서비스를 Firebase AI Logic을 사용하는 Gemini API 통합으로 대체합니다. Firebase를 구성하고, 필요한 제공업체를 설정하고, Gemini API와 통신하는 기본 채팅 서비스를 구현합니다.
이 단계에서 학습할 내용
- Flutter 애플리케이션에서 Firebase 설정
- Gemini 액세스를 위한 Firebase AI Logic 구성
- Firebase 및 Gemini 서비스를 위한 Riverpod 제공자 만들기
- Gemini API를 사용하여 기본 채팅 서비스 구현
- 비동기 API 응답 및 오류 상태 처리
Firebase 설정하기
먼저 Flutter 프로젝트에 Firebase를 설정해야 합니다. 여기에는 Firebase 프로젝트를 만들고, 앱을 추가하고, 필요한 Firebase AI Logic 설정을 구성하는 작업이 포함됩니다.
Firebase 프로젝트 만들기
- Firebase Console로 이동하여 Google 계정으로 로그인합니다.
- Firebase 프로젝트 만들기를 클릭하거나 기존 프로젝트를 선택합니다.
- 설정 마법사에 따라 프로젝트를 만듭니다.
Firebase 프로젝트에서 Firebase AI Logic 설정
- Firebase Console에서 프로젝트로 이동합니다.
- 왼쪽 사이드바에서 AI를 선택합니다.
- AI 드롭다운 메뉴에서 AI 로직을 선택합니다.
- Firebase AI Logic 카드에서 시작하기를 선택합니다.
- 메시지에 따라 프로젝트에 Gemini Developer API를 사용 설정합니다.
FlutterFire CLI 설치
FlutterFire CLI를 사용하면 Flutter 앱에서 Firebase 설정을 간소화할 수 있습니다.
dart pub global activate flutterfire_cli
Flutter 앱에 Firebase 추가
- Firebase 핵심 및 Firebase AI Logic 패키지를 프로젝트에 추가합니다.
flutter pub add firebase_core firebase_ai
- FlutterFire 구성 명령어를 실행합니다.
flutterfire configure
이 명령어는 다음 작업을 실행합니다.
- 방금 만든 Firebase 프로젝트를 선택하라는 메시지가 표시됩니다.
- Firebase에 Flutter 앱 등록
- 프로젝트 구성으로
firebase_options.dart
파일 생성
이 명령어를 사용하면 선택한 플랫폼 (iOS, Android, macOS, Windows, 웹)이 자동으로 감지되고 적절하게 구성됩니다.
플랫폼별 구성
Firebase에는 Flutter의 기본값보다 높은 최소 버전이 필요합니다. 또한 Firebase AI Logic 서버와 통신하려면 네트워크 액세스 권한이 필요합니다.
macOS 권한 구성
macOS의 경우 앱의 권한에서 네트워크 액세스를 사용 설정해야 합니다.
macos/Runner/DebugProfile.entitlements
을 열고 다음을 추가합니다.
macos/Runner/DebugProfile.entitlements
<key>com.apple.security.network.client</key>
<true/>
macos/Runner/Release.entitlements
도 열고 동일한 항목을 추가합니다.
iOS 설정 구성
iOS의 경우 ios/Podfile
상단의 최소 버전을 업데이트합니다.
ios/Podfile
# Firebase requires at least iOS 15.0
platform :ios, '15.0'
Gemini 모델 제공업체 만들기
이제 Firebase 및 Gemini용 Riverpod 제공자를 만듭니다. lib/providers/gemini.dart
라는 새 파일을 만듭니다.
lib/providers/gemini.dart
import 'dart:async';
import 'package:firebase_ai/firebase_ai.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../firebase_options.dart';
part 'gemini.g.dart';
@Riverpod(keepAlive: true)
Future<FirebaseApp> firebaseApp(Ref ref) =>
Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
@Riverpod(keepAlive: true)
Future<GenerativeModel> geminiModel(Ref ref) async {
await ref.watch(firebaseAppProvider.future);
final model = FirebaseAI.googleAI().generativeModel(
model: 'gemini-2.0-flash',
);
return model;
}
@Riverpod(keepAlive: true)
Future<ChatSession> chatSession(Ref ref) async {
final model = await ref.watch(geminiModelProvider.future);
return model.startChat();
}
이 파일은 세 가지 주요 키 제공업체의 기반을 정의합니다. 이러한 제공자는 Riverpod 코드 생성기로 dart run build_runner
를 실행할 때 생성됩니다. 이 코드는 업데이트된 제공자 패턴과 함께 Riverpod 3의 주석 기반 접근 방식을 사용합니다.
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_ai/firebase_ai.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../providers/gemini.dart';
part 'gemini_chat_service.g.dart';
class GeminiChatService {
GeminiChatService(this.ref);
final Ref ref;
Future<void> sendMessage(String message) async {
final chatSession = await ref.read(chatSessionProvider.future);
final chatStateNotifier = ref.read(chatStateProvider.notifier);
final logStateNotifier = ref.read(logStateProvider.notifier);
chatStateNotifier.addUserMessage(message);
logStateNotifier.logUserText(message);
final llmMessage = chatStateNotifier.createLlmMessage();
try {
final response = await chatSession.sendMessage(Content.text(message));
final responseText = response.text;
if (responseText != null) {
logStateNotifier.logLlmText(responseText);
chatStateNotifier.appendToMessage(llmMessage.id, responseText);
}
} catch (e, st) {
logStateNotifier.logError(e, st: st);
chatStateNotifier.appendToMessage(
llmMessage.id,
"\nI'm sorry, I encountered an error processing your request. "
"Please try again.",
);
} finally {
chatStateNotifier.finalizeMessage(llmMessage.id);
}
}
}
@Riverpod(keepAlive: true)
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 AI Logic을 통해 Gemini API로 전송됩니다.
- LLM 처리: Gemini 모델이 텍스트를 처리하고 대답을 생성합니다.
- 응답 처리: 앱이 응답을 수신하고 UI를 업데이트합니다.
- 로깅: 투명성을 위해 모든 커뮤니케이션이 기록됩니다.
채팅 세션 및 대화 컨텍스트
Gemini 채팅 세션은 메시지 간에 컨텍스트를 유지하여 대화형 상호작용을 지원합니다. 즉, LLM이 현재 세션의 이전 대화를 '기억'하여 더 일관성 있는 대화가 가능합니다.
채팅 세션 제공자에 관한 keepAlive: true
주석은 이 컨텍스트가 앱의 수명 주기 전반에 걸쳐 유지되도록 합니다. 이 지속적인 컨텍스트는 LLM과의 자연스러운 대화 흐름을 유지하는 데 매우 중요합니다.
다음 단계
이 시점에서는 Gemini API가 대답할 내용에 제한이 없으므로 무엇이든 물어볼 수 있습니다. 예를 들어 색상 앱의 목적과 관련이 없는 장미 전쟁에 관한 요약을 요청할 수 있습니다.
다음 단계에서는 Gemini가 색상 설명을 더 효과적으로 해석하도록 안내하는 시스템 프롬프트를 만듭니다. 이를 통해 애플리케이션별 요구사항에 맞게 LLM의 동작을 맞춤설정하고 앱 도메인에 기능을 집중하는 방법을 알 수 있습니다.
문제 해결
Firebase 구성 문제
Firebase 초기화에 오류가 발생하면 다음 단계를 따르세요.
firebase_options.dart
파일이 올바르게 생성되었는지 확인- Firebase AI Logic 액세스를 위해 Blaze 요금제로 업그레이드했는지 확인합니다.
API 액세스 오류
Gemini API에 액세스할 때 오류가 발생하면 다음 단계를 따르세요.
- Firebase 프로젝트에 결제가 올바르게 설정되어 있는지 확인
- Firebase 프로젝트에서 Firebase AI Logic 및 Cloud AI API가 사용 설정되어 있는지 확인합니다.
- 네트워크 연결 및 방화벽 설정 확인
- 모델 이름 (
gemini-2.0-flash
)이 올바르고 사용 가능한지 확인합니다.
대화 컨텍스트 문제
Gemini가 채팅의 이전 컨텍스트를 기억하지 못하는 경우 다음 단계를 따르세요.
chatSession
함수가@Riverpod(keepAlive: true)
로 주석이 지정되었는지 확인합니다.- 모든 메시지 교환에 동일한 채팅 세션을 재사용하고 있는지 확인
- 메시지를 보내기 전에 채팅 세션이 올바르게 초기화되었는지 확인하세요.
플랫폼별 문제
플랫폼별 문제:
- iOS/macOS: 적절한 권한이 설정되고 최소 버전이 구성되어 있는지 확인
- Android: 최소 SDK 버전이 올바르게 설정되었는지 확인
- 콘솔에서 플랫폼별 오류 메시지 확인
학습한 주요 개념
- Flutter 애플리케이션에서 Firebase 설정
- Gemini에 액세스하도록 Firebase AI Logic 구성
- 비동기 서비스를 위한 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
하단을 업데이트합니다.
pubspec.yaml
flutter:
uses-material-design: true
assets:
- assets/
flutter pub get
를 실행하여 애셋 번들을 새로고침합니다.
시스템 프롬프트 제공자 만들기
시스템 프롬프트를 로드할 새 파일 lib/providers/system_prompt.dart
을 만듭니다.
lib/providers/system_prompt.dart
import 'package:flutter/services.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'system_prompt.g.dart';
@Riverpod(keepAlive: true)
Future<String> systemPrompt(Ref ref) =>
rootBundle.loadString('assets/system_prompt.md');
이 제공자는 Flutter의 애셋 로드 시스템을 사용하여 런타임에 프롬프트 파일을 읽습니다.
Gemini 모델 제공업체 업데이트
이제 시스템 프롬프트를 포함하도록 lib/providers/gemini.dart
파일을 수정합니다.
lib/providers/gemini.dart
import 'dart:async';
import 'package:firebase_ai/firebase_ai.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../firebase_options.dart';
import 'system_prompt.dart'; // Add this import
part 'gemini.g.dart';
@Riverpod(keepAlive: true)
Future<FirebaseApp> firebaseApp(Ref ref) =>
Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
@Riverpod(keepAlive: true)
Future<GenerativeModel> geminiModel(Ref ref) async {
await ref.watch(firebaseAppProvider.future);
final systemPrompt = await ref.watch(systemPromptProvider.future); // Add this line
final model = FirebaseAI.googleAI().generativeModel(
model: 'gemini-2.0-flash',
systemInstruction: Content.system(systemPrompt), // And this line
);
return model;
}
@Riverpod(keepAlive: true)
Future<ChatSession> chatSession(Ref ref) async {
final model = await ref.watch(geminiModelProvider.future);
return model.startChat();
}
키 변경사항은 생성형 모델을 만들 때 systemInstruction: Content.system(systemPrompt)
를 추가하는 것입니다. 이렇게 하면 Gemini가 이 채팅 세션의 모든 상호작용에 사용자의 요청 사항을 시스템 프롬프트로 사용합니다.
Riverpod 코드 생성
빌드 러너 명령어를 실행하여 필요한 Riverpod 코드를 생성합니다.
dart run build_runner build --delete-conflicting-outputs
애플리케이션 실행 및 테스트
이제 애플리케이션을 실행합니다.
flutter run -d DEVICE
다양한 색상 설명으로 테스트해 보세요.
- '하늘색으로 해 줘'
- 'Give me a forest green'(포레스트 그린을 줘)
- '생기 있는 일몰 주황색을 만들어 줘'
- 'I want the color of fresh lavender(신선한 라벤더 색상으로 해 줘)'
- 'Show me something like a deep ocean blue(심해 파란색과 비슷한 색상 보여 줘)'
이제 Gemini가 일관된 형식의 RGB 값과 함께 색상에 관한 대화형 설명을 제공합니다. 시스템 프롬프트가 LLM이 필요한 유형의 대답을 제공하도록 효과적으로 안내했습니다.
색상과 관련 없는 콘텐츠를 요청해 보세요. 예를 들어 장미 전쟁의 주요 원인과 같은 것 말이야. 이전 단계와 차이가 있음을 알 수 있습니다.
전문 작업에 프롬프트 엔지니어링이 중요한 이유
시스템 프롬프트는 예술이자 과학입니다. 프롬프트는 LLM 통합의 중요한 부분으로, 특정 애플리케이션에서 모델의 유용성에 큰 영향을 미칠 수 있습니다. 여기서 수행한 작업은 프롬프트 엔지니어링의 한 형태입니다. 즉, 애플리케이션의 요구사항에 맞는 방식으로 모델이 작동하도록 맞춤설정된 안내를 제공한 것입니다.
효과적인 프롬프트 엔지니어링에는 다음이 포함됩니다.
- 명확한 역할 정의: LLM의 목적 설정
- 명시적 안내: LLM이 응답해야 하는 방식을 정확하게 설명합니다.
- 구체적인 예: 좋은 대답이 어떤 것인지 말로 설명하는 대신 보여주기
- 특이 사례 처리: 모호한 시나리오를 처리하는 방법을 LLM에 안내
- 형식 지정 사양: 대답이 일관되고 사용 가능한 방식으로 구성되도록 합니다.
생성한 시스템 프롬프트는 Gemini의 일반적인 기능을 애플리케이션의 요구사항에 맞게 특별히 포맷된 대답을 제공하는 전문 색상 해석 도우미로 변환합니다. 이는 다양한 도메인과 작업에 적용할 수 있는 강력한 패턴입니다.
다음 단계
다음 단계에서는 이 기반을 바탕으로 함수 선언을 추가하여 LLM이 RGB 값을 제안할 뿐만 아니라 앱에서 함수를 실제로 호출하여 색상을 직접 설정할 수 있도록 합니다. 이를 통해 LLM이 자연어와 구체적인 애플리케이션 기능 간의 격차를 어떻게 해소할 수 있는지 알 수 있습니다.
문제 해결
애셋 로드 문제
시스템 프롬프트를 로드하는 중에 오류가 발생하면 다음 단계를 따르세요.
pubspec.yaml
에 애셋 디렉터리가 올바르게 나열되어 있는지 확인합니다.rootBundle.loadString()
의 경로가 파일 위치와 일치하는지 확인합니다.flutter clean
를 실행한 다음flutter pub get
를 실행하여 애셋 번들을 새로고침합니다.
비일관적인 대답
LLM이 형식 안내를 일관되게 따르지 않는 경우 다음 단계를 따르세요.
- 시스템 프롬프트에서 형식 요구사항을 더 명시적으로 지정해 보세요.
- 예상을 보여주는 예시를 더 추가하세요.
- 요청하는 형식이 모델에 적합한지 확인합니다.
API 비율 제한
비율 제한과 관련된 오류가 발생하면 다음 단계를 따르세요.
- Firebase AI Logic 서비스에는 사용량 제한이 있습니다.
- 지수 백오프로 재시도 로직 구현 고려
- Firebase Console에서 할당량 문제 확인
학습한 주요 개념
- LLM 애플리케이션에서 시스템 프롬프트의 역할과 중요성 이해하기
- 명확한 요청 사항, 예시, 제약 조건을 사용하여 효과적인 프롬프트 작성
- Flutter 애플리케이션에서 시스템 프롬프트 로드 및 사용
- 도메인별 작업을 위한 LLM 동작 안내
- 프롬프트 엔지니어링을 사용하여 LLM 응답 형성
이 단계에서는 시스템 프롬프트에 명확한 안내를 제공하는 것만으로 코드를 변경하지 않고도 LLM 동작을 크게 맞춤설정할 수 있는 방법을 보여줍니다.
5. LLM 도구의 함수 선언
이 단계에서는 함수 선언을 구현하여 Gemini가 앱에서 작업을 실행할 수 있도록 하는 작업을 시작합니다. 이 강력한 기능을 사용하면 LLM이 RGB 값을 제안할 뿐만 아니라 특수 도구 호출을 통해 앱의 UI에서 실제로 설정할 수 있습니다. 하지만 Flutter 앱에서 실행된 LLM 요청을 확인하려면 다음 단계가 필요합니다.
이 단계에서 학습할 내용
- LLM 함수 호출 및 Flutter 애플리케이션의 이점 이해
- Gemini의 스키마 기반 함수 선언 정의
- Gemini 모델과 함수 선언 통합
- 도구 기능을 활용하도록 시스템 프롬프트 업데이트
함수 호출 이해하기
함수 선언을 구현하기 전에 함수 선언이 무엇이고 왜 유용한지 알아보겠습니다.
함수 호출이란 무엇인가요?
함수 호출('도구 사용'이라고도 함)은 LLM이 다음 작업을 할 수 있도록 지원하는 기능입니다.
- 사용자 요청이 특정 함수를 호출할 때 이점을 얻을 수 있는 경우 인식
- 해당 함수에 필요한 매개변수가 포함된 구조화된 JSON 객체를 생성합니다.
- 애플리케이션이 이러한 매개변수로 함수를 실행하도록 합니다.
- 함수의 결과를 수신하고 응답에 통합
LLM이 해야 할 일을 설명하는 대신 함수 호출을 통해 LLM이 애플리케이션에서 구체적인 작업을 트리거할 수 있습니다.
Flutter 앱에서 함수 호출이 중요한 이유
함수 호출은 자연어와 애플리케이션 기능 간에 강력한 연결을 만듭니다.
- 직접 작업: 사용자가 원하는 것을 자연어로 설명하면 앱이 구체적인 작업으로 응답합니다.
- 구조화된 출력: LLM은 파싱이 필요한 텍스트가 아닌 깔끔하고 구조화된 데이터를 생성합니다.
- 복잡한 작업: LLM이 외부 데이터에 액세스하거나, 계산을 실행하거나, 애플리케이션 상태를 수정할 수 있도록 지원합니다.
- 더 나은 사용자 환경: 대화와 기능 간의 원활한 통합을 지원합니다.
Colorist 앱에서 함수 호출을 사용하면 사용자가 'I want a forest green'(포레스트 그린을 원해)이라고 말하면 텍스트에서 RGB 값을 파싱하지 않고도 UI가 해당 색상으로 즉시 업데이트됩니다.
함수 선언 정의
새 파일 lib/services/gemini_tools.dart
을 만들어 함수 선언을 정의합니다.
lib/services/gemini_tools.dart
import 'package:firebase_ai/firebase_ai.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'gemini_tools.g.dart';
class GeminiTools {
GeminiTools(this.ref);
final Ref ref;
FunctionDeclaration get setColorFuncDecl => FunctionDeclaration(
'set_color',
'Set the color of the display square based on red, green, and blue values.',
parameters: {
'red': Schema.number(description: 'Red component value (0.0 - 1.0)'),
'green': Schema.number(description: 'Green component value (0.0 - 1.0)'),
'blue': Schema.number(description: 'Blue component value (0.0 - 1.0)'),
},
);
List<Tool> get tools => [
Tool.functionDeclarations([setColorFuncDecl]),
];
}
@Riverpod(keepAlive: true)
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_ai/firebase_ai.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../firebase_options.dart';
import '../services/gemini_tools.dart'; // Add this import
import 'system_prompt.dart';
part 'gemini.g.dart';
@Riverpod(keepAlive: true)
Future<FirebaseApp> firebaseApp(Ref ref) =>
Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
@Riverpod(keepAlive: true)
Future<GenerativeModel> geminiModel(Ref ref) async {
await ref.watch(firebaseAppProvider.future);
final systemPrompt = await ref.watch(systemPromptProvider.future);
final geminiTools = ref.watch(geminiToolsProvider); // Add this line
final model = FirebaseAI.googleAI().generativeModel(
model: 'gemini-2.0-flash',
systemInstruction: Content.system(systemPrompt),
tools: geminiTools.tools, // And this line
);
return model;
}
@Riverpod(keepAlive: true)
Future<ChatSession> chatSession(Ref ref) async {
final model = await ref.watch(geminiModelProvider.future);
return model.startChat();
}
핵심 변경사항은 생성형 모델을 만들 때 tools: geminiTools.tools
매개변수를 추가하는 것입니다. 이렇게 하면 Gemini가 호출할 수 있는 함수를 인식합니다.
시스템 프롬프트 업데이트
이제 새 set_color
도구를 사용하도록 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.
## 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는 다음을 기반으로 함수를 사용할 시기에 관해 지능적인 결정을 내립니다.
- 사용자 의도: 사용자의 요청에 함수를 사용하는 것이 가장 적합한지 여부
- 함수 관련성: 사용 가능한 함수가 작업과 얼마나 잘 일치하는지
- 매개변수 사용 가능 여부: 매개변수 값을 확실하게 결정할 수 있는지 여부
- 시스템 안내: 함수 사용에 관한 시스템 프롬프트의 안내
명확한 함수 선언과 시스템 안내를 제공하여 Gemini가 색상 설명 요청을 set_color
함수를 호출할 기회로 인식하도록 설정했습니다.
다음 단계
다음 단계에서는 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_ai/firebase_ai.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'gemini_tools.g.dart';
class GeminiTools {
GeminiTools(this.ref);
final Ref ref;
FunctionDeclaration get setColorFuncDecl => FunctionDeclaration(
'set_color',
'Set the color of the display square based on red, green, and blue values.',
parameters: {
'red': Schema.number(description: 'Red component value (0.0 - 1.0)'),
'green': Schema.number(description: 'Green component value (0.0 - 1.0)'),
'blue': Schema.number(description: 'Blue component value (0.0 - 1.0)'),
},
);
List<Tool> get tools => [
Tool.functionDeclarations([setColorFuncDecl]),
];
Map<String, Object?> handleFunctionCall( // Add from here
String functionName,
Map<String, Object?> arguments,
) {
final logStateNotifier = ref.read(logStateProvider.notifier);
logStateNotifier.logFunctionCall(functionName, arguments);
return switch (functionName) {
'set_color' => handleSetColor(arguments),
_ => handleUnknownFunction(functionName),
};
}
Map<String, Object?> handleSetColor(Map<String, Object?> arguments) {
final colorStateNotifier = ref.read(colorStateProvider.notifier);
final red = (arguments['red'] as num).toDouble();
final green = (arguments['green'] as num).toDouble();
final blue = (arguments['blue'] as num).toDouble();
final functionResults = {
'success': true,
'current_color': colorStateNotifier
.updateColor(red: red, green: green, blue: blue)
.toLLMContextMap(),
};
final logStateNotifier = ref.read(logStateProvider.notifier);
logStateNotifier.logFunctionResults(functionResults);
return functionResults;
}
Map<String, Object?> handleUnknownFunction(String functionName) {
final logStateNotifier = ref.read(logStateProvider.notifier);
logStateNotifier.logWarning('Unsupported function call $functionName');
return {
'success': false,
'reason': 'Unsupported function call $functionName',
};
} // To here.
}
@Riverpod(keepAlive: true)
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_ai/firebase_ai.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../providers/gemini.dart';
import 'gemini_tools.dart'; // Add this import
part 'gemini_chat_service.g.dart';
class GeminiChatService {
GeminiChatService(this.ref);
final Ref ref;
Future<void> sendMessage(String message) async {
final chatSession = await ref.read(chatSessionProvider.future);
final chatStateNotifier = ref.read(chatStateProvider.notifier);
final logStateNotifier = ref.read(logStateProvider.notifier);
chatStateNotifier.addUserMessage(message);
logStateNotifier.logUserText(message);
final llmMessage = chatStateNotifier.createLlmMessage();
try {
final response = await chatSession.sendMessage(Content.text(message));
final responseText = response.text;
if (responseText != null) {
logStateNotifier.logLlmText(responseText);
chatStateNotifier.appendToMessage(llmMessage.id, responseText);
}
if (response.functionCalls.isNotEmpty) { // Add from here
final geminiTools = ref.read(geminiToolsProvider);
final functionResultResponse = await chatSession.sendMessage(
Content.functionResponses([
for (final functionCall in response.functionCalls)
FunctionResponse(
functionCall.name,
geminiTools.handleFunctionCall(
functionCall.name,
functionCall.args,
),
),
]),
);
final responseText = functionResultResponse.text;
if (responseText != null) {
logStateNotifier.logLlmText(responseText);
chatStateNotifier.appendToMessage(llmMessage.id, responseText);
}
} // To here.
} catch (e, st) {
logStateNotifier.logError(e, st: st);
chatStateNotifier.appendToMessage(
llmMessage.id,
"\nI'm sorry, I encountered an error processing your request. "
"Please try again.",
);
} finally {
chatStateNotifier.finalizeMessage(llmMessage.id);
}
}
}
@Riverpod(keepAlive: true)
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
다양한 색상 설명을 입력해 보세요.
- 'I'd like a deep crimson red(진한 크림슨 레드를 원해)'
- 'Show me a calming sky blue'(차분한 하늘색을 보여 줘)
- 'Give me the color of fresh mint leaves'(신선한 민트 잎의 색상을 알려 줘)
- '따뜻한 일몰 오렌지색을 보고 싶어'
- '진한 보라색으로 해 줘'
이제 다음과 같이 표시됩니다.
- 채팅 인터페이스에 표시되는 메시지
- 채팅에 표시되는 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 값이나 색상 이론을 이해할 필요가 없습니다.
자연어와 UI 작업을 연결하기 위해 LLM 함수 호출을 사용하는 이 패턴은 색상 선택 외에도 수많은 다른 도메인으로 확장할 수 있습니다.
다음 단계
다음 단계에서는 스트리밍 응답을 구현하여 사용자 환경을 개선합니다. 전체 응답을 기다리는 대신 텍스트 청크와 함수 호출을 수신되는 대로 처리하여 더 응답성이 높고 매력적인 애플리케이션을 만들 수 있습니다.
문제 해결
함수 호출 문제
Gemini가 함수를 호출하지 않거나 매개변수가 잘못된 경우:
- 함수 선언이 시스템 프롬프트에 설명된 내용과 일치하는지 확인
- 매개변수 이름과 유형이 일치하는지 확인
- 시스템 프롬프트에서 LLM에 도구를 사용하도록 명시적으로 지시해야 합니다.
- 핸들러의 함수 이름이 선언의 이름과 정확히 일치하는지 확인합니다.
- 로그 패널에서 함수 호출에 관한 자세한 정보 확인
함수 응답 문제
함수 결과가 LLM에 올바르게 다시 전송되지 않는 경우:
- 함수가 올바른 형식의 지도를 반환하는지 확인
- Content.functionResponses가 올바르게 구성되고 있는지 확인
- 함수 응답과 관련된 로그에서 오류를 찾습니다.
- 대답에 동일한 채팅 세션을 사용해야 합니다.
색상 표시 문제
색상이 올바르게 표시되지 않는 경우 다음 단계를 따르세요.
- RGB 값이 double로 올바르게 변환되었는지 확인합니다 (LLM이 정수로 보낼 수 있음).
- 값이 예상 범위 (0.0~1.0)에 있는지 확인합니다.
- 색상 상태 알림이 올바르게 호출되는지 확인
- 함수에 전달되는 정확한 값을 로그에서 확인합니다.
일반적인 문제
일반적인 문제의 경우:
- 로그에서 오류 또는 경고 검사
- Firebase AI Logic 연결 확인
- 함수 매개변수의 유형 불일치 확인
- 모든 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를 점진적으로 업데이트하여 더욱 동적인 환경을 만들 수 있습니다.
- 대화 상태 관리: 스트리밍은 대답이 완료되었는지 아니면 아직 진행 중인지에 관한 명확한 신호를 제공하여 더 나은 상태 관리를 지원합니다.
- 제한 시간 위험 감소: 비스트리밍 응답을 사용하면 장기 실행 생성으로 인해 연결 제한 시간이 발생할 수 있습니다. 스트리밍은 연결을 일찍 설정하고 유지합니다.
컬러리스트 앱의 경우 스트리밍을 구현하면 사용자에게 텍스트 응답과 색상 변경이 더 빠르게 표시되어 훨씬 더 반응성이 높은 환경이 조성됩니다.
대화 상태 관리 추가
먼저 앱이 현재 스트리밍 응답을 처리하고 있는지 추적하는 상태 제공자를 추가해 보겠습니다. lib/services/gemini_chat_service.dart
파일을 업데이트합니다.
lib/services/gemini_chat_service.dart
import 'dart:async';
import 'package:colorist_ui/colorist_ui.dart';
import 'package:firebase_ai/firebase_ai.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../providers/gemini.dart';
import 'gemini_tools.dart';
part 'gemini_chat_service.g.dart';
class ConversationStateNotifier extends Notifier<ConversationState> { // Add from here...
@override
ConversationState build() => ConversationState.idle;
void busy() {
state = ConversationState.busy;
}
void idle() {
state = ConversationState.idle;
}
}
final conversationStateProvider =
NotifierProvider<ConversationStateNotifier, ConversationState>(
ConversationStateNotifier.new,
); // To here.
class GeminiChatService {
GeminiChatService(this.ref);
final Ref ref;
Future<void> sendMessage(String message) async {
final chatSession = await ref.read(chatSessionProvider.future);
final conversationState = ref.read(conversationStateProvider); // Add this line
final chatStateNotifier = ref.read(chatStateProvider.notifier);
final logStateNotifier = ref.read(logStateProvider.notifier);
if (conversationState == ConversationState.busy) { // Add from here...
logStateNotifier.logWarning(
"Can't send a message while a conversation is in progress",
);
throw Exception(
"Can't send a message while a conversation is in progress",
);
}
final conversationStateNotifier = ref.read(
conversationStateProvider.notifier,
);
conversationStateNotifier.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.idle(); // Add this line.
}
}
Future<void> _processBlock( // Add from here...
GenerateContentResponse block,
String llmMessageId,
) async {
final chatSession = await ref.read(chatSessionProvider.future);
final chatStateNotifier = ref.read(chatStateProvider.notifier);
final logStateNotifier = ref.read(logStateProvider.notifier);
final blockText = block.text;
if (blockText != null) {
logStateNotifier.logLlmText(blockText);
chatStateNotifier.appendToMessage(llmMessageId, blockText);
}
if (block.functionCalls.isNotEmpty) {
final geminiTools = ref.read(geminiToolsProvider);
final responseStream = chatSession.sendMessageStream(
Content.functionResponses([
for (final functionCall in block.functionCalls)
FunctionResponse(
functionCall.name,
geminiTools.handleFunctionCall(
functionCall.name,
functionCall.args,
),
),
]),
);
await for (final response in responseStream) {
final responseText = response.text;
if (responseText != null) {
logStateNotifier.logLlmText(responseText);
chatStateNotifier.appendToMessage(llmMessageId, responseText);
}
}
}
} // To here.
}
@Riverpod(keepAlive: true)
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()
을 호출하면 다음과 같은 결과가 발생합니다.
- 앱이 Firebase AI Logic 서비스에 연결을 설정합니다.
- 사용자 요청이 서비스로 전송됩니다.
- 서버가 요청 처리를 시작합니다.
- 스트림 연결이 열린 상태로 유지되어 청크를 전송할 준비가 됩니다.
청크 전송
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 AI Logic 연결 확인
- 3단계: 모든 Riverpod 생성 코드가 최신 상태인지 확인
- 4단계: 스트리밍 구현에서 누락된 await 문이 있는지 검토
학습한 주요 개념
- Gemini API로 스트리밍 응답을 구현하여 더 빠른 UX 제공
- 스트리밍 상호작용을 적절하게 처리하기 위해 대화 상태 관리
- 실시간 문자 메시지와 함수 호출이 도착하면 처리
- 스트리밍 중에 점진적으로 업데이트되는 반응형 UI 만들기
- 적절한 비동기 패턴으로 동시 스트림 처리
- 스트리밍 응답 중에 적절한 시각적 피드백 제공
스트리밍을 구현하여 Colorist 앱의 사용자 환경을 크게 개선하고, 진정한 대화형 인터페이스를 제공하여 반응성이 높고 매력적인 앱을 만들었습니다.
8. LLM 컨텍스트 동기화
이 보너스 단계에서는 사용자가 기록에서 색상을 선택할 때 Gemini에 알림을 보내 LLM 컨텍스트 동기화를 구현합니다. 이렇게 하면 LLM이 명시적인 메시지뿐만 아니라 인터페이스에서의 사용자 작업을 인식하는 더 일관된 환경이 만들어집니다.
이 단계에서 다룰 내용
- UI와 LLM 간의 LLM 컨텍스트 동기화 만들기
- LLM이 이해할 수 있는 컨텍스트로 UI 이벤트 직렬화
- 사용자 작업에 따라 대화 컨텍스트 업데이트
- 다양한 상호작용 방법에서 일관된 환경 만들기
- 명시적인 채팅 메시지 외에 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_ai/firebase_ai.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../providers/gemini.dart';
import 'gemini_tools.dart';
part 'gemini_chat_service.g.dart';
class ConversationStateNotifier extends Notifier<ConversationState> {
@override
ConversationState build() => ConversationState.idle;
void busy() {
state = ConversationState.busy;
}
void idle() {
state = ConversationState.idle;
}
}
final conversationStateProvider =
NotifierProvider<ConversationStateNotifier, ConversationState>(
ConversationStateNotifier.new,
);
class GeminiChatService {
GeminiChatService(this.ref);
final Ref ref;
Future<void> notifyColorSelection(ColorData color) => sendMessage( // Add from here...
'User selected color from history: ${json.encode(color.toLLMContextMap())}',
); // To here.
Future<void> sendMessage(String message) async {
final chatSession = await ref.read(chatSessionProvider.future);
final conversationState = ref.read(conversationStateProvider);
final chatStateNotifier = ref.read(chatStateProvider.notifier);
final logStateNotifier = ref.read(logStateProvider.notifier);
if (conversationState == ConversationState.busy) {
logStateNotifier.logWarning(
"Can't send a message while a conversation is in progress",
);
throw Exception(
"Can't send a message while a conversation is in progress",
);
}
final conversationStateNotifier = ref.read(
conversationStateProvider.notifier,
);
conversationStateNotifier.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.idle();
}
}
Future<void> _processBlock(
GenerateContentResponse block,
String llmMessageId,
) async {
final chatSession = await ref.read(chatSessionProvider.future);
final chatStateNotifier = ref.read(chatStateProvider.notifier);
final logStateNotifier = ref.read(logStateProvider.notifier);
final blockText = block.text;
if (blockText != null) {
logStateNotifier.logLlmText(blockText);
chatStateNotifier.appendToMessage(llmMessageId, blockText);
}
if (block.functionCalls.isNotEmpty) {
final geminiTools = ref.read(geminiToolsProvider);
final responseStream = chatSession.sendMessageStream(
Content.functionResponses([
for (final functionCall in block.functionCalls)
FunctionResponse(
functionCall.name,
geminiTools.handleFunctionCall(
functionCall.name,
functionCall.args,
),
),
]),
);
await for (final response in responseStream) {
final responseText = response.text;
if (responseText != null) {
logStateNotifier.logLlmText(responseText);
chatStateNotifier.appendToMessage(llmMessageId, responseText);
}
}
}
}
}
@Riverpod(keepAlive: true)
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 컨텍스트 동기화 테스트에는 다음이 포함됩니다.
- 먼저 채팅에서 색상을 설명하여 몇 가지 색상을 생성합니다.
- '선명한 보라색 보여 줘'
- 'I'd like a forest green'
- '밝은 빨간색으로 해 줘'
- 그런 다음 기록 스트립에서 색상 썸네일 중 하나를 클릭합니다.
다음과 같이 표시됩니다.
- 선택한 색상이 기본 디스플레이에 표시됩니다.
- 채팅에 색상 선택을 나타내는 사용자 메시지가 표시됩니다.
- LLM은 선택을 확인하고 색상에 관해 언급하여 응답합니다.
- 전체 상호작용이 자연스럽고 일관성 있게 느껴집니다.
이렇게 하면 LLM이 다이렉트 메시지와 UI 상호작용을 모두 인식하고 적절하게 응답하는 원활한 환경이 만들어집니다.
LLM 컨텍스트 동기화 작동 방식
이 동기화가 작동하는 방식의 기술적 세부정보를 살펴보겠습니다.
데이터 흐름
- 사용자 작업: 사용자가 기록 스트립에서 색상을 클릭합니다.
- UI 이벤트:
MainScreen
위젯이 이 선택을 감지합니다. - 콜백 실행:
notifyColorSelection
콜백이 트리거됩니다. - 메시지 생성: 색상 데이터로 특수 형식의 메시지가 생성됩니다.
- LLM 처리: 메시지가 Gemini로 전송되며, 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 AI Logic 연결 확인
- 함수 매개변수의 유형 불일치 확인
- 모든 Riverpod 생성 코드가 최신 상태인지 확인
학습한 주요 개념
- UI와 LLM 간 LLM 컨텍스트 동기화 만들기
- UI 이벤트를 LLM 친화적인 컨텍스트로 직렬화
- 다양한 상호작용 패턴에 맞게 LLM 동작 안내
- 메시지 및 비메시지 상호작용 전반에 걸쳐 일관된 경험 만들기
- 더 광범위한 애플리케이션 상태에 대한 LLM 인식 개선
LLM 컨텍스트 동기화를 구현하면 LLM이 텍스트 생성기일 뿐만 아니라 인식하고 반응하는 어시스턴트처럼 느껴지는 진정한 통합 환경을 만들 수 있습니다. 이 패턴은 수많은 다른 애플리케이션에 적용하여 더 자연스럽고 직관적인 AI 기반 인터페이스를 만들 수 있습니다.
9. 축하합니다.
컬러리스트 Codelab을 완료했습니다. 🎉
빌드한 항목
Google의 Gemini API를 통합하여 자연어 색상 설명을 해석하는 완전한 기능을 갖춘 Flutter 애플리케이션을 만들었습니다. 이제 앱에서 다음 작업을 할 수 있습니다.
- '일몰 오렌지색' 또는 '심해 파란색'과 같은 자연어 설명 처리
- Gemini를 사용하여 이러한 설명을 RGB 값으로 지능적으로 변환합니다.
- 스트리밍 응답으로 해석된 색상을 실시간으로 표시
- 채팅과 UI 요소를 모두 통해 사용자 상호작용 처리
- 다양한 상호작용 방법에서 맥락 인식 유지
다음 단계
이제 Gemini를 Flutter와 통합하는 기본사항을 숙지했으므로 다음과 같은 방법으로 여정을 이어갈 수 있습니다.
Colorist 앱 개선
- 색상 팔레트: 보색 또는 일치하는 색상 구성표를 생성하는 기능 추가
- 음성 입력: 음성 색상 설명을 위한 음성 인식 통합
- 기록 관리: 색상 세트의 이름을 지정하고, 정리하고, 내보내는 옵션 추가
- 맞춤 프롬프트: 사용자가 시스템 프롬프트를 맞춤설정할 수 있는 인터페이스를 만듭니다.
- 고급 분석: 어떤 설명이 가장 효과적인지 또는 어떤 설명이 어려움을 야기하는지 추적
더 많은 Gemini 기능 살펴보기
- 멀티모달 입력: 이미지 입력을 추가하여 사진에서 색상 추출
- 콘텐츠 생성: Gemini를 사용하여 설명이나 스토리와 같은 색상 관련 콘텐츠를 생성합니다.
- 함수 호출 개선사항: 여러 함수를 사용하여 더 복잡한 도구 통합 만들기
- 안전 설정: 다양한 안전 설정과 응답에 미치는 영향 살펴보기
다른 도메인에 이 패턴 적용
- 문서 분석: 문서를 이해하고 분석할 수 있는 앱 만들기
- 창작 글쓰기 지원: LLM 기반 추천으로 글쓰기 도구 빌드
- 작업 자동화: 자연어를 자동화된 작업으로 변환하는 앱 설계
- 지식 기반 애플리케이션: 특정 도메인에서 전문가 시스템 만들기
리소스
학습을 계속하는 데 도움이 되는 유용한 리소스를 소개합니다.
공식 문서
프롬프트 작성 과정 및 가이드
커뮤니티
관찰 가능한 Flutter 에이전트형 시리즈
59화에서 Craig Labenz와 Andrew Brogden이 이 Codelab을 살펴보고 앱 빌드의 흥미로운 부분을 강조합니다.
60화에서는 Craig와 Andrew가 다시 등장하여 새로운 기능으로 Codelab 앱을 확장하고 LLM이 지시대로 작동하도록 노력합니다.
61화에서는 크레이그가 크리스 셀스와 함께 뉴스 헤드라인을 분석하고 이에 상응하는 이미지를 생성합니다.
의견
이 Codelab 사용 경험에 관한 의견을 들려주세요. 다음 채널을 통해 의견을 제공해 주세요.
이 Codelab을 완료해 주셔서 감사합니다. Flutter와 AI의 교차점에서 흥미로운 가능성을 계속 탐색해 보세요.