Core Concepts:Flow、Tool、Prompt の実装
Flow の設計パターン
Flow は AI 処理の最小単位です。簡単な例から複雑な例まで段階的に解説します。
シンプルな Text-to-Text Flow:
import { defineFlow, runFlow } from 'firebase-genkit';
import { gemini15Pro } from '@genkit-ai/gemini';
export const summarizeArticle = defineFlow(
{
name: 'summarizeArticle',
inputSchema: z.object({
text: z.string(),
maxLength: z.number().default(200)
}),
outputSchema: z.string()
},
async (input) => {
const result = await ai.generate({
model: gemini15Pro,
prompt: `以下のテキストを${input.maxLength}文字以内で要約してください:\n\n${input.text}`,
config: {
temperature: 0.3,
maxOutputTokens: 500
}
});
return result.text();
}
);
// 実行
const summary = await runFlow(summarizeArticle, {
text: '長いテキスト...',
maxLength: 200
});
ストリーミング対応 Flow:
export const streamingSummarize = defineFlow(
{
name: 'streamingSummarize',
inputSchema: z.object({ text: z.string() }),
outputSchema: z.string(),
streamSchema: z.string() // ストリーミング出力型
},
async (input, streamCallback) => {
const stream = await ai.generate({
model: gemini15Pro,
prompt: `要約してください: ${input.text}`,
streaming: true
});
let fullResponse = '';
for await (const chunk of stream) {
fullResponse += chunk.text();
streamCallback(chunk.text()); // クライアントへリアルタイム配信
}
return fullResponse;
}
);
Tool の定義と Gemini による自動呼び出し
Tool は JSON Schema で定義され、Gemini が自動的に選択・実行します。
import { defineTool } from 'firebase-genkit';
const getWeather = defineTool(
{
name: 'getWeather',
description: '指定された都市の天気を取得',
inputSchema: z.object({
city: z.string().describe('都市名')
}),
outputSchema: z.object({
temperature: z.number(),
condition: z.string()
})
},
async (input) => {
// 実装: 外部 API を呼び出し
const response = await fetch(`https://weather-api.example.com?city=${input.city}`);
return response.json();
}
);
const getFlightInfo = defineTool(
{
name: 'getFlightInfo',
description: '飛行機の運航情報を取得',
inputSchema: z.object({
departure: z.string(),
arrival: z.string(),
date: z.string().describe('YYYY-MM-DD 形式')
}),
outputSchema: z.object({
flights: z.array(z.object({
flightNumber: z.string(),
departure: z.string(),
arrival: z.string()
}))
})
},
async (input) => {
const response = await fetch(
`https://flight-api.example.com/search?from=${input.departure}&to=${input.arrival}&date=${input.date}`
);
return response.json();
}
);
Tool を Flow に統合:
export const tripPlannerAgent = defineFlow(
{
name: 'tripPlanner',
inputSchema: z.object({
destination: z.string(),
departureDate: z.string()
}),
outputSchema: z.string()
},
async (input) => {
const result = await ai.generate({
model: gemini15Pro,
tools: [getWeather, getFlightInfo],
prompt: `${input.destination} への旅行プランを立ててください。出発日は ${input.departureDate} です。天気と飛行機の情報を参考にしてください。`,
maxIterations: 5 // Tool 呼び出しの最大回数
});
return result.text();
}
);
RAG 実装:Embedder と Retriever の統合
検索拡張生成(RAG)は、大規模なナレッジベースから関連ドキュメントを取得し、LLM の入力に含める手法です。Genkit でこれを実装します。
Embedder の選択と設定
import { defineEmbedder } from 'firebase-genkit';
import { embedding001 } from '@genkit-ai/gemini';
// Gemini 組み込みの Embedder を使用
const embedder = embedding001;
// カスタム Embedder の定義も可能
const customEmbedder = defineEmbedder(
{
name: 'customEmbedder',
configSchema: z.object({ model: z.string() })
},
async (input, config) => {
const response = await fetch('https://embedding-api.example.com', {
method: 'POST',
body: JSON.stringify({ texts: input, model: config.model })
});
const data = await response.json();
return data.embeddings;
}
);
Vector Store の統合
Genkit は複数の Vector Store をサポート。ここでは Pinecone を例に:
import { pineconeRetrieverRef } from '@genkit-ai/pinecone';
import { Pinecone } from '@pinecone-database/pinecone';
const pc = new Pinecone({
apiKey: process.env.PINECONE_API_KEY
});
const index = pc.index(process.env.PINECONE_INDEX);
const retriever = defineRetriever(
{
name: 'documentRetriever',
configSchema: z.object({ topK: z.number().default(5) })
},
async (input, config) => {
// テキストをベクトル化
const embedding = await embedder.embed(input);
// Pinecone で類似度検索
const results = await index.query({
vector: embedding,
topK: config.topK,
includeMetadata: true
});
// 検索結果をドキュメント形式で返す
return results.matches.map(match => ({
content: match.metadata.text,
metadata: match.metadata,
score: match.score
}));
}
);
RAG Flow の実装
export const ragQA = defineFlow(
{
name: 'ragQA',
inputSchema: z.object({
question: z.string()
}),
outputSchema: z.string()
},
async (input) => {
// Step 1: 関連ドキュメントを検索
const documents = await retriever.retrieve(input.question, { topK: 3 });
// Step 2: コンテキストを構築
const context = documents
.map(doc => doc.content)
.join('\n---\n');
// Step 3: Gemini で生成
const result = await ai.generate({
model: gemini15Pro,
prompt: `以下のコンテキストを参考に、質問に答えてください。
コンテキスト:
${context}
質問: ${input.question}`,
config: {
temperature: 0.5,
maxOutputTokens: 1000
}
});
return result.text();
}
);
Agentic パターン:エージェントの段階的な進化
AI エージェントの複雑性は、順序立てた以下のパターンで進化します。
パターン 1:Sequential(順序固定)
複数の Flow を決まった順序で実行。エージェントの考慮度が最も低いものの、予測可能性が高いです。
export const sequentialWorkflow = defineFlow(
{
name: 'sequentialWorkflow',
inputSchema: z.object({ topic: z.string() }),
outputSchema: z.string()
},
async (input) => {
// Step 1: リサーチ
const research = await runFlow(researchTopic, { topic: input.topic });
// Step 2: 構成化
const structure = await runFlow(outlineContent, { research });
// Step 3: 執筆
const article = await runFlow(writeArticle, { outline: structure });
return article;
}
);
パターン 2:Tool Calling(自動選択)
Tool calling により、Gemini が自動的に必要な Tool を選択します。
export const autonomousAgent = defineFlow(
{
name: 'autonomousAgent',
inputSchema: z.object({ request: z.string() }),
outputSchema: z.string()
},
async (input) => {
const result = await ai.generate({
model: gemini15Pro,
tools: [
getWeather,
getFlightInfo,
getHotelAvailability,
makeReservation
],
prompt: `ユーザーのリクエスト: ${input.request}\n\n必要な Tool を呼び出して対応してください。`,
maxIterations: 10,
config: {
temperature: 0.7
}
});
return result.text();
}
);
パターン 3:Iterative Refinement(反復改善)
LLM の出力を評価し、品質が不足していれば改善指示を与えて再実行。
export const refinementAgent = defineFlow(
{
name: 'refinementAgent',
inputSchema: z.object({ topic: z.string() }),
outputSchema: z.string()
},
async (input) => {
let content = '';
let quality = 0;
let iterations = 0;
const maxIterations = 3;
while (quality < 0.8 && iterations < maxIterations) {
// コンテンツ生成
content = await ai.generate({
model: gemini15Pro,
prompt: `${input.topic} について詳しく説明してください。`
}).then(r => r.text());
// 品質評価
const evaluation = await ai.generate({
model: gemini15Pro,
prompt: `以下のテキストを 0 から 1 の品質スコアで評価してください: ${content}`,
responseSchema: z.object({ score: z.number().min(0).max(1) })
});
quality = evaluation.score;
if (quality < 0.8) {
const feedback = await ai.generate({
model: gemini15Pro,
prompt: `品質スコア ${quality} の以下のテキストを改善してください: ${content}`
}).then(r => r.text());
content = feedback;
}
iterations++;
}
return content;
}
);
パターン 4:Autonomous(自律的実行)
Genkit の Agentic Flow では、LLM が複数回にわたって思考し、Tool を呼び出し、結果を評価して次のアクションを決定します。
export const fullyAutonomousAgent = defineFlow(
{
name: 'fullyAutonomousAgent',
inputSchema: z.object({
goal: z.string(),
maxSteps: z.number().default(10)
}),
outputSchema: z.string()
},
async (input) => {
const conversationHistory = [];
for (let step = 0; step < input.maxSteps; step++) {
// 現在のゴール状況を LLM に評価させる
const nextAction = await ai.generate({
model: gemini15Pro,
tools: [
searchDatabase,
callAPI,
executeCode,
delegateSubgoal
],
prompt: `
目標: ${input.goal}
これまでの進捗:
${conversationHistory.map(h => `- ${h}`).join('\n')}
次に何をすべきか判断してください。必要に応じて Tool を呼び出してください。
ゴール達成時は "GOAL_ACHIEVED" を出力してください。
`,
maxIterations: 1
});
conversationHistory.push(nextAction.text());
if (nextAction.text().includes('GOAL_ACHIEVED')) {
break;
}
}
// 最終結果をまとめる
const finalSummary = await ai.generate({
model: gemini15Pro,
prompt: `以下のプロセスの結果をまとめてください:\n${conversationHistory.join('\n')}`
}).then(r => r.text());
return finalSummary;
}
);
MCP(Model Context Protocol)との統合
MCP は、LLM がローカルツール(ファイルシステム、データベース、IDE など)にアクセスするための標準プロトコルです。Genkit は MCP サーバーを Tool として統合できます。
import { defineModel } from '@genkit-ai/mcp';
const mcpTool = defineModel(
{
name: 'mcpFilesystem',
description: 'ファイルシステムへのアクセス(MCP 経由)'
},
async (input) => {
const mcpClient = new MCPClient({
host: 'localhost',
port: 3000
});
const response = await mcpClient.call(input.toolName, input.params);
return response;
}
);
// Flow に統合
export const codeAnalysisAgent = defineFlow(
{
name: 'codeAnalysis',
inputSchema: z.object({ filePath: z.string() }),
outputSchema: z.string()
},
async (input) => {
const result = await ai.generate({
model: gemini15Pro,
tools: [mcpTool],
prompt: `ファイル ${input.filePath} のコードを分析してください。`
});
return result.text();
}
);
デプロイ:Cloud Functions と Cloud Run
Cloud Functions へのデプロイ
Cloud Functions は小規模で短時間の Flow に最適です。
// functions/src/index.ts
import * as functions from 'firebase-functions';
import { runFlow } from 'firebase-genkit';
import { summarizeArticle } from './flows';
export const summarize = functions.https.onCall(async (data, context) => {
// 認証チェック
if (!context.auth) {
throw new functions.https.HttpsError(
'unauthenticated',
'Authentication required'
);
}
try {
const result = await runFlow(summarizeArticle, {
text: data.text,
maxLength: data.maxLength || 200
});
return { success: true, summary: result };
} catch (error) {
throw new functions.https.HttpsError('internal', error.message);
}
});
デプロイコマンド:
firebase deploy --only functions:summarize
Cold Start 対策:
// 事前ウォームアップエンドポイント
export const warmUp = functions.pubsub
.schedule('every 5 minutes')
.onRun(async () => {
await ai.generate({
model: gemini15Pro,
prompt: 'Warm-up request'
});
});
Cloud Run へのデプロイ
複雑な Flow やストリーミング対応が必要な場合は Cloud Run を使用します。
// Dockerfile
FROM node:18-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci --only production
COPY . .
EXPOSE 8080
CMD ["node", "server.js"]
// server.ts - Express サーバー
import express from 'express';
import { runFlow } from 'firebase-genkit';
import { tripPlannerAgent } from './flows';
const app = express();
app.use(express.json());
// ストリーミング対応
app.post('/plan-trip', async (req, res) => {
const { destination, departureDate } = req.body;
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
try {
const stream = await runFlow(
tripPlannerAgent,
{ destination, departureDate },
{ streamCallback: (chunk) => {
res.write(`data: ${JSON.stringify({ chunk })}\n\n`);
}}
);
res.write(`data: ${JSON.stringify({ result: stream })}\n\n`);
res.end();
} catch (error) {
res.write(`data: ${JSON.stringify({ error: error.message })}\n\n`);
res.end();
}
});
const PORT = process.env.PORT || 8080;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
デプロイコマンド:
gcloud run deploy genkit-agent \
--source . \
--platform managed \
--region us-central1 \
--set-env-vars GEMINI_API_KEY=YOUR_GEMINI_API_KEY
セキュリティ:認証・認可・秘密情報管理
認可ポリシーの設定
import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';
export const protectedFlow = functions.https.onCall(async (data, context) => {
// ユーザー認証の確認
if (!context.auth) {
throw new functions.https.HttpsError('unauthenticated', 'User must be logged in');
}
// Firestore でユーザーロールを確認
const userDoc = await admin.firestore()
.collection('users')
.doc(context.auth.uid)
.get();
if (!userDoc.data()?.isPremium) {
throw new functions.https.HttpsError(
'permission-denied',
'Premium subscription required'
);
}
// Flow 実行
return await runFlow(expensiveFlow, data);
});
Secret Manager の統合
import { SecretManagerServiceClient } from '@google-cloud/secret-manager';
const client = new SecretManagerServiceClient();
async function getSecret(secretId: string): Promise<string> {
const projectId = process.env.GCP_PROJECT_ID;
const name = `projects/${projectId}/secrets/${secretId}/versions/latest`;
const [version] = await client.accessSecretVersion({ name });
return version.payload.data.toString('utf8');
}
// Flow で使用
const apiKey = await getSecret('gemini-api-key');
const ai = genkit({
plugins: [googleAI({ apiKey })]
});
App Check による API 保護
import * as appCheck from 'firebase-functions/v2/https';
export const protectedAgent = appCheck.onCall(
{ region: 'us-central1' },
async (request) => {
// App Check トークンは自動検証済み
return await runFlow(secureFlow, request.data);
}
);
パフォーマンス最適化
第 2 世代 Cloud Functions の並行実行
# firebase.json
{
"functions": {
"runtime": "nodejs18",
"concurrency": 100 // デフォルト 100 から増加
}
}
export const parallelAgent = functions.https.onCall(async (data) => {
// 複数の Flow を並行実行
const [result1, result2, result3] = await Promise.all([
runFlow(flowA, data),
runFlow(flowB, data),
runFlow(flowC, data)
]);
return { result1, result2, result3 };
});
Flow のグループ化
// 同じメモリ設定での複数 Flow をグループ化
export const aiOperations = functions
.runWith({
memory: '2GB',
timeoutSeconds: 300
})
.https.onCall(async (data, context) => {
const flowName = data.flowName;
switch (flowName) {
case 'summarize':
return await runFlow(summarizeArticle, data.params);
case 'translate':
return await runFlow(translateContent, data.params);
case 'generate':
return await runFlow(generateContent, data.params);
default:
throw new Error('Unknown flow');
}
});
キャッシング戦略
import * as admin from 'firebase-admin';
const cache = admin.firestore();
export const cachedRAGFlow = defineFlow(
{
name: 'cachedRAGFlow',
inputSchema: z.object({ question: z.string() })
},
async (input) => {
const cacheKey = Buffer.from(input.question).toString('base64');
// キャッシュをチェック
const cached = await cache.collection('rag_cache').doc(cacheKey).get();
if (cached.exists) {
return cached.data().answer;
}
// キャッシュミス時は実行
const result = await runFlow(ragQA, input);
// 結果をキャッシュ
await cache.collection('rag_cache').doc(cacheKey).set({
answer: result,
timestamp: admin.firestore.FieldValue.serverTimestamp(),
ttl: 86400 // 24 時間
});
return result;
}
);
本番監視と可観測性
Genkit のトレーシング機能
Genkit はすべての API 呼び出しを自動的に記録します。
import { getLogger } from 'firebase-functions/logger';
export const monitoredFlow = defineFlow(
{
name: 'monitoredFlow',
inputSchema: z.object({ text: z.string() })
},
async (input, context) => {
const logger = getLogger();
logger.info('Flow started', { input });
const result = await ai.generate({
model: gemini15Pro,
prompt: input.text,
config: {
temperature: 0.5
}
});
logger.info('Flow completed', {
tokens: result.usage,
executionTime: context.executionTime
});
return result.text();
}
);
Cloud Logging への統合
import * as logging from '@google-cloud/logging';
const log = logging.log('genkit-operations');
export const loggingFlow = defineFlow(
{
name: 'loggingFlow',
inputSchema: z.object({ data: z.any() })
},
async (input) => {
const metadata = {
severity: 'INFO',
sourceLocation: {
file: 'flows.ts',
line: 123
}
};
log.write(log.entry(metadata, { input }));
const result = await runFlow(ragQA, input);
log.write(log.entry(metadata, { result }));
return result;
}
);
コスト最適化戦略
モデル選択の指標
| 用途 | 推奨モデル | 理由 |
| 要約・分類 | Gemini 2.5 Flash | 高速で低コスト |
| コンテンツ生成 | Gemini 2.5 Pro | 品質と速度のバランス |
| 複雑な推論 | Gemini 2.5 Pro | 深い思考が必要 |
export const costOptimizedFlow = defineFlow(
{
name: 'costOptimized',
inputSchema: z.object({
task: z.enum(['summarize', 'generate', 'reason']),
text: z.string()
})
},
async (input) => {
const model = input.task === 'summarize'
? gemini15Flash
: input.task === 'generate'
? gemini15Pro
: gemini15Pro;
return await ai.generate({
model,
prompt: input.text
}).then(r => r.text());
}
);
バッチ処理による API 呼び出しの削減
export const batchFlow = defineFlow(
{
name: 'batchFlow',
inputSchema: z.object({
items: z.array(z.string())
})
},
async (input) => {
// 複数アイテムを 1 つのプロンプトで処理
const batchPrompt = `以下の ${input.items.length} 個のテキストをまとめて処理してください:\n${input.items.map((item, i) => `${i + 1}. ${item}`).join('\n')}`;
return await ai.generate({
model: gemini15Pro,
prompt: batchPrompt
}).then(r => r.text());
}
);
実践例:AI カスタマーサポートシステムの構築
顧客からの問い合わせに自動対応するシステムを実装します。
// カスタマーサポート エージェント
const fetchTicket = defineTool(
{
name: 'fetchTicket',
description: 'チケット ID からサポートチケット情報を取得',
inputSchema: z.object({ ticketId: z.string() }),
outputSchema: z.object({
id: z.string(),
customer: z.string(),
issue: z.string(),
status: z.string()
})
},
async (input) => {
const ticket = await fetch(
`https://support-api.example.com/tickets/${input.ticketId}`
).then(r => r.json());
return ticket;
}
);
const updateTicket = defineTool(
{
name: 'updateTicket',
description: 'チケットステータスと応答メッセージを更新',
inputSchema: z.object({
ticketId: z.string(),
status: z.string(),
response: z.string()
}),
outputSchema: z.object({ success: z.boolean() })
},
async (input) => {
const response = await fetch(
`https://support-api.example.com/tickets/${input.ticketId}`,
{
method: 'PATCH',
body: JSON.stringify({
status: input.status,
agentResponse: input.response
})
}
);
return { success: response.ok };
}
);
export const customerSupportAgent = defineFlow(
{
name: 'customerSupport',
inputSchema: z.object({ ticketId: z.string() }),
outputSchema: z.string()
},
async (input) => {
// チケット情報を取得
const ticket = await runFlow(fetchTicketFlow, { ticketId: input.ticketId });
// AI が自動対応を生成
const response = await ai.generate({
model: gemini15Pro,
tools: [updateTicket],
prompt: `
顧客サポートチケット:
ID: ${ticket.id}
顧客: ${ticket.customer}
問題: ${ticket.issue}
現在のステータス: ${ticket.status}
適切な対応メッセージを生成し、チケットを更新してください。
解決可能な場合は "resolved" に、さらに情報が必要な場合は "awaiting-info" に設定してください。
`,
maxIterations: 3
});
return response.text();
}
);
公式ドキュメントには書かれていない、本番運用で気づいた 7 つのインサイト
ここからは、私自身が個人開発のバックエンドに Firebase Genkit を入れて、1 ヶ月ほど動かしてみて遭遇した「公式には書かれていないハマりどころ」を 7 つにまとめます。順番は重要度ではなく、踏みやすさの順です。
1. Cold Start は Gemini 初回呼び出し込みで 35〜45 秒に伸びる
Cloud Functions Gen2 + Genkit + Gemini 2.5 Pro の組み合わせで、Cold Start から最初のレスポンスまで実測 35〜45 秒かかりました。内訳はだいたい、コンテナ起動 8〜12 秒、Genkit 初期化(genkit({...}) の googleAI プラグイン解決)4〜6 秒、Gemini 2.5 Pro の最初の generate 呼び出し 20〜25 秒、です。
Pro を Flash に変えるとレスポンス部分が 8〜12 秒まで落ちますが、Cold Start 全体は 20〜28 秒残ります。私は AdMob から入る広告収益で運用しているアプリにつき、ユーザーから見える機能には Gemini を直接当てず、バックグラウンドのバッチ要約と、サーバー側の minInstances を 1 にした Cloud Run コンテナでの常時受け待ちに分けるようにしました。
# firebase.json — Cold Start 軽減のためメモリ増 + min-instances 維持
{
"functions": {
"runtime": "nodejs18",
"memory": "1GiB",
"concurrency": 80,
"minInstances": 1
}
}
minInstances: 1 を入れておくと、Cloud Functions Gen2 で時間あたり数十円ですが Cold Start のロングテールがほぼ消えました。広告クリック単価の高い時間帯にユーザーを待たせると AdMob の eCPM にも響くため、結果的に元は取れています。
2. concurrency のデフォルトが Gen2 でも 1 のままになりやすい
Cloud Functions Gen2 のドキュメントには concurrency 上限が 1000 と書かれていますが、firebase deploy でデフォルト設定のままだと、コンテナの concurrency が 1 のまま動いていることが何度かありました。Gemini を呼び出す関数は I/O 待ちが大半なので、concurrency=1 のままだと急に来た 10 件のリクエストで 10 個のコンテナが起動し、起動分のコストと Cold Start のロングテールを全部踏みます。
私は最低でも concurrency: 50、Flash 中心の関数は 80 を入れるようにしました。Pro を使う関数だけメモリ消費が大きいので、concurrency: 30 程度に下げてバランスを取っています。
3. runFlow の入れ子はトレースが「親 Flow にも子 Flow にも」二重計上される
Genkit のトレース機能は便利ですが、runFlow(parentFlow, ...) の中で runFlow(childFlow, ...) を呼ぶと、Cloud Logging のトレースエクスポートで親 Flow と子 Flow の両方にトークン使用量が重複して載ります。月末に集計したとき、Cloud Billing の Gemini 課金額より Genkit トレース集計のほうが 1.8 倍ほど多くなって面食らいました。
対処は単純で、コスト集計は「Cloud Billing の generativelanguage.googleapis.com だけを真と扱う」ことに割り切ること。Genkit トレースはあくまでデバッグ用と位置付け、ダッシュボードには出さないようにしました。
4. Secret Manager の accessSecretVersion 呼び出しに 1 リクエスト課金が乗る
accessSecretVersion は 10,000 リクエストあたり $0.03 と安いものの、Cloud Functions の Cold Start で毎回叩くと地味に積もります。Gemini API キーは関数のグローバルスコープで一度だけ取得し、その後はモジュール変数にキャッシュするのが正解でした。
let cachedApiKey: string | null = null;
async function getCachedSecret(name: string): Promise<string> {
if (cachedApiKey) return cachedApiKey;
const [version] = await secretManager.accessSecretVersion({ name });
cachedApiKey = version.payload.data.toString("utf8");
return cachedApiKey;
}
Cold Start の度に取り直されるため、Warm な間はキャッシュされ、Cold が来たら再取得されるという挙動になります。
5. Flow 名は文字列で完全一致:リネームすると Cloud Logging の絞り込みが死ぬ
defineFlow({ name: 'summarizeArticle', ... }) の name は Cloud Logging のラベルに直接出ます。私はリファクタで summarizeArticleV2 にリネームしたところ、それまでの監視ダッシュボードと Slack アラートが全部 V1 で絞り込まれていて、3 日ほど気づきませんでした。
リネームするときは、必ず旧名と新名の両方を含むワイルドカードクエリ(name=~"summarizeArticle.*")に書き換えてから移行することをお勧めします。
6. トレースに PII が混ざる:本番投入前に redactor を必ず噛ませる
Genkit の自動トレースは、Flow に渡された入力をそのまま Cloud Logging に書き出します。レビュー要約 Flow に流していたユーザー本文には、ごく稀にメールアドレスや電話番号が紛れ込んでいて、本番投入してから気づきました。
defineFlow の前段にレダクタを挟むのが安全です。私の場合はざっくり「メールアドレス・電話番号・クレジットカード番号らしき桁数の数列」を [REDACTED] に置換する簡易関数を Flow の入口に挟んでいます。
const REDACT_PATTERNS = [
/[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}/g,
/0\d{1,3}-?\d{2,4}-?\d{3,4}/g,
/\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}/g,
];
function redact(text: string): string {
return REDACT_PATTERNS.reduce(
(acc, pat) => acc.replace(pat, "[REDACTED]"),
text
);
}
ユーザーが投稿したテキストを Gemini に渡す系のアプリでは、これを入口で噛ませないと、Cloud Logging の保管期間 30 日の間ずっと PII が露出し続けることになります。
7. streamCallback は同期的に呼ばれる:重い処理を入れるとバックプレッシャーで詰まる
ストリーミング Flow の streamCallback は、Gemini から chunk が届くたびに同期的に呼ばれます。ここに DB 書き込みや別 API への通知を素朴に入れると、その処理時間ぶん Gemini のストリーム受信が止まり、全体のレスポンスが体感で 2〜3 倍遅くなりました。
streamCallback の中では、queue.push(chunk) のような軽い操作だけにとどめ、重い処理は別ワーカーや setImmediate で逃がすのが鉄則です。
const bufferQueue: string[] = [];
let flushTimer: NodeJS.Timeout | null = null;
const streamCallback = (chunk: string) => {
bufferQueue.push(chunk);
if (!flushTimer) {
flushTimer = setImmediate(() => {
const batch = bufferQueue.splice(0);
// ここで DB 書き込み等の重い処理
persistChunks(batch);
flushTimer = null;
});
}
};
個人開発アプリで Genkit を運用してみたコスト・レイテンシ実測値
ここからは、実際に 1 ヶ月動かしてみて取れた数値を共有します。対象は私の壁紙系アプリのうち 1 本に組み込んだ「ユーザーレビュー要約 Flow」で、月間およそ 12,000 件のレビューが入ってきます。
モデル別コスト・レイテンシ実測(2026 年 4 月実測)
| モデル | 1 リクエストあたり平均トークン | 平均レイテンシ | p95 レイテンシ | 1 リクエストあたりコスト | 月間総コスト(12,000件) |
| Gemini 2.5 Pro | 入力 1,800 / 出力 250 | 4.2 秒 | 11.8 秒 | $0.018 | 約 $216 |
| Gemini 2.5 Flash | 入力 1,800 / 出力 250 | 1.1 秒 | 3.4 秒 | $0.0014 | 約 $17 |
| Gemini 2.0 Flash | 入力 1,800 / 出力 250 | 0.9 秒 | 2.7 秒 | $0.0009 | 約 $11 |
レビュー要約のように「定型的な日本語の整形」がメインのタスクでは、2.5 Flash で十分な品質が出ました。コストは Pro 比で約 12.7 分の 1、レイテンシも 4 倍速くなります。私は最終的に Flash に寄せて、月額 $17 程度で運用しています。
Cold Start を含む全体レイテンシ(Cloud Functions Gen2)
| 状態 | Pro | Flash |
| Warm(直前に呼ばれた直後) | 4.2 秒 | 1.1 秒 |
| Cool(5 分以上経過) | 28 秒 | 20 秒 |
| Cold(初回起動) | 42 秒 | 24 秒 |
| Cold + minInstances=1 | 5.0 秒 | 1.4 秒 |
minInstances: 1 を入れたときの追加コストは、私の構成で月額およそ $4 でした。レビュー要約は非同期処理なので付けていませんが、ユーザーから直接呼ばれるエンドポイント(例:FAQ 検索 Bot)には付けています。
キャッシュヒット率による実コスト圧縮
// Firestore キャッシュを噛ませた後の実測(同じレビュー本文が来た場合のみヒット)
const CACHE_HIT_RATE = 0.18; // 約 18%
const ORIGINAL_COST_PER_MONTH = 17; // Flash で月 $17
const EFFECTIVE_COST = ORIGINAL_COST_PER_MONTH * (1 - CACHE_HIT_RATE);
// → 約 $14/月(さらに 3 ドル削減)
レビュー要約のように同じ文章が再投稿されることが少ない用途では、キャッシュヒット率は 20% を切る程度でした。FAQ Bot のように「同じ質問が繰り返し来る」用途だと、私の実測ではヒット率が 50〜65% まで上がり、$17 → $7 程度まで落ちます。
5,000 万 DL 運用の AdMob 連携アプリで Genkit を活かす設計パターン
最後に、私が壁紙・癒し・引き寄せ系アプリで実際に採用している、AdMob 連携アプリ特有の「Genkit の置き場所」設計を共有します。AdMob 連携アプリは、ユーザー体験を 1 秒でも遅らせると広告 impression あたりの収益(eCPM)に直接響くため、Gemini のような「答えが返るまで数秒待たされる API」をどう扱うかが収益に直結します。
パターン A:ユーザー操作の主導線には Gemini を入れない
壁紙アプリのトップ画面、検索画面、ダウンロード画面など、ユーザーが秒単位で操作する画面では Gemini を直接呼びません。これらの画面は AdMob のバナー・ネイティブ広告の表示機会が多く、レイテンシが下がると impression 数が増えてしまいます。
Gemini を使うのは、「画像にぴったりな名前をつけてもらう」「ユーザーが集めた壁紙コレクションに対してテーマ別の説明文を生成する」のような、明示的にユーザーが「考えてもらう」ボタンを押したときに限定しています。この場合はローディング表示が許容されるので、Pro でも問題ありません。
パターン B:オフピーク時間帯のバッチで Gemini を回す
新着壁紙のタグ生成、レビュー要約、SEO 用説明文の生成といった「ユーザー操作と非同期で良いもの」は、すべてオフピーク時間帯(私の場合 02:00〜05:00 JST)の Cloud Scheduler バッチで Genkit Flow を走らせています。
// Cloud Scheduler から発火される、ユーザー非接続なバッチ
export const overnightTagGeneration = functions.pubsub
.schedule("0 3 * * *")
.timeZone("Asia/Tokyo")
.onRun(async () => {
const newWallpapers = await fetchNewWallpapersWithoutTags();
for (const wallpaper of newWallpapers) {
await runFlow(tagGenerationFlow, { wallpaperId: wallpaper.id });
}
});
この時間帯なら Cold Start が出ても誰も困りませんし、Gemini 側のレート制限にも余裕があります。私はこのパターンで、月間およそ 80,000 件の自動タグ付けを Flash で回しており、コストは月 $25 ほどです。
パターン C:FAQ / サポート用は Flash + 1 秒キャッシュ TTL
サポート FAQ 検索のように、ユーザーが質問を入力して数秒待つことが許容される用途は、Gemini 2.5 Flash に絞り、Firestore に直近 24 時間のキャッシュを置いています。同じ質問が来たらキャッシュから返し、新規質問だけ Flash に投げる構成です。
// FAQ Bot 用:温度を低く、キャッシュ強めに
const faqResponse = await ai.generate({
model: gemini15Flash,
prompt: faqPrompt,
config: {
temperature: 0.2, // 個性は不要、安定性重視
maxOutputTokens: 400,
}
});
temperature: 0.2 にしているのは、FAQ で「個性的な回答」が出ると逆にサポートのオペレーションコストが上がるためです。私は実体験として、temperature: 0.7 で運用していた時期に「Gemini が独自解釈した使い方案内」がユーザーから問い合わせとして返ってきて、結局人間が修正することになった件が複数あったため、ここは低めに固定しています。
Cloud Functions と Cloud Run の使い分け早見表
| 用途 | 推奨 | 理由 |
| ユーザー操作直結(即時性必要・Gemini を直接呼ぶ) | Cloud Run + minInstances=1 + Flash | Cold Start を吸収しないと UX が崩れる |
| バッチ処理(オフピーク・大量) | Cloud Functions Gen2 + 適切な concurrency | 起動の柔軟さとコスト |
| ストリーミングが必要 | Cloud Run(Cloud Functions は SSE 不向き) | Functions の SSE はタイムアウト 60 秒で詰む |
| 数秒の処理を月数百回 | Cloud Functions(最安構成) | 起動コストが時間課金より安い |
| 数十秒の処理を月数万回 | Cloud Run(concurrency 活用) | 1 コンテナで多重処理できる |
私の現状の構成は、レビュー要約とタグ生成が Cloud Functions Gen2、サポート FAQ Bot だけ Cloud Run、という分け方です。AdMob 収益が日次でブレるアプリでは、固定費の minInstances は最小限に抑えたほうが精神衛生上も良いと感じています。
全体を振り返ってと次のステップ
Firebase Genkit を個人開発の本番に置いてみて率直に感じているのは、「Gemini を素で叩いていた頃に比べて、運用が単純に静かになった」ということです。Cloud Functions に書き散らしていたトークン制限のコード、エラー時のリトライ、Cloud Logging への構造化ログ出力、これらが Flow / Tool / トレースという 3 つの抽象化に吸収されて、コードレビュー時に追うべき場所が明確になりました。
ただ、この記事で書いてきたように、Cold Start のロングテール、concurrency のデフォルト、Secret Manager の重複呼び出し、トレースの PII 露出、streamCallback のバックプレッシャーといった「公式には書かれていない」ポイントを踏まないと、コストもレイテンシも想定の 2〜3 倍に膨らみます。
これから Genkit を入れる方には、まずは「ユーザー操作から外れたバッチ用途」で Flash を回し、コストとレイテンシの実測を取ってみることをお勧めします。私の場合はそこから「常駐させても元が取れる用途」を見極めて、Cloud Run に少しずつ移していきました。
次に踏むと良いステップは、自分の業務やアプリの中から「人間が同じ作業を毎日繰り返している部分」を 1 つ選び、その部分を defineFlow ひとつに置き換えてみることだと考えています。Genkit の良さは、小さく始めても後から段階的に拡張できる設計に表れています。
同じように個人開発のバックエンドに Gemini を組み込もうとしている方の参考になれば幸いです。お読みいただきありがとうございました。