セマンティックルーターのアーキテクチャ設計
2 段階ルーティングの概念
効果的なルーターは「速いチェック → 深いチェック」の 2 段階で動作します。
Stage 1: ルールベースフィルター(<1ms)
正規表現パターンマッチング、キーワード検出、リクエストメタデータ(ユーザープラン、セッション種別)で判断します。
Stage 2: セマンティック類似度ルーティング(5〜20ms)
クエリの embedding を生成し、事前定義したルート例との類似度を計算、信頼スコアに基づくモデルを選択します。
Stage 1 で決定できない曖昧なクエリのみ Stage 2 に進む設計にすることで、ルーティングコスト自体を最小化できます。私の壁紙アプリのバックエンドでは、実測で 75% のクエリが Stage 1 で確定し、Stage 2 まで進んだのは 25% 程度でした。
ルーティング判定フロー
Query 入力
↓
[Stage 1] ルールベースチェック
├─ 明確にシンプル → Flash
├─ 明確にコンプレックス → 2.5 Pro
└─ 不明 → Stage 2 へ
↓
[Stage 2] Embedding ルーティング
├─ Flash ルートとの類似度 > 0.85 → Flash
├─ Pro ルートとの類似度 > 0.80 → Pro
└─ それ以外 → 2.5 Pro(デフォルト)
↓
選択されたモデルで推論実行
↓
コスト記録・メトリクス更新
Python 実装:動く完全版セマンティックルーター
依存ライブラリのインストール
pip install google-generativeai numpy scikit-learn python-dotenv
メインルーター実装
import os
import re
import time
import json
import logging
from dataclasses import dataclass, field
from typing import Optional
import numpy as np
import google.generativeai as genai
from dotenv import load_dotenv
load_dotenv()
# ロギング設定
logging.basicConfig( level = logging. INFO )
logger = logging.getLogger( __name__ )
genai.configure( api_key = os.environ[ "YOUR_GEMINI_API_KEY" ])
# =====================
# データクラス定義
# =====================
@dataclass
class RouteResult :
"""ルーティング結果"""
model_name: str
route_method: str # "rule_based" or "semantic"
confidence: float
reasoning: str
estimated_cost_ratio: float # Pro を 1.0 として正規化
@dataclass
class RouterMetrics :
"""コスト・パフォーマンス追跡"""
total_requests: int = 0
flash_count: int = 0
pro_count: int = 0
ultra_count: int = 0
total_tokens_in: int = 0
total_tokens_out: int = 0
estimated_cost_usd: float = 0.0
estimated_savings_usd: float = 0.0
def log_summary (self):
total = self .total_requests or 1
print ( f " \n 📊 Router Metrics Summary" )
print ( f " Total requests : { self .total_requests } " )
print ( f " Flash ratio : { self .flash_count / total :.1% } ( { self .flash_count } 件)" )
print ( f " Pro ratio : { self .pro_count / total :.1% } ( { self .pro_count } 件)" )
print ( f " Ultra ratio : { self .ultra_count / total :.1% } ( { self .ultra_count } 件)" )
print ( f " Est. Cost : $ { self .estimated_cost_usd :.4f } " )
print ( f " Est. Savings : $ { self .estimated_savings_usd :.4f } " )
# =====================
# Stage 1: ルールベースルーター
# =====================
class RuleBasedRouter :
"""高速ルールベースフィルター"""
# シンプルタスクのパターン
SIMPLE_PATTERNS = [
r " ^( はい | いいえ | yes | no )[ ?? ] ? $ " ,
r " ^. {1,30} [ ?? ]$ " , # 30 文字以下の短い質問
r " ( 翻訳して | translate | 要約して | summarize in one sentence ) " ,
r " ( 分類して | classify | ラベルを付け | label this ) " ,
r " ( 感情分析 | sentiment | ポジティブ | ネガティブ | positive | negative ) " ,
r " ( 今日の日付 | current date | 今何時 | what time ) " ,
]
# 複雑タスクのパターン
COMPLEX_PATTERNS = [
r " ( アーキテクチャ | architecture | 設計書 | design document ) " ,
r " ( 証明して | prove that | 数学的に | mathematically ) " ,
r " ( 包括的な | comprehensive | 詳細な分析 | detailed analysis ) " ,
r " ( コードをゼロから | implement from scratch | 完全実装 | full implementation ) " ,
r " ( 論文 | research paper | 学術 | academic | 文献調査 ) " ,
r " ( 最適化アルゴリズム | optimization algorithm | 複雑度 | complexity ) " ,
]
def route (self, query: str ) -> Optional[RouteResult]:
query_lower = query.lower()
# シンプルパターンチェック
for pattern in self . SIMPLE_PATTERNS :
if re.search(pattern, query, re. IGNORECASE ):
return RouteResult(
model_name = "gemini-2.5-flash" ,
route_method = "rule_based" ,
confidence = 0.95 ,
reasoning = f "Simple pattern matched: { pattern } " ,
estimated_cost_ratio = 0.05 ,
)
# 複雑パターンチェック
for pattern in self . COMPLEX_PATTERNS :
if re.search(pattern, query, re. IGNORECASE ):
return RouteResult(
model_name = "gemini-2.5-pro" ,
route_method = "rule_based" ,
confidence = 0.90 ,
reasoning = f "Complex pattern matched: { pattern } " ,
estimated_cost_ratio = 1.0 ,
)
# クエリ長による簡易判定
word_count = len (query.split())
if word_count < 10 :
return RouteResult(
model_name = "gemini-2.5-flash" ,
route_method = "rule_based" ,
confidence = 0.75 ,
reasoning = f "Short query ( { word_count } words)" ,
estimated_cost_ratio = 0.05 ,
)
return None # Stage 2 へ
# =====================
# Stage 2: セマンティックルーター
# =====================
class SemanticRouter :
"""Embedding ベースのセマンティックルーター"""
# 各ルートのサンプルクエリ(事前に定義しておく)
ROUTE_EXAMPLES = {
"gemini-2.5-flash" : [
"今日の天気を教えて" ,
"このテキストを英語に翻訳して" ,
"このレビューはポジティブですか?" ,
"一文で要約してください" ,
"郵便番号から都道府県を教えて" ,
"このカテゴリに分類して" ,
"メールの件名を 5 つ考えて" ,
"この文章の誤字を直して" ,
],
"gemini-2.5-pro" : [
"このコードのバグを見つけてデバッグして" ,
"マーケティング戦略のアウトラインを作成して" ,
"このデータセットのトレンドを分析して" ,
"API の設計をレビューして改善点を提案して" ,
"この契約書の重要なポイントをまとめて" ,
"Python で機械学習モデルを実装して" ,
],
"gemini-2.5-pro-ultra" : [
"量子コンピューターの数学的基礎から応用まで包括的に解説して" ,
"ゼロから分散システムのアーキテクチャを設計して" ,
"この研究論文の手法を批判的に評価して" ,
"複雑なアルゴリズムの時間複雑度を証明して" ,
"マイクロサービス移行の完全な技術設計書を作成して" ,
],
}
def __init__ (self):
self ._route_embeddings: dict[ str , list ] = {}
self ._initialized = False
def _get_embedding (self, text: str ) -> list[ float ]:
"""Gemini Embedding API でテキストをベクトル化"""
result = genai.embed_content(
model = "models/text-embedding-004" ,
content = text,
task_type = "SEMANTIC_SIMILARITY" ,
)
return result[ "embedding" ]
def _cosine_similarity (self, a: list[ float ], b: list[ float ]) -> float :
"""コサイン類似度を計算"""
a_np = np.array(a)
b_np = np.array(b)
return float (np.dot(a_np, b_np) / (np.linalg.norm(a_np) * np.linalg.norm(b_np)))
def initialize (self):
"""起動時にルート embedding を事前計算してキャッシュ"""
logger.info( "🔧 Initializing semantic router embeddings..." )
for route_name, examples in self . ROUTE_EXAMPLES .items():
embeddings = [ self ._get_embedding(ex) for ex in examples]
# 各ルートの embedding を平均化(centroid)
self ._route_embeddings[route_name] = np.mean(embeddings, axis = 0 ).tolist()
self ._initialized = True
logger.info( "✅ Semantic router initialized" )
def route (self, query: str ) -> RouteResult:
if not self ._initialized:
self .initialize()
# クエリの embedding を取得
query_embedding = self ._get_embedding(query)
# 各ルートとの類似度を計算
similarities = {}
for route_name, route_embedding in self ._route_embeddings.items():
sim = self ._cosine_similarity(query_embedding, route_embedding)
similarities[route_name] = sim
# 最も類似度が高いルートを選択
best_route = max (similarities, key = similarities.get)
best_score = similarities[best_route]
# コスト比率マッピング
cost_ratios = {
"gemini-2.5-flash" : 0.05 ,
"gemini-2.5-pro" : 0.40 ,
"gemini-2.5-pro-ultra" : 1.0 ,
}
return RouteResult(
model_name = best_route.replace( "-ultra" , "" ), # API モデル名に変換
route_method = "semantic" ,
confidence = best_score,
reasoning = f "Similarity scores: { json.dumps({k: round (v, 3 ) for k, v in similarities.items()}) } " ,
estimated_cost_ratio = cost_ratios.get(best_route, 0.5 ),
)
# =====================
# メインルーター(オーケストレーター)
# =====================
class GeminiSmartRouter :
"""
2 段階セマンティックルーターのメインクラス。
ルールベース → セマンティック の順で最適モデルを選択し、
実際に Gemini API を呼び出して結果を返す。
"""
# モデル名のエイリアス解決
MODEL_ALIASES = {
"gemini-2.5-flash" : "gemini-2.5-flash" ,
"gemini-2.5-pro" : "gemini-2.5-pro" ,
"gemini-pro" : "gemini-2.5-pro" , # フォールバック
}
def __init__ (self):
self .rule_router = RuleBasedRouter()
self .semantic_router = SemanticRouter()
self .metrics = RouterMetrics()
# コスト定数(USD/1M tokens)
self ._costs = {
"gemini-2.5-flash" : { "in" : 0.15 , "out" : 0.60 },
"gemini-2.5-pro" : { "in" : 1.25 , "out" : 10.00 },
}
def _update_metrics (self, model: str , usage):
"""使用量とコストをメトリクスに記録"""
self .metrics.total_requests += 1
if "flash" in model:
self .metrics.flash_count += 1
elif "pro" in model:
self .metrics.pro_count += 1
if usage:
in_tok = getattr (usage, "prompt_token_count" , 0 ) or 0
out_tok = getattr (usage, "candidates_token_count" , 0 ) or 0
self .metrics.total_tokens_in += in_tok
self .metrics.total_tokens_out += out_tok
# 実際コスト計算
cost_cfg = self ._costs.get(model, self ._costs[ "gemini-2.5-pro" ])
actual_cost = (in_tok * cost_cfg[ "in" ] + out_tok * cost_cfg[ "out" ]) / 1_000_000
self .metrics.estimated_cost_usd += actual_cost
# 仮に全部 Pro で処理した場合のコスト(節約額計算用)
pro_cfg = self ._costs[ "gemini-2.5-pro" ]
pro_cost = (in_tok * pro_cfg[ "in" ] + out_tok * pro_cfg[ "out" ]) / 1_000_000
self .metrics.estimated_savings_usd += max ( 0 , pro_cost - actual_cost)
def query (
self,
prompt: str ,
system_instruction: Optional[ str ] = None ,
force_model: Optional[ str ] = None ,
user_tier: str = "free" , # "free" | "pro" | "premium"
) -> dict :
"""
クエリをルーティングして Gemini API を呼び出す。
Args:
prompt: ユーザーのクエリテキスト
system_instruction: システムプロンプト(オプション)
force_model: モデルを強制指定(デバッグ用)
user_tier: ユーザープラン(プレミアムは常に高品質モデルを使用)
Returns:
{"text": str, "model": str, "route": RouteResult, "latency_ms": float}
"""
start_time = time.perf_counter()
# プレミアムユーザーは常に最高モデルを使用
if user_tier == "premium" :
route = RouteResult(
model_name = "gemini-2.5-pro" ,
route_method = "user_tier" ,
confidence = 1.0 ,
reasoning = "Premium user always gets top model" ,
estimated_cost_ratio = 1.0 ,
)
elif force_model:
route = RouteResult(
model_name = force_model,
route_method = "forced" ,
confidence = 1.0 ,
reasoning = "Model explicitly forced" ,
estimated_cost_ratio = 0.5 ,
)
else :
# Stage 1: ルールベースルーティング
route = self .rule_router.route(prompt)
# Stage 2: セマンティックルーティング(Stage 1 が決定できなかった場合)
if route is None :
route = self .semantic_router.route(prompt)
model_name = self . MODEL_ALIASES .get(route.model_name, route.model_name)
# Gemini API 呼び出し
try :
gen_config = {
"temperature" : 0.7 ,
"max_output_tokens" : 4096 ,
}
if system_instruction:
model = genai.GenerativeModel(
model_name,
system_instruction = system_instruction,
generation_config = gen_config,
)
else :
model = genai.GenerativeModel(model_name, generation_config = gen_config)
response = model.generate_content(prompt)
latency_ms = (time.perf_counter() - start_time) * 1000
# メトリクス更新
self ._update_metrics(model_name, response.usage_metadata)
return {
"text" : response.text,
"model" : model_name,
"route" : route,
"latency_ms" : round (latency_ms, 2 ),
}
except Exception as e:
# フォールバック: エラー時は Pro にアップグレードして再試行
logger.warning( f "⚠️ { model_name } failed: { e } . Falling back to gemini-2.5-pro" )
model = genai.GenerativeModel( "gemini-2.5-pro" )
response = model.generate_content(prompt)
latency_ms = (time.perf_counter() - start_time) * 1000
self ._update_metrics( "gemini-2.5-pro" , response.usage_metadata)
return {
"text" : response.text,
"model" : "gemini-2.5-pro (fallback)" ,
"route" : route,
"latency_ms" : round (latency_ms, 2 ),
}
# =====================
# 使用例
# =====================
if __name__ == "__main__" :
router = GeminiSmartRouter()
test_queries = [
( "このレビューはポジティブですか?「最高の商品でした!」" , "free" ),
( "Python のデコレーターパターンを実装して説明してください" , "free" ),
( "ゼロから分散トランザクションシステムを設計してください" , "premium" ),
]
for query, tier in test_queries:
result = router.query(query, user_tier = tier)
print ( f " \n 📝 Query : { query[: 50 ] } ..." )
print ( f " Model : { result[ 'model' ] } " )
print ( f " Method : { result[ 'route' ].route_method } " )
print ( f " Conf. : { result[ 'route' ].confidence :.2f } " )
print ( f " Time : { result[ 'latency_ms' ] } ms" )
print ( f " Answer : { result[ 'text' ][: 100 ] } ..." )
# メトリクスサマリーを表示
router.metrics.log_summary()
期待する出力例:
📝 Query : このレビューはポジティブですか?「最高の商品でした!」...
Model : gemini-2.5-flash
Method : rule_based
Conf. : 0.95
Time : 342.5ms
Answer : はい、このレビューは明らかにポジティブです。...
📝 Query : Python のデコレーターパターンを実装して説明してください...
Model : gemini-2.5-pro
Method : semantic
Conf. : 0.82
Time : 1243.1ms
Answer : Python のデコレーターは...
📊 Router Metrics Summary
Total requests : 3
Flash ratio : 33.3% (1 件)
Pro ratio : 66.7% (2 件)
Est. Cost : $0.0024
Est. Savings : $0.0089
TypeScript 実装:Next.js / Cloudflare Workers 対応版
バックエンドに Node.js / Next.js / Cloudflare Workers を使っている場合の TypeScript 実装です。私の壁紙アプリのバックエンドはこの構成で動いており、Edge から低レイテンシで応答できるのが利点です。
import { GoogleGenerativeAI, EmbedContentRequest } from "@google/generative-ai" ;
const genAI = new GoogleGenerativeAI (process.env. GEMINI_API_KEY ! );
// =====================
// 型定義
// =====================
interface RouteResult {
modelName : string ;
routeMethod : "rule_based" | "semantic" | "user_tier" | "forced" ;
confidence : number ;
reasoning : string ;
}
interface QueryResult {
text : string ;
model : string ;
route : RouteResult ;
latencyMs : number ;
}
// =====================
// ルールベースルーター(TypeScript 版)
// =====================
const SIMPLE_PATTERNS = [
/ ^ . {1,40} [??] $ / ,
/(翻訳 | translate | 要約 | summarize in one | 感情分析 | sentiment | classify)/ i ,
/ ^ (yes | no | はい | いいえ)/ i ,
];
const COMPLEX_PATTERNS = [
/(アーキテクチャ | architecture | 設計書 | design document)/ i ,
/(証明 | prove that | 数学的 | mathematically)/ i ,
/(包括的 | comprehensive | 詳細な分析 | detailed analysis)/ i ,
/(ゼロから | from scratch | 完全実装 | full implementation)/ i ,
];
function ruleBasedRoute ( query : string ) : RouteResult | null {
for ( const pattern of SIMPLE_PATTERNS ) {
if (pattern. test (query)) {
return {
modelName: "gemini-2.5-flash" ,
routeMethod: "rule_based" ,
confidence: 0.92 ,
reasoning: `Matched simple pattern: ${ pattern }` ,
};
}
}
for ( const pattern of COMPLEX_PATTERNS ) {
if (pattern. test (query)) {
return {
modelName: "gemini-2.5-pro" ,
routeMethod: "rule_based" ,
confidence: 0.90 ,
reasoning: `Matched complex pattern: ${ pattern }` ,
};
}
}
// 短いクエリは Flash
if (query. split ( / \s + / ). length < 8 ) {
return {
modelName: "gemini-2.5-flash" ,
routeMethod: "rule_based" ,
confidence: 0.72 ,
reasoning: "Short query heuristic" ,
};
}
return null ;
}
// =====================
// セマンティックルーター(TypeScript 版)
// =====================
async function getEmbedding ( text : string ) : Promise < number []> {
const embeddingModel = genAI. getGenerativeModel ({ model: "text-embedding-004" });
const result = await embeddingModel. embedContent ({
content: { parts: [{ text }], role: "user" },
taskType: "SEMANTIC_SIMILARITY" as any ,
} as EmbedContentRequest );
return result.embedding.values;
}
function cosineSimilarity ( a : number [], b : number []) : number {
let dot = 0 , normA = 0 , normB = 0 ;
for ( let i = 0 ; i < a. length ; i ++ ) {
dot += a[i] * b[i];
normA += a[i] * a[i];
normB += b[i] * b[i];
}
return dot / (Math. sqrt (normA) * Math. sqrt (normB));
}
// キャッシュ(起動時に初期化)
let routeEmbeddingsCache : Record < string , number []> | null = null ;
const ROUTE_CENTROIDS : Record < string , string []> = {
"gemini-2.5-flash" : [
"このテキストを英語に翻訳して" ,
"感情分析してください" ,
"一文で要約して" ,
"カテゴリを分類して" ,
],
"gemini-2.5-pro" : [
"このコードをデバッグして" ,
"マーケティング戦略を作成して" ,
"データを分析してトレンドを教えて" ,
"API を設計してください" ,
],
};
async function initSemanticRouter () : Promise < Record < string , number []>> {
if (routeEmbeddingsCache) return routeEmbeddingsCache;
const cache : Record < string , number []> = {};
for ( const [ route , examples ] of Object. entries ( ROUTE_CENTROIDS )) {
const embeddings = await Promise . all (examples. map (getEmbedding));
// 平均埋め込みベクトルを計算
const centroid = embeddings[ 0 ]. map (( _ , i ) =>
embeddings. reduce (( sum , emb ) => sum + emb[i], 0 ) / embeddings. length
);
cache[route] = centroid;
}
routeEmbeddingsCache = cache;
return cache;
}
async function semanticRoute ( query : string ) : Promise < RouteResult > {
const routeEmbeddings = await initSemanticRouter ();
const queryEmb = await getEmbedding (query);
let bestRoute = "gemini-2.5-pro" ;
let bestScore = - 1 ;
for ( const [ route , routeEmb ] of Object. entries (routeEmbeddings)) {
const sim = cosineSimilarity (queryEmb, routeEmb);
if (sim > bestScore) {
bestScore = sim;
bestRoute = route;
}
}
return {
modelName: bestRoute,
routeMethod: "semantic" ,
confidence: bestScore,
reasoning: `Highest semantic similarity: ${ bestScore . toFixed ( 3 ) }` ,
};
}
// =====================
// メインルーター関数
// =====================
export async function smartQuery (
prompt : string ,
options : {
systemInstruction ?: string ;
userTier ?: "free" | "pro" | "premium" ;
forceModel ?: string ;
} = {}
) : Promise < QueryResult > {
const start = performance. now ();
const { systemInstruction , userTier = "free" , forceModel } = options;
// ルート決定
let route : RouteResult ;
if (userTier === "premium" ) {
route = { modelName: "gemini-2.5-pro" , routeMethod: "user_tier" , confidence: 1.0 , reasoning: "Premium tier" };
} else if (forceModel) {
route = { modelName: forceModel, routeMethod: "forced" , confidence: 1.0 , reasoning: "Forced" };
} else {
route = ruleBasedRoute (prompt) ?? await semanticRoute (prompt);
}
// API 呼び出し
try {
const model = genAI. getGenerativeModel ({
model: route.modelName,
systemInstruction: systemInstruction,
generationConfig: { temperature: 0.7 , maxOutputTokens: 4096 },
});
const response = await model. generateContent (prompt);
const latencyMs = performance. now () - start;
return {
text: response.response. text (),
model: route.modelName,
route,
latencyMs: Math. round (latencyMs),
};
} catch (error) {
// フォールバック
console. warn ( `[SmartRouter] ${ route . modelName } failed, falling back to gemini-2.5-pro` );
const fallbackModel = genAI. getGenerativeModel ({ model: "gemini-2.5-pro" });
const response = await fallbackModel. generateContent (prompt);
return {
text: response.response. text (),
model: "gemini-2.5-pro (fallback)" ,
route,
latencyMs: Math. round (performance. now () - start),
};
}
}
// =====================
// Next.js API Route での利用例
// =====================
// app/api/chat/route.ts
//
// import { smartQuery } from "@/lib/smart-router";
//
// export async function POST(req: Request) {
// const { message, userTier } = await req.json();
// const result = await smartQuery(message, { userTier });
// return Response.json({ text: result.text, model: result.model });
// }
本番運用のモニタリングとアラート
Cloudflare Workers KV を使ったコスト追跡
// Edge 環境でのコスト追跡(Cloudflare Workers KV)
async function trackUsage (
env : { COST_KV : KVNamespace },
model : string ,
inputTokens : number ,
outputTokens : number
) : Promise < void > {
const today = new Date (). toISOString (). split ( "T" )[ 0 ]; // YYYY-MM-DD
const key = `usage:${ today }` ;
const existing = await env. COST_KV . get (key, "json" ) as any || {
flash: { in: 0 , out: 0 },
pro: { in: 0 , out: 0 },
total_usd: 0 ,
};
const modelKey = model. includes ( "flash" ) ? "flash" : "pro" ;
existing[modelKey].in += inputTokens;
existing[modelKey].out += outputTokens;
// コスト計算
const rates = {
flash: { in: 0.15 , out: 0.60 },
pro: { in: 1.25 , out: 10.00 },
};
existing. total_usd = (
existing.flash.in * rates.flash.in +
existing.flash.out * rates.flash.out +
existing.pro.in * rates.pro.in +
existing.pro.out * rates.pro.out
) / 1_000_000 ;
await env. COST_KV . put (key, JSON . stringify (existing), {
expirationTtl: 60 * 60 * 24 * 30 , // 30 日保持
});
}
デイリーコストアラートの設定
日次のコストが閾値を超えた場合にアラートを送信する仕組みは、本番運用で必須です。KV に記録したコストデータを cron で定期チェックし、Slack Webhook へ通知するパターンが手軽に実装できます。
私のアプリ事業では、AdMob の収益最適化とサーバー費用の監視を併走させていた経験から、しきい値は「日次コスト > 月予算 ÷ 30 × 1.3」あたりに設定するのがおすすめです。1.3 倍のバッファは、急なトラフィック増を即アラートにせず、3 日続いたら警告にするためのマージンです。
コスト最適化のベースとなるモデル選択については、Gemini API モデル選択ガイド も合わせてご参照ください。
コスト削減効果のシミュレーション
実際のプロダクションで 100 万リクエスト/月を処理する場合のコスト比較です(1 リクエストあたり平均 500 input tokens / 300 output tokens 想定)。
シナリオ A: すべて gemini-2.5-pro で処理
Input: 500M tokens × $1.25/1M = $625
Output: 300M tokens × $10.00/1M = $3,000
合計: $3,625/月
シナリオ B: セマンティックルーター(Flash 60% / Pro 35% / Fail 5%)
Flash Input: 300M × $0.15/1M = $45
Flash Output: 180M × $0.60/1M = $108
Pro Input: 175M × $1.25/1M = $218.75
Pro Output: 105M × $10.00/1M = $1,050
合計: $1,421.75/月
節約額: $2,203.25/月(約 61% 削減)
さらに、コンテキストキャッシングと組み合わせることで追加の削減が可能です。詳細は Gemini API コンテキストキャッシング最適化ガイド をご覧ください。
公式ドキュメントには書かれていない、運用で気づいた 7 つの実装インサイト
ここからは私が実際にアプリのバックエンドへ導入する過程でつまずいたり、後から修正した「公式ドキュメントには書かれていない」運用上の知見をまとめます。設計段階で踏まないでほしい落とし穴ばかりです。
1. Embedding コスト自体が無視できなくなる
セマンティックルーティングでは、すべてのクエリに embedding API コールが発生します。text-embedding-004 は $0.00001/1K tokens と非常に安価ですが、ルーティング自体のコストを見落とすと本末転倒になります。私が経験した数字で言うと、Stage 1 を充実させずに 100% を Embedding 経由にしていた時期は、ルーティングコストだけで月 $40 程度かかっていました。
対処は Stage 1 のルールベースフィルターを丁寧に育てることです。私の本番では、過去 30 日分のクエリログから「ルールベースで確実に判別できる割合」を測定し、70〜80% を Stage 1 で解決するよう設計しました。
2. 低信頼度ルーティングは「上位モデルへ寄せる」が安全側
セマンティック類似度が低い(0.65 未満)場合に誤ったモデルを選ぶと、ユーザー体験が悪化します。これは本番ロールアウトの初週に実際に起きました。Flash で処理されたクエリのうち、信頼度 0.6 前後だったものはユーザーから「回答が浅い」というフィードバックが数件入りました。
if route.confidence < 0.70 and route.route_method == "semantic" :
route.model_name = "gemini-2.5-pro" # 低信頼時は安全側へ
数行のフォールバックを足すだけで、コストはほとんど上がらず(全体の数% にしか発動しないため)、品質クレームが急減します。
3. ルート embedding は陳腐化します。月 1 回の再生成を推奨
サービスの内容が変化するにつれて、初期のサンプルクエリがユーザーの実際のクエリ分布と乖離していきます。私の壁紙アプリでは、3 ヶ月前のサンプルクエリを使い続けていた結果、ユーザーの主要トピックが「壁紙の選び方」から「設定の自動化」にシフトしていることに気づかず、Pro 行きの比率が想定より 15% 高くなっていました。
月 1 回、実際のクエリログからクラスタリングで新しいサンプルを生成し、ルート embedding を更新する運用を組み込むのをおすすめします。私の場合は cron で月初に過去 30 日のクエリを抽出し、簡易な k-means で各ルートに代表クエリを再選出しています。
4. システムプロンプトの長さがルーティング判定に影響する
これは意外と知られていない点です。Embedding API に渡すテキストにシステムプロンプトを含めるか含めないかでルーティング結果が変わります。長いシステムプロンプトを含めると、それ自体が「複雑なタスク」として認識され、Pro 行きの比率が上がります。
私の本番では、ユーザー入力のみを embedding に渡し、システムプロンプトは API 呼び出し時にだけ付与する設計にしています。これだけで Flash の比率が 8% ほど改善しました。
5. リトライ時のモデルアップグレードはコスト計算から漏れがち
エラー時に Pro へフォールバックするロジックは安全網として有効ですが、メトリクスでは「Flash として記録されたまま Pro のコストが発生」しているケースが頻発します。
対処として、_update_metrics 内で「最終的に成功した呼び出しのモデル」を必ず記録するよう、try/except ブロックの構造を見直すべきです。今回のコードでは fallback 経路でも _update_metrics("gemini-2.5-pro", ...) を呼ぶようにしてあります。
6. 多言語対応は「ルートサンプルも多言語にする」が肝
text-embedding-004 は多言語対応していますが、Flash ルートのサンプルクエリを日本語のみで作っていた頃は、英語の「Sentiment analysis please」を Pro へ振り分けてしまっていました。コサイン類似度は計算できるものの、言語のクラスタが優先されてしまうためです。
私は日英の両言語でサンプルを 4 件ずつ用意し、centroid を平均化する形に変更しました。これで日英混在クエリでも一貫した振り分けができるようになりました。
7. API キーを Flash 用と Pro 用に分けると課金トラブルが減る
これは運用上のテクニックです。Google AI Studio で Flash 用と Pro 用に API キーを分け、それぞれに別の Quota / 課金プロジェクトをひもづけておくと、誤って想定外のモデルが大量に呼ばれた際の被害を最小化できます。
私自身、ローンチ初日に Pro 行きのフォールバックが想定以上に発火し、想定の 3 倍のコストがかかった経験があります。キー分離をしていれば「Pro 側の Quota が上限に達して止まる」というセーフティが効きます。
A/B テストと段階的ロールアウト
セマンティックルーターは「一度作って終わり」ではなく、本番のクエリ分布に合わせて継続的に改善していくものです。私が採用している段階的ロールアウトの手順を共有します。
ステップ 1: シャドウモード(1 週間)
新ルーターをデプロイし、判定結果はログに記録するだけで実際の振り分けには使わず、全クエリは Pro で処理し続けます。「もしルーターが判定していたら何が起きたか」を観察する期間です。
ステップ 2: 10% トラフィックでの試験運用(1〜2 週間)
ユーザー ID のハッシュで 10% のリクエストをルーター経由に切り替え、残りの 90% は Pro 固定のままにします。両群の応答品質を以下の 3 指標で比較します。
ユーザーの再質問率(同じセッション内で 2 回以上同じ意図のクエリを送る割合)
ネガティブフィードバック率(👎 ボタンや低評価レビュー)
平均レスポンスタイム
ステップ 3: 50% 移行(2 週間)
10% 群で品質劣化が見られなければ、50% に拡大して長期影響を観察します。私の場合はこの段階で「ネガティブフィードバックは 0.3% → 0.4% へごく僅かに増えたものの、コストは 58% 削減」という結果が得られ、本番投入を決断しました。
ステップ 4: 100% 移行と継続観察
100% にしたあとも、週次でルーティング比率のドリフトをダッシュボードで監視します。Flash 比率が急に下がった週は、ルート embedding の陳腐化を疑って再生成します。
このプロセスは、私が AdMob の収益最適化を 12 年かけて続けてきた経験から学んだ「重要な意思決定は段階的に・観察できる形で進める」という原則そのものです。
導入を検討するときの補足ノート
セマンティックルーティングの初期化にかかる時間は、サンプルクエリの数にもよりますが、Flash / Pro 各 4〜8 件であれば起動時の初期化は 3〜8 秒程度です。Web サービスでは起動時に非同期で初期化しておくか、Cloudflare Workers KV に embedding をキャッシュしておくことをおすすめします。
ユーザーがモデル品質の差で不満を感じるかについては、適切に設計されたルーターであれば、ユーザーの 95% 以上が差を認識しないはずです。単純なタスクを Flash で処理しても出力品質に差はありません。問題が発生するのは、ルーターが誤って複雑なクエリを Flash に振り分けた場合です。そのため、「不確かな場合は上位モデルへ」というフォールバック原則が重要です。
特定のユーザーに常に上位モデルを使いたい場合は、user_tier パラメーターを使ってプレミアムユーザーへのルーティングをオーバーライドできます。コードの user_tier == "premium" の分岐がそれに対応しています。価格設計については Gemini API コスト完全ガイド も参考になります。
締めくくり
セマンティックルーターの本質は「すべてのクエリに最強のモデルを当てない」というシンプルな発想です。2 段階ルーティング(ルールベース + セマンティック)により、ルーティングコスト自体を最小化しながら高精度なモデル選択が実現できます。適切な振り分けにより、典型的なプロダクション環境では API コストを 50〜70% 削減できます。フォールバック設計と信頼度閾値を設けることで、品質の安全網も確保されます。
私自身、2014年から個人で 250 本超のアプリを運営してきた経験から痛感するのは、初期の設計が運用 1 年後の損益に直結することです。Gemini API のような従量課金は、設計初期の数行のフォールバックや、月 1 回の embedding 再生成といった「小さな運用習慣」がコスト削減と品質維持の差を生みます。
まずは小規模に A/B テストしながら、ご自身のユースケースに最適化されたルーターを少しずつ育てていただければと思います。読み終えたあと、まず Stage 1 のルールベースを 5 パターン書いてみてください。それだけで体感できるコスト削減があるはずです。