大家好,歡迎來到第二十一天!在 Day 20,我們建立了完整的效能監控系統。今天,我們要為 Crew Up! 加入 AI 功能,讓應用程式能夠理解聊天內容、分析氣氛,並提供建議。
從專案開發的經驗來看,社群活動 App 最大的挑戰之一就是維持聊天室的活躍度。有時候群組太安靜,有時候不知道該聊什麼話題。我們想到:能不能用 AI 來幫忙炒熱氣氛呢?今天就來分享我們整合 Google Gemini API 的實際過程。
在開始實作前,我們面臨一個重要的選擇:使用 Gemini Developer API 還是 Vertex AI?
Gemini Developer API 的優勢
Vertex AI 的特點
我們的選擇:Gemini Developer API
對於 Crew Up! 這樣的專案來說,Gemini Developer API 是更好的選擇:
💡 重要提示:Google 提供了統一的 SDK,讓你可以先用 Gemini API 開發,未來需要時只需要改幾行初始化程式碼就能切換到 Vertex AI,不需要重寫整個應用。
多模態理解能力
適合 Crew Up! 的應用場景
在開始之前,必須先釐清一個容易混淆的概念:Gemini API 有兩種不同的存取方式。
特點:
generativelanguage.googleapis.com
API 端點:
https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash-latest:generateContent
特點:
us-central1-aiplatform.googleapis.com
API 端點:
https://us-central1-aiplatform.googleapis.com/v1/projects/YOUR_PROJECT/locations/us-central1/publishers/google/models/gemini-1.5-flash:generateContent
為什麼?
⚠️ 重要提醒:如果你看到教學文章要求設定 Google Cloud Platform、啟用 billing、建立 Service Account,那是在講 Vertex AI,不是我們這篇文章要介紹的 Gemini Developer API。兩者雖然都能呼叫 Gemini 模型,但設定方式和使用場景完全不同!
我們使用 Gemini Developer API,所以只需要從 Google AI Studio 取得 API Key,不需要設定 Google Cloud Platform。過程非常簡單:
步驟 1:前往 Google AI Studio
開啟瀏覽器,前往 Google AI Studio
步驟 2:登入 Google 帳號
使用你的 Google 帳號登入。如果還沒有帳號,可以免費註冊一個。
步驟 3:獲取 API Key
步驟 4:複製 API Key
💡 提示:API Key 格式通常是
AIzaSy
開頭,後面接著一串英文和數字
步驟 5:儲存 API Key
將 API Key 儲存在安全的地方。我們建議:
📝 Google AI Studio vs Google Cloud Console:
Google AI Studio 建立 API Key 時,背後也會自動建立一個 Google Cloud 專案。但你不需要自己去 Google Cloud Console 設定任何東西。這是 Gemini Developer API 的優點 - 簡化了繁瑣的 GCP 設定流程。
獲取 API Key 後,我們可以用簡單的測試來驗證是否正常:
# 使用 curl 測試 API(Mac/Linux)
curl \
-H 'Content-Type: application/json' \
-d '{"contents":[{"parts":[{"text":"Say hello in Traditional Chinese"}]}]}' \
-X POST 'https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash-latest:generateContent?key=YOUR_API_KEY'
這個指令做了什麼?
這個 curl
指令會向 Gemini API 發送一個 POST 請求,要求它用繁體中文說「你好」。讓我們拆解一下:
-H 'Content-Type: application/json'
:告訴 API 我們發送的是 JSON 格式-d '{"contents":...}'
:請求內容,包含我們要 AI 處理的提示詞-X POST
:使用 POST 方法key=YOUR_API_KEY
:在 URL 中帶入您的 API Key(記得替換成真實的 Key)成功的回應範例:
{
"candidates": [
{
"content": {
"parts": [
{
"text": "你好!"
}
],
"role": "model"
},
"finishReason": "STOP"
}
]
}
✅ 如果您看到類似上面的 JSON 回應,且包含繁體中文的「你好」或其他問候語,恭喜!您的 API Key 設定成功了。
失敗的回應範例:
{
"error": {
"code": 400,
"message": "API key not valid. Please pass a valid API key.",
"status": "INVALID_ARGUMENT"
}
}
❌ 如果看到類似的錯誤訊息,通常代表:
⚠️ 安全提醒:
- 絕對不要將 API Key 硬編碼在程式碼中
- 絕對不要將 API Key 提交到 Git 儲存庫
- 絕對不要在公開的地方分享 API Key
- 如果不小心洩漏了 API Key,立即到 Google Cloud Console 刪除並重新建立
Gemini API 提供慷慨的免費配額:
免費層(Free Tier)
對於 Crew Up! 這樣的專案來說,免費配額在開發和測試階段已經相當充足。如果未來需要更多配額,可以升級到付費方案。
成本估算
付費方案的定價(2025年1月參考):
一則聊天室氣氛分析大約使用 500-1000 Token,成本不到 $0.001(約台幣 0.03 元)。
# pubspec.yaml
dependencies:
google_generative_ai: ^0.4.7 # Google Gemini API SDK
flutter_riverpod: ^2.6.1 # 狀態管理
riverpod_annotation: ^2.6.1 # Riverpod 程式碼生成
安裝套件:
flutter pub get
為了安全起見,我們使用 --dart-define
傳遞 API Key。這個方法可以讓 API Key 在編譯時被傳入,而不需要硬編碼在程式碼中。
# 開發環境執行
flutter run --dart-define=GEMINI_API_KEY=your_api_key_here
# 建置 APK
flutter build apk --dart-define=GEMINI_API_KEY=your_api_key_here
在專案根目錄建立或編輯 .vscode/launch.json
:
{
"version": "0.2.0",
"configurations": [
{
"name": "crew_up (development)",
"request": "launch",
"type": "dart",
"args": [
"--dart-define=GEMINI_API_KEY=your_api_key_here"
]
},
{
"name": "crew_up (profile mode)",
"request": "launch",
"type": "dart",
"flutterMode": "profile",
"args": [
"--dart-define=GEMINI_API_KEY=your_api_key_here"
]
}
]
}
設定完成後,在 VS Code 的 Run and Debug 面板中選擇 "crew_up (development)",就可以一鍵啟動並自動帶入 API Key。
--dart-define=GEMINI_API_KEY=your_api_key_here
之後每次點擊 Run 按鈕,都會自動帶入這個參數。
.env
檔案(進階)對於團隊開發,我們可以結合 .env
檔案和建置腳本:
步驟 1:建立 .env
檔案
在專案根目錄建立 .env
檔案:
GEMINI_API_KEY=your_api_key_here
步驟 2:將 .env
加入 .gitignore
echo ".env" >> .gitignore
步驟 3:建立 .env.example
作為範本
# .env.example
GEMINI_API_KEY=your_api_key_here_replace_this
這個檔案可以提交到 Git,讓團隊成員知道需要哪些環境變數。
步驟 4:使用建置腳本
建立 scripts/run_with_gemini.sh
(已在專案中):
#!/bin/bash
set -a
source .env
set +a
flutter run --dart-define=GEMINI_API_KEY=$GEMINI_API_KEY
執行:
chmod +x scripts/run_with_gemini.sh
./scripts/run_with_gemini.sh
💡 最佳實踐:
- 開發環境:使用 VS Code 或 Android Studio 的 Run Configuration
- CI/CD:使用環境變數或密鑰管理服務
- 生產環境:改用 Firebase Remote Config 或後端代理(詳見「安全性考量」章節)
在開始整合之前,我們需要了解一些控制 AI 行為的重要參數。這些參數就像是 AI 的「個性調整器」,會大幅影響回應的風格和品質。
/// Gemini AI 服務配置
class GeminiConfig {
/// 預設模型
static const String defaultModel = 'gemini-1.5-flash-latest';
/// Temperature(控制回應的隨機性)
static const double defaultTemperature = 0.7;
/// 最大輸出 Token 數
static const int maxOutputTokens = 1024;
/// Top-K 參數(控制候選詞數量)
static const int topK = 40;
/// Top-P 參數(累積機率閾值)
static const double topP = 0.95;
const GeminiConfig();
}
這個參數做什麼?
想像 AI 在選擇下一個詞時,面前有很多選項。Temperature 決定了 AI 會不會「冒險」選擇不太可能的詞。
範圍:0.0 ~ 2.0(實務上常用 0.0 ~ 1.0)
不同溫度的效果:
Temperature = 0.0(冰冷、確定)
提示:「用一句話描述台灣」
回應:「台灣是位於東亞的島國。」(每次都一樣)
Temperature = 0.7(溫暖、平衡) ⭐ 我們的選擇
提示:「用一句話描述台灣」
回應可能:
- 「台灣是個充滿活力的美麗島嶼。」
- 「台灣以美食和友善的人民聞名。」
- 「台灣擁有豐富的文化和自然景觀。」
Temperature = 1.0+(火熱、創意)
提示:「用一句話描述台灣」
回應可能:
- 「台灣!夜市、高山、海浪,一切都在這座心形島嶼跳動!」
- 「福爾摩沙,東方的珍珠,科技與傳統交織的奇蹟之地。」
為什麼我們選 0.7?
對於聊天室氛圍分析,我們需要:
這個參數做什麼?
限制 AI 一次最多可以生成多少內容。
1 Token ≈ 0.75 個英文單詞
1 Token ≈ 0.5 ~ 1 個中文字
1024 Tokens ≈ 500-800 個中文字
為什麼設 1024?
範例:
// maxOutputTokens = 100(太短)
「氣氛分數 75 分,情緒正面,建議...」(可能被截斷)
// maxOutputTokens = 1024(剛好)
完整的 JSON 格式氣氛分析,包含所有欄位
// maxOutputTokens = 4096(太長)
可能產生冗長的廢話,浪費成本
這兩個參數更進階,控制 AI 如何從候選詞中挑選。
Top-K = 40:只考慮前 40 個最可能的詞
假設 AI 要選下一個詞,所有候選詞的機率:
「台灣」(30%), 「島嶼」(25%), 「美麗」(15%), 「友善」(10%), ...
Top-K = 40 → 只從機率最高的前 40 個詞中選擇
Top-P = 0.95:累積機率達到 95% 就停止
候選詞按機率排序:
「台灣」(30%) → 累計 30%
「島嶼」(25%) → 累計 55%
「美麗」(15%) → 累計 70%
「友善」(10%) → 累計 80%
「文化」(8%) → 累計 88%
「科技」(7%) → 累計 95% ✓ 到這裡就停止
只從這 6 個詞中選擇,忽略剩下機率很低的詞
Top-K 和 Top-P 的協同作用:
這兩個參數通常一起使用,提供雙重過濾:
class GeminiException implements Exception {
final String message;
final String? code;
final dynamic originalError;
const GeminiException(this.message, {this.code, this.originalError});
}
為什麼需要自訂異常?
當 AI API 出錯時,我們需要知道:
API_KEY_INVALID
、QUOTA_EXCEEDED
)範例:
try {
await geminiService.ping();
} on GeminiException catch (e) {
if (e.code == 'QUOTA_EXCEEDED') {
// 顯示「今日配額已用完,請明天再試」
} else if (e.code == 'API_KEY_INVALID') {
// 顯示「API Key 設定錯誤」
}
}
參數 | Crew Up! 的設定 | 原因 |
---|---|---|
Temperature | 0.7 | 平衡穩定性和靈活性,適合聊天分析 |
maxOutputTokens | 1024 | 足夠完整分析,不會太浪費 |
Top-K | 40 | Google 建議的預設值,適合大多數場景 |
Top-P | 0.95 | 保持多樣性但避免太離譜的回應 |
💡 進階提示:這些參數不是固定的!在 Day 22,我們會根據不同功能(氣氛分析 vs 鼓勵訊息)調整這些參數。
現在我們已經了解 AI 參數的意義,接下來要建立一個輕量級的 GeminiService
來驗證整合是否成功。
在 Day 21,我們不會馬上實作複雜的聊天室氣氛分析功能。相反地,我們先建立一個簡單的服務來確保環境設定正確。
// lib/app/core/services/gemini_service.dart
import 'dart:developer' as developer;
import 'package:google_generative_ai/google_generative_ai.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'gemini_service.g.dart';
/// Gemini AI 服務異常
class GeminiException implements Exception {
final String message;
final String? code;
final dynamic originalError;
const GeminiException(this.message, {this.code, this.originalError});
@override
String toString() =>
'GeminiException: $message${code != null ? ' (code: $code)' : ''}';
}
/// 輕量級 Gemini AI 服務(驗證版本)
///
/// 這個版本只用來驗證 API Key 是否正確設定
/// 完整的功能實作將在 Day 22 介紹
class GeminiService {
final GenerativeModel _model;
final String _apiKey;
GeminiService({required String apiKey})
: _apiKey = apiKey,
_model = GenerativeModel(
model: 'gemini-1.5-flash-latest',
apiKey: apiKey,
);
/// 初始化並驗證 Gemini AI 服務
static Future<GeminiService?> initialize() async {
try {
// 從環境變數讀取 API Key
const apiKey = String.fromEnvironment('GEMINI_API_KEY', defaultValue: '');
if (apiKey.isEmpty) {
developer.log(
'⚠️ Gemini API Key not found. Please set GEMINI_API_KEY.',
name: 'GeminiService',
);
return null;
}
developer.log(
'🔄 Initializing Gemini AI Service...',
name: 'GeminiService',
);
final service = GeminiService(apiKey: apiKey);
// 發送一個簡單的 ping 請求來驗證連線
await service.ping();
developer.log(
'✅ Gemini AI Service initialized and validated successfully',
name: 'GeminiService',
);
return service;
} on Exception catch (e) {
developer.log(
'❌ Gemini AI Service initialization failed: $e',
name: 'GeminiService',
error: e,
level: 1000,
);
return null;
}
}
/// 發送簡單的 ping 請求來驗證 API 連線
Future<void> ping() async {
try {
final content = [Content.text('Say hello in Traditional Chinese')];
final response = await _model.generateContent(content);
if (response.text == null || response.text!.isEmpty) {
throw const GeminiException(
'Empty response from Gemini API',
code: 'EMPTY_RESPONSE',
);
}
developer.log(
'✅ Ping successful. Response: ${response.text}',
name: 'GeminiService',
);
} on Exception catch (e) {
developer.log(
'❌ Ping failed: $e',
name: 'GeminiService',
error: e,
);
throw GeminiException(
'Failed to ping Gemini API',
code: 'PING_FAILED',
originalError: e,
);
}
}
}
/// Gemini Service Provider
@riverpod
Future<GeminiService?> geminiService(GeminiServiceRef ref) async =>
await GeminiService.initialize();
GenerativeModel
null
而不是崩潰將 Gemini Service 加入到 App 的初始化流程中:
// lib/app/core/services/firebase_initializer.dart
import 'dart:developer' as developer;
import 'package:crew_up/app/core/services/crashlytics_service.dart';
import 'package:crew_up/app/core/services/gemini_service.dart';
import 'package:crew_up/app/core/services/performance_service.dart';
import 'package:crew_up/firebase_options.dart';
import 'package:firebase_core/firebase_core.dart';
class FirebaseInitializer {
FirebaseInitializer._();
/// 初始化所有 Firebase 和核心服務
///
/// 返回 true 表示初始化完成(不論個別服務成功或失敗)
/// 返回 false 表示核心初始化流程失敗
static Future<bool> initialize() async {
try {
developer.log('🚀 Initializing Firebase services...', name: 'Firebase');
// 初始化 Firebase
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
// 初始化其他服務
await CrashlyticsService.initialize();
await PerformanceService.initialize();
// 🆕 初始化 Gemini AI Service 並取得實例
final gemini = await GeminiService.initialize();
if (gemini == null) {
developer.log(
'⚠️ Gemini Service initialization returned null. AI features will be disabled.',
name: 'Firebase',
);
}
developer.log('✅ All services initialized', name: 'Firebase');
return true; // 代表初始化流程完成
} catch (e, stackTrace) {
developer.log(
'❌ Core services initialization failed',
name: 'Firebase',
error: e,
stackTrace: stackTrace,
);
return false; // 初始化失敗
}
}
}
最後,我們需要在 main.dart
中等待 FirebaseInitializer.initialize()
完成:
// lib/main.dart
import 'package:crew_up/app/core/services/firebase_initializer.dart';
import 'package:crew_up/app/crew_up_app.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// 執行並等待所有核心服務初始化
await FirebaseInitializer.initialize();
runApp(
const ProviderScope(
child: CrewUpApp(),
),
);
}
關鍵改動:
main
函數現在是 async
await
等待 FirebaseInitializer.initialize()
完成這個模式與 Day 18 (Crashlytics) 和 Day 20 (Performance) 保持一致。
現在執行您的 App:
flutter run --dart-define=GEMINI_API_KEY=your_api_key_here
成功的控制台輸出:
🚀 Initializing Firebase services...
🔄 Initializing Gemini AI Service...
✅ Ping successful. Response: 你好!
✅ Gemini AI Service initialized and validated successfully
✅ All services initialized
✅ 如果您看到這樣的輸出,恭喜!Day 21 的環境設定任務完成了。
失敗的控制台輸出:
🚀 Initializing Firebase services...
⚠️ Gemini API Key not found. Please set GEMINI_API_KEY.
或
🚀 Initializing Firebase services...
🔄 Initializing Gemini AI Service...
❌ Ping failed: GeminiException: API key not valid
❌ Gemini AI Service initialization failed
❌ 如果看到錯誤,請檢查:
--dart-define
參數是否正確傳入今天我們完成了 Google Gemini API 的環境設定:
✅ 已完成的任務
curl
驗證 API Key 是否有效--dart-define
傳遞環境變數google_generative_ai
套件GeminiService
並成功驗證連線🎓 關鍵學習
從今天的設定過程中,我們學到:
curl
先確認 API 可用,再寫程式🔗 與前幾天的關聯
Day 21 我們只是完成了環境設定和基礎驗證。在 Day 22,我們將深入實作:
即將實作的功能
期待在 Day 22 與您分享 AI 實作!