Flow を1本デプロイして気づいた「フレームワークの薄さ」の価値
最初に Firebase Genkit を触ったとき、正直なところ「また新しい抽象化層か」という警戒がありました。個人開発で Dolice Labs を回している私にとって、増えるのは覚えることばかりで、得られるものが見合わない道具はむしろ負債になります。
その警戒が解けたのは、挨拶を返すだけの小さな Flow を1本、ローカルの Dev UI で動かし、そのまま Cloud Functions にデプロイできたときでした。入出力のスキーマ、ローカル検証、トレース、デプロイ。これらが同じ書き味の中に収まっている。フレームワークが薄いからこそ、Gemini の呼び出しそのものに集中できる。
本稿は、その最初の一本から RAG とエージェントまでを、私が実際に書いて確かめたコードでたどる実装ノートです。公式ドキュメントの逐条訳ではなく、個人開発で運用するときに詰まる箇所を中心にまとめています。なお Genkit は API の変遷が速い領域です。ここでは 1.x 系の genkit() コンストラクタと zod スキーマを前提にしています。
インストールと初期化 — まず1箇所に集約する
Genkit 本体と Google AI プラグインを入れます。TypeScript で書くのが素直です。
npm install genkit @genkit-ai/googleai
npm install -D typescript tsx @types/node
初期化は1ファイルに集約し、他のモジュールはここから ai を取り込む形にしておくと、モデルの差し替えが一箇所で済みます。
// src/genkit.ts
import { genkit } from "genkit" ;
import { googleAI } from "@genkit-ai/googleai" ;
// モデルIDは利用したいものに置き換えてください(現行の Flash / Pro 系)
export const ai = genkit ({
plugins: [ googleAI ({ apiKey: process.env. GOOGLE_API_KEY })],
model: googleAI. model ( "gemini-2.5-flash" ),
});
apiKey はコードに直書きせず、必ず環境変数から渡します。ローカルでは .env、本番では後述の Secret Manager 経由が安全です。
最初の Flow — zod スキーマで入出力を固める
Genkit の Flow は「入力スキーマ・処理・出力スキーマ」を1つにまとめた関数です。スキーマを zod で書いておくと、Dev UI がそのまま入力フォームを生成し、実行トレースも型付きで残ります。
// src/flows/greeting.ts
import { z } from "genkit" ;
import { ai } from "../genkit" ;
export const greetingFlow = ai. defineFlow (
{
name: "greeting" ,
inputSchema: z. object ({
name: z. string (),
language: z. enum ([ "ja" , "en" ]),
}),
outputSchema: z. string (),
},
async ({ name , language }) => {
const prompt =
language === "ja"
? `${ name }さんへの、短くて親切な挨拶を日本語で書いてください。`
: `Write a short, friendly greeting for ${ name }.` ;
const { text } = await ai. generate ({ prompt });
return text;
},
);
ローカルでの確認は Dev UI から行います。
# Dev UI を起動(http://localhost:4000 でフローを試せます)
npx genkit start -- npx tsx --watch src/index.ts
ここで大切なのは、ai.generate() の戻り値から text を分割代入で受け取っている点です。旧来のサンプルで見かける response.text() のようなメソッド呼び出しではありません。Genkit 1.x の戻り値はプロパティであり、この差はコピー&ペーストで最初につまずく箇所でした。
マルチステップ Flow — ドキュメント解析を1関数に畳む
複数の前処理と生成を1つの Flow にまとめると、呼び出し側は入力を渡すだけで済みます。ここでは文書を受け取り、要約・感情・キーワードのいずれかを返す例です。
// src/flows/documentAnalysis.ts
import { z } from "genkit" ;
import { ai } from "../genkit" ;
export const documentAnalysisFlow = ai. defineFlow (
{
name: "documentAnalysis" ,
inputSchema: z. object ({
documentText: z. string (),
analysisType: z. enum ([ "summary" , "sentiment" , "keywords" ]),
}),
outputSchema: z. object ({
analysisType: z. string (),
result: z. string (),
}),
},
async ({ documentText , analysisType }) => {
// ステップ1: 入力を安全な長さに切り詰める
const cleaned = documentText. trim (). slice ( 0 , 5000 );
// ステップ2: 分析種別ごとにプロンプトを用意
const prompts : Record < string , string > = {
summary: `次の文書を3文で要約してください: \n ${ cleaned }` ,
sentiment: `次の文書の感情を ネガティブ / ニュートラル / ポジティブ で判定してください: \n ${ cleaned }` ,
keywords: `次の文書から主要キーワードを5つ挙げてください: \n ${ cleaned }` ,
};
// ステップ3: Gemini で生成し、構造化して返す
const { text } = await ai. generate ({ prompt: prompts[analysisType] });
return { analysisType, result: text };
},
);
入力を 5,000 文字で切り詰めているのは、想定外に長い本文が渡ってトークン費用が跳ねるのを防ぐためです。上限は扱う文書に合わせて決めますが、外側に置いておくと事故の芽を一つ潰せます。
Tool Calling とエージェント — ツールをモデルに委ねる
Genkit では、ツールを ai.defineTool で定義し、generate に渡すだけで、モデルが必要に応じて呼び出しを判断します。エージェントのために特別なクラスを構築する必要はありません。
// src/tools/weather.ts
import { z } from "genkit" ;
import { ai } from "../genkit" ;
export const getWeather = ai. defineTool (
{
name: "getWeather" ,
description: "指定した都市の現在の天気を返します" ,
inputSchema: z. object ({ city: z. string () }),
outputSchema: z. object ({
city: z. string (),
temperature: z. number (),
condition: z. string (),
}),
},
async ({ city }) => {
// 実運用では外部の天気APIを呼び出します
return { city, temperature: 25 , condition: "晴れ" };
},
);
// src/flows/assistant.ts
import { z } from "genkit" ;
import { ai } from "../genkit" ;
import { getWeather } from "../tools/weather" ;
export const assistantFlow = ai. defineFlow (
{
name: "assistant" ,
inputSchema: z. object ({ question: z. string () }),
outputSchema: z. string (),
},
async ({ question }) => {
const { text } = await ai. generate ({
prompt: question,
tools: [getWeather],
system: "あなたは有能なアシスタントです。必要な場合のみツールを使い、簡潔に答えてください。" ,
});
return text;
},
);
ツールを増やすほど、モデルが「どのツールを・いつ呼ぶか」を誤る余地も増えます。ツールの説明文(description)は、モデルにとっての唯一の判断材料です。曖昧だと誤選択が増えます。この計測と対策については、エージェントのツール誤選択を計測して直す実践記録 に切り分けの手順をまとめています。
RAG — Firestore Vector Search と接続する
RAG では、クエリを埋め込みに変換し、近い文書を取り出して、その文脈だけを根拠に答えさせます。Genkit は埋め込みを ai.embed で扱えます。
// src/flows/rag.ts
import { z } from "genkit" ;
import { ai } from "../genkit" ;
import { googleAI } from "@genkit-ai/googleai" ;
export const ragFlow = ai. defineFlow (
{
name: "documentRAG" ,
inputSchema: z. object ({ query: z. string (), topK: z. number (). default ( 3 ) }),
outputSchema: z. string (),
},
async ({ query , topK }) => {
// 1. クエリを埋め込みベクトルに変換
const [ embedding ] = await ai. embed ({
embedder: googleAI. embedder ( "text-embedding-004" ),
content: query,
});
// 2. Firestore Vector Search で近傍文書を取得
const docs = await searchSimilar (embedding.embedding, topK);
// 3. 取得した文脈だけを根拠にプロンプトを組む
const context = docs. map (( d ) => `- ${ d . content }` ). join ( " \n " );
const { text } = await ai. generate ({
prompt: `次の参考資料だけを根拠に、日本語で簡潔に答えてください。根拠がなければ その旨を述べてください。 \n\n 参考資料: \n ${ context } \n\n 質問: ${ query }` ,
});
return text;
},
);
// Firestore の近傍検索は別記事に実装を分けています
async function searchSimilar (
_embedding : number [],
_topK : number ,
) : Promise < Array <{ content : string }>> {
return [];
}
近傍検索の実体と、再インデックス時のドリフト対策はFirestore × Gemini 埋め込みの再埋め込みドリフト対策 に、マルチモーダルへの拡張はGemini マルチモーダル RAG システムの構築 に分けています。埋め込みの次元とベクトルDBのコストの関係はMatryoshka 次元削減でベクトルDBコストを抑える が参考になります。
Cloud Functions へのデプロイ — onCallGenkit で薄く包む
Flow はそのまま Firebase Functions に載せられます。onCallGenkit を使うと、認証・ストリーミング・App Check の配線を短く書けます。
// functions/src/index.ts
import { onCallGenkit } from "firebase-functions/https" ;
import { defineSecret } from "firebase-functions/params" ;
import { greetingFlow } from "./flows/greeting" ;
import { assistantFlow } from "./flows/assistant" ;
// APIキーは Secret Manager から注入する
const apiKey = defineSecret ( "GOOGLE_API_KEY" );
export const greeting = onCallGenkit ({ secrets: [apiKey] }, greetingFlow);
export const assistant = onCallGenkit ({ secrets: [apiKey] }, assistantFlow);
デプロイ手順は次の通りです。
# 1. Firebase CLI を用意
npm install -g firebase-tools
# 2. APIキーを Secret Manager に登録(コードには残さない)
firebase functions:secrets:set GOOGLE_API_KEY
# 3. Functions のみをデプロイ
firebase deploy --only functions
# 4. 一覧で確認
firebase functions:list
APIキーを defineSecret で受けている点が肝心です。環境変数のベタ書きは、リポジトリやログへの漏洩経路になります。Secret Manager に寄せておけば、鍵のローテーションもコード変更なしで回せます。より大規模な本番運用はVertex AI Agent Engine への本番デプロイ も選択肢になります。
コスト最適化 — モデルルーティングと費用の見える化
サーバーレスの料金は、油断すると一部のフローだけで膨らみます。私が最初に入れるのは、難易度でモデルを振り分けるルーティングです。
// src/flows/routed.ts
import { z } from "genkit" ;
import { ai } from "../genkit" ;
import { googleAI } from "@genkit-ai/googleai" ;
const FLASH = googleAI. model ( "gemini-2.5-flash" );
const PRO = googleAI. model ( "gemini-2.5-pro" );
export const routedFlow = ai. defineFlow (
{
name: "routed" ,
inputSchema: z. object ({ prompt: z. string (), hard: z. boolean () }),
outputSchema: z. string (),
},
async ({ prompt , hard }) => {
// 難しいタスクだけ Pro に回し、それ以外は Flash で捌く
const { text } = await ai. generate ({ model: hard ? PRO : FLASH , prompt });
return text;
},
);
あわせて、フローごとに呼び出し回数と実行時間を記録します。全体の合計だけを見ていると、どのフローが費用を押し上げているのかが分かりません。私の手元では、単純な分類タスクを Pro で回していた時期があり、Flash に落とすだけで当該フローのモデル費用が体感で半分以下になりました。段階的なコスト制御の考え方は個人開発のコストガードレール にもまとめています。
コールドスタートと運用 — 個人開発で効いた地味な調整
サーバーレスで最初にぶつかる壁はコールドスタートでした。たまにしか呼ばれないフローほど初回が遅く、ユーザーには「重いアプリ」に映ってしまいます。私の手元での計測は次の通りです。
フロー
ウォーム時の平均
コールドスタート初回
最小インスタンス設定後の初回
greeting
約0.9秒
約4.2秒
約1.1秒
documentAnalysis
約1.6秒
約5.0秒
約1.7秒
documentRAG
約2.3秒
約6.1秒
約2.4秒
対策は素朴です。呼び出し頻度の高い経路は minInstances を 1 に設定してウォームに保ち、重い初期化はモジュール読み込み時ではなくリクエスト経路の外へ寄せる。派手さはありませんが、体感速度は結局こうした調整で決まると感じています。最小インスタンスは常時課金につながるため、本当に温めるべき経路だけに絞るのが、個人開発での落としどころです。
まとめ — 小さく動かし、費用の内訳を持って育てる
Firebase Genkit の価値は、フレームワークが薄く、Gemini の呼び出しに集中できる点にあります。まずは挨拶を返すだけの Flow を1本、ローカルで動かしてデプロイしてみてください。その一本が通れば、Tool Calling も RAG も同じ書き味の延長です。
次の一歩として、あなたのフローに minInstances の要否を判断する材料(フローごとのレイテンシと呼び出し頻度)を、最初から記録しておくことをおすすめします。あとから足すより、ずっと楽に育てられます。
実装の足がかりになれば幸いです。お読みいただきありがとうございました。
さらに深掘りするには、Firebase Genkit 公式ドキュメント とGemini API 公式ドキュメント もあわせてご覧ください。