はじめに
前回の記事 では、 Vertex AI Vector Search を使って高精度なハイブリッド検索の検証手順と結果の知見をまとめました。
今回はその技術を基盤とし、AIエージェントの頭脳と神経系を構築していきます。
AIエージェント開発には、単なるチャット応答だけでなく、外部ツールとの連携(Function Calling)、文脈に応じた記憶の参照(RAG)、さらにはユーザーからのフィードバックを受けて成長する「自己学習」の仕組みなど、多くの複雑な要素が絡み合います。
これらの複雑な要件を、宣言的かつ効率的に実装するために、Firebase Genkit というフレームワークを採用しました。
この記事では、Genkitを使って構築したパーソナルAIエージェントのバックエンドの検証と知見をまとめます。
対象読者
- Firebase Genkitの具体的な活用事例を知りたい方
- AIエージェントやLLMアプリケーションのバックエンド開発に興味がある方
- Function Calling、RAG、エージェントのパーソナライズといった先進的な手法を学びたい方
構築したパーソナルAIエージェントの全体像
今回構築したシステムのアーキテクチャは以下の通りです。
ユーザーからのリクエストはFirebase Functions経由でGenkit Flowに渡され、必要に応じて各種ツール(Google Calendar, Vector Search など)を実行し、応答を生成します。
Genkitのコア機能①:FlowとToolによる外部連携 (Function Calling)
Genkitの中核をなすのが Flow と Tool です。
Flow は一連の処理の流れを定義し、Tool はLLMが呼び出し可能な具体的な機能(関数)を定義します。
mainAgentFlow では、ユーザーの質問に応じてGoogleカレンダーを操作したり、天気を調べたりするためのツール群をLLMに提供しています。
(抜粋)
// カレンダーに新しい予定を作成するツール
const createEventTool = ai.defineTool({
name: 'create_calendar_event',
description: 'ユーザーのGoogleカレンダーに新しい予定を作成する。',
inputSchema: z.object({
summary: z.string().describe("予定のタイトル"),
start_datetime_iso: z.string().describe("開始日時"),
end_datetime_iso: z.string().describe("終了日時"),
}),
// ... (実装は省略)
}, async (input) => { /* ... Google Calendar APIを叩く処理 ... */ });
// メインの対話フロー
const mainAgentFlow = ai.defineFlow({
name: 'mainAgentFlow',
// ... (入力・出力スキーマ定義は省略)
}, async ({ query, userId }, { sendChunk }) => {
// ...
// LLMにプロンプトと利用可能なツール群を渡して応答を生成
const llmResponseStream = await ai.generateStream({
prompt: finalPrompt,
model: vertexAI.model('gemini-2.5-flash'),
tools: [
getDateTool,
listEventsTool,
createEventTool,
getWeatherTool,
// ... 他のツール
],
});
// ... (ストリーミング応答の処理)
});
このように、Genkitでは自然言語でツールの説明を記述するだけで、LLMがユーザーの意図を解釈し、適切なツールを自動で選択・実行(Function Calling)してくれます。
Genkitのコア機能②:Retrieverによる長期記憶の実装 (RAG)
前回の記事で実装したハイブリッド検索は、Genkitでは Retriever という概念で抽象化され、簡単にFlowに組み込めます。
(抜粋)
// ハイブリッド検索を実行するRetrieverを定義
async function hybridSearchRetriever(input: { query: string | Document, options: any }): Promise<{ documents: Document[] }> {
const queryString = typeof input.query === 'string' ? input.query : input.query.text;
const userId = input.options?.userId;
// Dense Vector と Sparse Vector を生成
const denseVector = /* ... */;
const sparseVector = /* ... */;
// Vertex AI Vector Search APIを呼び出し
const searchResult = await fetch(/* ... */);
// 結果をDocument形式に変換して返す
return { documents: /* ... */ };
}
// メインフロー内で、ユーザーの意図が「知識検索」の場合にRetrieverを呼び出す
if (intent === 'KNOWLEDGE_QUERY') {
const documents = await ai.retrieve({
retriever: hybridSearchRetriever,
query: query,
options: { userId: userId }
});
longTermMemory = JSON.stringify(documents.map(d => d.toJSON()));
}
さらに、会話のやり取りから「これは記憶すべき重要な情報だ」とLLM自身に判断させ、自動でVector Searchに記憶を登録していくフローも実装しています。
(抜粋)
// 記憶生成フロー
const memoryCreationFlow = ai.defineFlow({
name: 'memoryCreationFlow',
// ...
}, async ({ userId, userQuery, modelResponse }) => {
// 1. 会話内容が記憶に値するかをLLMが判断
const memoryType = await ai.generate({ prompt: `...` });
if (memoryType !== 'none') {
// 2. 記憶すべき内容をLLMが要約・抽出し、Firestoreに保存
const extractedData = await ai.generate({ prompt: `...` });
await db.collection('memoriesTest').doc().set({ ... });
// 3. Vector Searchにベクトルを登録 (Upsert)
// ...
}
});
// mainAgentFlowの最後に非同期で呼び出す
memoryCreationFlow.run({ ... }).catch(e => console.error(e));
Genkitが可能にする高度なエージェント機能:自己学習ループ
このエージェントの最大の特徴は、ユーザーからのフィードバックを通じて自律的に成長する「自己学習ループ」を実装している点です。
-
フィードバック収集 (フロントエンド): ユーザーはAIの応答に対して「Good」または「Bad」の評価ができます
-
フィードバック保存 (Genkit Flow): 評価が行われると、フロントエンドは
recordFeedbackFlowを呼び出し、評価内容をFirestoreに保存します -
夜間バッチ処理 (Scheduled Functions): 毎日深夜に
dailyProcessingOrchestratorが起動します -
プロファイルの更新 (Genkit Flow): その日の会話履歴とフィードバックを基に、
updateProfileFlowがLLMを使ってユーザーの性格や好みを分析し、プロファイル情報を更新します
(抜粋)
// 1. フィードバックを受け取るフロー
const recordFeedbackFlow = ai.defineFlow({ /* ... */ });
// 2. ユーザープロファイルを更新するフロー
const updateProfileFlow = ai.defineFlow({ /* ... */ },
async ({ userId, conversations, feedbackSummary }) => {
const existingProfile = /* ... Firestoreから既存プロファイル取得 ... */;
const prompt = `あなたは優秀なプロファイラーです。
# 既存のプロファイル: ${existingProfile}
# 本日の会話: ${conversations}
# 本日のフィードバック (最重要): ${feedbackSummary}
上記を基に、ユーザーのプロファイルを更新してください。`;
const updatedProfile = await ai.generate({ prompt, model: vertexAI.model('gemini-2.5-pro') });
await db.collection('userProfiles').doc(userId).set({ profile: updatedProfile.text });
});
// 3. 毎日深夜に実行されるオーケストレーター
export const dailyProcessingOrchestrator = onSchedule(/* ... */, async (event) => {
// ... その日の会話とフィードバックを取得 ...
await updateProfileFlow.run({ userId, conversations, feedbackSummary });
});
このループにより、エージェントは使われれば使われるほどユーザーを理解し、よりパーソナルで気の利いた応答ができるように進化していきます。
Genkitで実装するAIエージェントの思考プロセス
mainAgentFlowは、単にLLMを呼び出すだけではなく、ユーザーに応答を返すまでに、以下のような多段階の思考プロセスを経ています。
Genkitは、この複雑な思考のオーケストレーション(指揮)を実現しています。
1. 意図分類(Intent Classification)という名の司令塔
まず、エージェントはユーザーの質問を受け取ると、最初に intentClassifierFlow を実行します。
これは、ユーザーの「意図」が何であるかを分類する、いわば司令塔の役割を果たします。
// フロー2: 意図分類フロー
const intentClassifierFlow = ai.defineFlow({
name: 'intentClassifierFlow',
inputSchema: z.string(),
outputSchema: z.enum([
'CALENDAR_TASK_QUERY', // カレンダーやタスク操作
'KNOWLEDGE_QUERY', // 過去の記憶の参照
'GOAL_MANAGEMENT_QUERY', // 目標設定
'GREETING', // 挨拶・日常会話
]),
}, async (query) => {
// ... (LLMによる分類プロンプト) ...
});
例えば、ユーザーが「こんにちは」と挨拶しただけなのに、コストの高いVector Search(長期記憶の参照)を動かしてしまうのは非効率です。
意図を分類することで、「挨拶には挨拶で返す」「カレンダーの操作ならToolを使う」「過去の出来事の質問ならRetrieverを使う」といった最適な応答戦略を、的確に切り替えることができます。
2. コンテキストの収集とプロンプトの構築
意図を判断した後、エージェントは応答を生成するために必要な情報(コンテキスト)を多角的に収集します。
- 短期記憶: 直近の会話履歴(Firestoreから取得)
-
長期記憶: 関連する過去の出来事(
KNOWLEDGE_QUERYの場合のみ、Vector Search Retrieverを実行) - 自己認識: ユーザーの性格や好み(ユーザープロファイル)
-
外部状況: 現在時刻や直近の予定(
listEventsToolを実行)
これらの情報をすべて集約し、最終的なプロンプト(finalPrompt)を構築します。これにより、単なる質疑応答ではない、文脈を深く理解したパーソナルな応答が可能になります。
3. 応答後の記憶形成
ユーザーに応答を返した後は、非同期で memoryCreationFlow が起動し、今回の会話が将来役に立つ「記憶」すべき情報かを判断します。
もし重要だと判断されれば、ベクトル化されて長期記憶(Vector Search)に保存されます。
この「応答して終わり」ではない継続的な学習プロセスが、エージェントを徐々に賢くしていきます。
【発展】パフォーマンス最適化:onInitによる遅延初期化
AIエージェントのように多機能なバックエンドをサーバーレス(Cloud Functions)で運用する際、コールドスタートの問題は避けて通れません。
関数が久しぶりに呼び出された際、初期化処理に時間がかかり、応答が遅延する現象です。
Genkitは、onInit フックと動的 import() を組み合わせることで、この問題にスマートに対処できます。
// 2. グローバル変数を定義
let tfidf: TfIdf;
let segmenter: TinySegmenter;
// 3. onInitフックで、重いライブラリの「値」を動的にインポートして初期化
onInit(async () => {
console.log("Cold Start: Initializing models...");
// ライブラリの「値」(クラス本体)を動的にインポート
const TinySegmenterConstructor = (await import('tiny-segmenter')).default;
const { TfIdf: TfIdfConstructor } = await import('natural');
// TinySegmenterを初期化
segmenter = new TinySegmenterConstructor();
// TF-IDFモデルをファイルから読み込み
// ...
tfidf = model;
console.log("TF-IDF model initialized successfully.");
});
この実装では、TF-IDFモデルや形態素解析器といったメモリ消費の大きいライブラリを、Cloud Functionsのインスタンスが起動する最初の1回だけ onInit フック内で初期化します。
2回目以降の呼び出しでは、この初期化処理がスキップされるため、高速な応答が可能になります。
Genkit Flowの実行トリガーとローカル開発
Genkitで定義したFlowは、ただ定義するだけでは実行できません。
クライアントアプリから呼び出すための「APIエンドポイント」を作成したり、特定の時間に自動実行する「スケジュール」を設定したりする必要があります。
また、Genkitには非常に強力なローカル開発環境が備わっています。
1. onCallGenkit:クライアントからのHTTPトリガー
onCallGenkitは、GenkitのFlowをHTTPS CallableなCloud Functionsとして公開するための関数です。
これにより、Webフロントエンドやモバイルアプリから安全にFlowを呼び出すことが可能になります。
今回のエージェントのメイン機能であるmainAgentFlowも、このonCallGenkitを使って公開しています。
(抜粋)
import { onCallGenkit } from 'firebase-functions/v2/https';
export const mainAgent = onCallGenkit(
{
region: 'asia-northeast1',
memory: '1GiB',
timeoutSeconds: 600,
cors: true
},
mainAgentFlow
);
このようにラップすることで、クライアントのFirebase SDKからmainAgentという名前でこのFlowを呼び出せるようになります。
regionやmemoryといったFunctionsの実行環境に関する設定もここで行います。
2. onSchedule:定時実行トリガー
onScheduleは、Flowをスケジュールに基づいて自動実行させるためのトリガーです。
Cloud Schedulerと連携し、「毎日朝7時」や「15分ごと」といった柔軟な設定が可能です。
このエージェントの「自己学習ループ」や「モーニングブリーフィング」といったプロアクティブな機能は、このonScheduleによって実現されています。
(抜粋)
import { onSchedule } from 'firebase-functions/v2/scheduler';
// 毎日朝7時にモーニングブリーフィングを実行
export const morningBriefing = onSchedule({
region: 'asia-northeast1',
schedule: "every day 07:00",
memory: '1GiB',
timeZone: 'Asia/Tokyo'
}, async (event) => {
// ... モーニングブリーフィングを生成・送信する処理 ...
});
scheduleプロパティにcron形式で実行タイミングを指定するだけで、定期実行されるFunctionを簡単に作成できます。
3. startFlowServer:ローカル開発サーバーとUI
Genkitの大きな魅力の一つが、ローカルでの開発体験です。
コードの最後に記述されているstartFlowServerは、開発環境でのみ動作するローカルサーバーを起動します。
(抜粋)
import { startFlowServer } from '@genkit-ai/express';
// ローカル開発環境の場合のみサーバーを起動
if (process.env.NODE_ENV === 'development') {
console.log('Starting Genkit flow server for local development...');
startFlowServer({
flows: [
mainAgentFlow,
updateThreadTitleFlow,
deleteThreadFlow,
memoryCreationFlow,
verifyMemorySearchFlow
]
});
}
このサーバーが起動している状態で、ターミナルでgenkit startコマンドを実行すると、記事で紹介したDeveloper UIが立ち上がります。
これにより、Flowをデプロイすることなく、手元で入力値を試したり、実行トレースを詳細に確認したりできるため、開発サイクルを高速に回すことができます。
Firebaseコマンドでデプロイ
GenkitのFlowやToolとして作成したら下記コマンドでデプロイ可能です。
firebase deploy --only functions
FirebaseのコンソールでSchedulerもFunctionsも一元管理できます。
フロントエンド(React Native)との連携
GenkitのFlowは、Firebase Functions SDKを通じてクライアントから簡単に呼び出せます。
ストリーミング応答にも対応しているため、ChatGPTのようなUIも実現可能です。
(抜粋)
import { httpsCallable } from "firebase/functions";
// Genkitフローへの参照を定義
const mainAgent = httpsCallable(functions, 'mainAgent');
const recordFeedback = httpsCallable(functions, 'recordFeedback');
const sendMessage = async () => {
// ...
try {
const requestBody = { query: currentInput, userId: USER_ID, threadId: currentThreadId };
// ストリーミングでFlowを呼び出し
const { stream } = await mainAgent.stream(requestBody);
for await (const chunk of stream) {
// リアルタイムでUIを更新
setMessages(prev => /* ... */);
}
} catch (error) { /* ... */ }
};
const handleFeedback = async (message: Message, rating: 'good' | 'bad') => {
await recordFeedback({ /* ...フィードバック情報を渡す... */ });
};
おわりに
Firebase Genkitは、単なるLLMのAPIラッパーに留まらず、Flow、Tool、Retrieverといった強力な抽象化を提供することで、複雑なAIエージェントのバックエンド開発を劇的に効率化してくれます。
特に、Firebaseの各種サービス(Functions, Firestore, Authentication)とシームレスに連携できる点は大きな魅力です。
今回は実装しませんでしたが、GenkitはLangChainなどの外部ライブラリとも連携可能です。
今後のAIエージェント開発の基盤となりうるフレームワークとして活用していけると感じていますので、今回全ては紹介しませんでしたが、様々な視点や方法で自己学習をさせて、よりパーソナルなエージェントの開発を進めていきます。

