Vertex AI Context Cachingで LLMアプリのコストを73%削減した話
はじめに
LLMを使ったアプリケーションを運用していると、APIコストが気になってきます。特に、毎回同じシステムプロンプトを送信している場合、そのトークンは完全にムダです。
この記事では、Google Cloud Vertex AIのContext Caching機能を使って、入力トークンコストを約73%削減した実験結果を共有します。
対象読者
- LLMアプリのコスト最適化に興味がある方
- Vertex AI(Gemini)を使っている方
- Next.js + Vercelでバックエンドを構築している方
TL;DR
- Context Cachingを使うと、繰り返し送信するプロンプトをキャッシュできる
- Gemini 2.0 Flashで75%割引、Gemini 2.5で90%割引
- 実測でキャッシュヒット率97.6%、コスト73%削減を達成
- 初回リクエスト遅延はバックグラウンドキャッシュ作成で対策可能
課題:毎回同じプロンプトを送るムダ
私が開発している「Silver Tongue」というゲームでは、AIがクレーマー役を演じます。
毎回のAPIリクエスト構成:
| 項目 | トークン数 | 性質 |
|---|---|---|
| SYSTEM_PROMPT | 約2300 | 毎回同じ |
| 会話履歴(最大8件) | 約50 | 変動 |
| 現在の怒りレベル | 約10 | 変動 |
SYSTEM_PROMPTには、キャラクターの性格、バックストーリー、セキュリティルールなどが含まれています。
問題点:
- 全リクエストで同じSYSTEM_PROMPTを送信
- 毎回2300トークン分の課金が発生
- 実際に変動するのは会話部分の約50トークンだけ
解決策:Vertex AI Context Caching
Context Cachingとは
GoogleのVertex AIには、繰り返し送信するコンテンツをキャッシュする機能があります。
| 種類 | 説明 | 割引率 |
|---|---|---|
| 暗黙的キャッシング | 自動で適用(Gemini 2.5以降) | 90% |
| 明示的キャッシング | APIで明示的に管理 | 75-90% |
今回は明示的キャッシングを使用しました(Gemini 2.0 Flash対応)。
料金体系
通常の入力トークン: $0.075 / 1M tokens
キャッシュヒット: $0.01875 / 1M tokens(75%割引)
実験1:Before(Context Caching なし)
まず、現状のトークン使用量を計測しました。
計測条件
- モデル: gemini-2.0-flash-001
- リージョン: us-central1
- 実行回数: 20回
- テストシナリオ: 固定の会話パターン
結果
| 指標 | 値 |
|---|---|
| 平均入力トークン | 2282.80 |
| 平均出力トークン | 104.05 |
| キャッシュヒット率 | 0% |
| 平均レスポンス時間 | 1949.95ms |
重要な発見: SYSTEM_PROMPTは約2283トークンで、Context Cachingの最小要件(2048トークン)を満たしていました。
実装方法
1. キャッシュの作成
@google-cloud/vertexaiパッケージを使用します。
import { VertexAI, CachedContent } from '@google-cloud/vertexai';
const vertexAI = new VertexAI({
project: 'your-project-id',
location: 'us-central1',
});
// キャッシュを作成
const cache = await vertexAI.preview.cachedContents.create({
displayName: 'my-system-prompt-cache',
model: 'projects/.../models/gemini-2.0-flash-001',
contents: [
{
role: 'user',
parts: [{ text: SYSTEM_PROMPT }],
},
],
ttl: '3600s', // 1時間
});
2. キャッシュを使った応答生成
// キャッシュからモデルを取得
const model = vertexAI.preview.getGenerativeModelFromCachedContent(cache, {
model: 'gemini-2.0-flash-001',
generationConfig: {
temperature: 0.8,
maxOutputTokens: 300,
responseMimeType: 'application/json',
},
});
// 会話部分のみ送信(SYSTEM_PROMPTは不要)
const result = await model.generateContent(conversationPrompt);
3. 既存キャッシュの取得
キャッシュは永続的ではないため、リクエストごとに存在確認が必要です。
async function getOrCreateCache(systemPrompt: string): Promise<CachedContent | null> {
// 既存のキャッシュを検索
const listResponse = await vertexAI.preview.cachedContents.list();
const existingCache = listResponse.cachedContents?.find(
(cache) => cache.displayName === CACHE_DISPLAY_NAME
);
if (existingCache) {
return existingCache;
}
// 新規キャッシュを作成
return await vertexAI.preview.cachedContents.create({
displayName: CACHE_DISPLAY_NAME,
model: `projects/${PROJECT_ID}/locations/${LOCATION}/publishers/google/models/${MODEL_ID}`,
contents: [{ role: 'user', parts: [{ text: systemPrompt }] }],
ttl: '3600s',
});
}
実験2:After(Context Caching あり・初期実装)
Context Cachingを実装して再計測しました。
結果
| 指標 | 値 |
|---|---|
| 平均入力トークン | 2282.80 |
| 平均出力トークン | 100.85 |
| 平均キャッシュトークン | 2227.00 |
| キャッシュヒット率 | 97.6% |
| 平均レスポンス時間 | 1733.15ms |
キャッシュが効いています!しかし...
問題発生:初回リクエストがタイムアウト
Run 1/5: FAILED - HTTP 504: Gateway Timeout
Run 2/5: 3874ms, tokens: 2286/99, cached: 2227 [CACHE] - OK
Run 3/5: 2113ms, tokens: 2282/90, cached: 2227 [CACHE] - OK
原因: キャッシュが存在しない初回リクエストで、キャッシュ作成に時間がかかり60秒でタイムアウトしました。
初回リクエスト遅延の対策
解決策:高速フォールバック + バックグラウンド作成
リクエスト受信時のフロー:
-
キャッシュ検索(タイムアウト2秒)
- 見つかった場合 → キャッシュ使用してAI応答
-
見つからない/タイムアウトの場合 ↓
- 通常モードでAI応答(即座にレスポンス)
- バックグラウンドでキャッシュ作成(次回以降に適用)
実装コード
const CACHE_LOOKUP_TIMEOUT_MS = 2000; // 2秒
// タイムアウト付きでキャッシュ検索
async function getExistingCache(): Promise<CachedContent | null> {
const timeoutPromise = new Promise<null>((resolve) =>
setTimeout(() => resolve(null), CACHE_LOOKUP_TIMEOUT_MS)
);
const listPromise = (async () => {
const listResponse = await vertexAI.preview.cachedContents.list();
return listResponse.cachedContents?.find(
(cache) => cache.displayName === CACHE_DISPLAY_NAME
) || null;
})();
return Promise.race([listPromise, timeoutPromise]);
}
// バックグラウンドでキャッシュ作成(待機しない)
function createCacheInBackground(systemPrompt: string): void {
(async () => {
try {
await vertexAI.preview.cachedContents.create({
displayName: CACHE_DISPLAY_NAME,
model: `projects/${PROJECT_ID}/locations/${LOCATION}/publishers/google/models/${MODEL_ID}`,
contents: [{ role: 'user', parts: [{ text: systemPrompt }] }],
ttl: '3600s',
});
console.log('Background cache creation successful');
} catch (error) {
console.error('Background cache creation failed:', error);
}
})();
}
メイン処理の修正
// Context Cachingを試行(高速フォールバック方式)
let result;
let usedCache = false;
// 既存キャッシュを高速検索(タイムアウト付き)
const cache = await getExistingCache();
if (cache) {
// キャッシュ使用
const cachedModel = vertexAI.preview.getGenerativeModelFromCachedContent(cache, {...});
result = await cachedModel.generateContent(conversationPrompt);
usedCache = true;
} else {
// キャッシュがない場合、バックグラウンドで作成を開始
createCacheInBackground(SYSTEM_PROMPT);
// 通常モードで即座にレスポンス
const model = vertexAI.getGenerativeModel({...});
result = await model.generateContent(fullPrompt);
}
実験3:After(対策後)
初回リクエスト遅延対策を実装して再計測しました。
結果
Run 1/10: 3168ms, tokens: 2283/95, cached: 2227 [CACHE] - OK
Run 2/10: 2358ms, tokens: 2286/107, cached: 2227 [CACHE] - OK
...
Run 10/10: 1756ms, tokens: 2280/103, cached: 2227 [CACHE] - OK
タイムアウトなし! 全リクエストが成功しました。
| 指標 | 値 |
|---|---|
| 成功率 | 100% (10/10) |
| キャッシュヒット率 | 97.6% |
| 平均レスポンス時間 | 2196.70ms |
最終結果まとめ
Before vs After 比較
| 指標 | Before | After (対策後) | 変化 |
|---|---|---|---|
| 平均入力トークン | 2282.80 | 2282.80 | 同じ |
| キャッシュトークン | 0 | 2227 | +2227 |
| キャッシュヒット率 | 0% | 97.6% | +97.6% |
| 平均レスポンス時間 | 1949ms | 1733ms | -11.1% |
| 初回リクエスト | 正常 | 正常(対策済) | - |
コスト試算(1000リクエストあたり)
| 項目 | Before | After |
|---|---|---|
| 入力トークン(通常課金) | 2,282,800 | 55,800 |
| キャッシュトークン | 0 | 2,227,000 |
| コスト | $0.171 | $0.046 |
| 削減率 | - | 約73% |
月間コスト試算例
| 月間リクエスト数 | Before | After | 削減額 |
|---|---|---|---|
| 10,000 | $1.71 | $0.46 | $1.25 |
| 100,000 | $17.10 | $4.60 | $12.50 |
| 1,000,000 | $171 | $46 | $125 |
実験の全体像
| フェーズ | 内容 | 結果 |
|---|---|---|
| 実験1 | Before計測(Caching なし) | 入力トークン2283/リクエスト |
| 実験2 | Caching実装(初期版) | キャッシュ97.6%ヒット、初回タイムアウト問題 |
| 実験3 | 対策後(高速フォールバック) | タイムアウト解消、コスト73%削減 |
注意点
1. 最小トークン数
Context Cachingには最低2048トークンが必要です。短いプロンプトでは効果がありません。
2. キャッシュTTL
キャッシュには有効期限(TTL)があります。期限切れ後は再作成が必要です。
3. ストレージ料金
キャッシュの保存には別途料金がかかります。
$1.00 / 1Mトークン / 時間
2227トークンを1時間保存すると、約$0.002(0.2円)です。
4. Serverless環境での考慮
Vercel等のServerless環境では、リクエストごとに状態がリセットされます。
そのため、毎回キャッシュの存在確認が必要です。
まとめ
Vertex AI Context Cachingを使うことで:
- キャッシュヒット率97.6% を達成
- レスポンス時間11.1%改善
- 入力トークンコスト約73%削減
ただし、初回リクエスト遅延には注意が必要です。「高速フォールバック + バックグラウンド作成」方式で対策できます:
- キャッシュ検索にタイムアウト(2秒)を設定
- キャッシュがない場合は通常モードで即座にレスポンス
- バックグラウンドでキャッシュを作成し、2回目以降に適用
システムプロンプトが2048トークン以上あるLLMアプリを運用している方は、ぜひ試してみてください。
参考リンク
補足:実験環境
| 項目 | 値 |
|---|---|
| モデル | gemini-2.0-flash-001 |
| リージョン | us-central1 |
| フレームワーク | Next.js (App Router) |
| デプロイ先 | Vercel |
| 認証 | Workload Identity Federation |
Discussion