エージェントを動かし始めて半年が経った頃、私は月末のAPI請求を眺めて「これは設計を入れ替える時期だ」と感じました。2014年から個人開発でアプリを出し続けてきて、累計5,000万DLに届いたタイミングで AdMob 収益が伸び、それと並走させる形で Antigravity のエージェントを業務に組み込んでいました。エージェントは仕事を確実に楽にしてくれていましたが、その「楽さ」のコストが、収益から差し引かれる固定費として体感できる規模になってきたのです。
ここで腹をくくって、プロンプトキャッシュとコンテキスト戦略を本格的に組み直しました。結果として、同じ機能を維持したまま月額API費用が 60〜80% 削れました。具体的にはエージェント1回あたりの入力トークンが平均で 12,400 → 4,800 トークンに、月次総コストは $480 → $112 まで落ちています。以下、その設計と落とし穴を順に書きます。
なぜプロンプトキャッシュ単体ではコストが下がりきらないのか
プロンプトキャッシュは公式に推奨される最初の手です。私も最初はこれだけで十分だと思っていました。ところが、実測してみるとキャッシュヒット率は 30〜40% にしか乗らず、期待した費用削減が出ませんでした。
理由はシンプルで、エージェントに渡すコンテキストの構造が「キャッシュしやすい部分」と「毎回変わる部分」を分けずに混ぜていたからです。システムプロンプトと参照ドキュメントは固定なのに、その間に動的なユーザー入力やツール実行結果を挿入していました。プロンプトキャッシュは前方一致でしか効きません。1文字でも前にズレが入った瞬間、それ以降は全部キャッシュミスになります。
そこで、コンテキスト自体を「キャッシュレイヤー」として設計し直す必要がありました。
3層キャッシュアーキテクチャ
私が現在運用している構成は、TTL とヒット率の特性に応じて3層に分かれます。
| 層 | 内容 | TTL | 期待ヒット率 |
| Layer 1 | システムプロンプト | 24 時間 | 99% |
| Layer 2 | 参照ドキュメント | 6 時間 | 85% |
| Layer 3 | 直近の対話履歴 | 5 分 | 60% |
ポイントは、層の順序を厳格に守ることです。Layer 1 を必ず最初に置き、その後に Layer 2、最後に Layer 3 を積みます。動的な部分(ユーザー入力・ツール結果)は Layer 3 の中、それも後半に集めます。
実装は TypeScript で次のように組んでいます。
import { Anthropic } from "@anthropic-ai/sdk";
interface CacheLayer {
type: "system" | "reference" | "history";
content: string;
cacheControl: { type: "ephemeral" } | undefined;
}
const buildContext = (
systemPrompt: string,
references: string[],
history: Array<{ role: string; content: string }>,
userInput: string,
): CacheLayer[] => {
const layers: CacheLayer[] = [];
// Layer 1: システム(24h想定の長期キャッシュ)
layers.push({
type: "system",
content: systemPrompt,
cacheControl: { type: "ephemeral" },
});
// Layer 2: 参照ドキュメント(6h想定の中期キャッシュ)
for (const ref of references) {
layers.push({
type: "reference",
content: ref,
cacheControl: { type: "ephemeral" },
});
}
// Layer 3: 履歴と動的入力(短期キャッシュ + 非キャッシュ)
for (let i = 0; i < history.length; i++) {
const isRecent = i >= history.length - 2;
layers.push({
type: "history",
content: `${history[i].role}: ${history[i].content}`,
// 直近2件はキャッシュしない(変動が激しいため)
cacheControl: isRecent ? undefined : { type: "ephemeral" },
});
}
// 最終ユーザー入力はキャッシュしない
layers.push({
type: "history",
content: `user: ${userInput}`,
cacheControl: undefined,
});
return layers;
};
この構造に切り替えた直後、キャッシュヒット率は 38% → 78% に跳ね上がりました。コードの本質は「変わらないものを前に、変わるものを後ろに」というルールだけです。当たり前のことなのに、最初は気付かずにシステムプロンプトの中にタイムスタンプを混ぜていたりして、それだけで全層のキャッシュが死んでいました。
コンテキスト圧縮 — 入力トークンを 60% 削る具体策
キャッシュヒット率が上がっても、ヒットしない 20% 部分は依然として課金されます。ここをさらに削るために、コンテキスト圧縮を入れます。
私が使っている手法は3つです。
- 要約スナップショット: 過去 10 ターン以上の履歴を 200 トークン程度の要約に置き換える
- 意味的重複除去: 参照ドキュメント内の同じトピックを述べる箇所を抽出して 1 つにまとめる
- 構造化サマリ: 履歴を「決定事項」「未解決事項」「ユーザー嗜好」の3カテゴリに集約する
実装の中で最も効果があったのは1番目の要約スナップショットです。Antigravity のエージェントは長時間対話が前提なので、何もしないと履歴は無制限に膨らみます。私は以下のしきい値で要約を発火させています。
const MAX_HISTORY_TOKENS = 6000;
const SUMMARY_TARGET_TOKENS = 200;
const compressHistoryIfNeeded = async (
history: Array<{ role: string; content: string }>,
estimateTokens: (text: string) => number,
summarize: (text: string) => Promise<string>,
): Promise<Array<{ role: string; content: string }>> => {
const total = history.reduce((sum, h) => sum + estimateTokens(h.content), 0);
if (total < MAX_HISTORY_TOKENS) return history;
// 直近 4 ターンは保持し、それ以前を要約に置き換える
const recent = history.slice(-4);
const old = history.slice(0, -4);
const oldText = old.map((h) => `${h.role}: ${h.content}`).join("\n");
const summary = await summarize(
`次の対話履歴を ${SUMMARY_TARGET_TOKENS} トークン以内に要約してください。\n` +
`決定事項・未解決事項・ユーザー嗜好の3点に整理:\n\n${oldText}`,
);
return [
{ role: "system", content: `[これまでの対話要約]\n${summary}` },
...recent,
];
};
要約発火のしきい値(6,000 トークン)は、実測で「これより上に積むとキャッシュヒット率が落ちる境界」を探って決めました。最初は 10,000 にしていましたが、要約処理自体のコストと、キャッシュミスのコストを天秤にかけて 6,000 が損益分岐になりました。この数字はモデル・用途で変わるので、自分の運用データで都度調整するべきです。
TTL 設計とキャッシュ無効化の落とし穴
3層キャッシュの実装で一番ハマったのが TTL の扱いでした。Antigravity の ephemeral キャッシュは、表向きには「5 分間有効」と説明されていますが、実際には連続アクセスがあると延命されます。
私は最初これを誤解していて、システムプロンプトを 24 時間キャッシュさせる前提で組んでいました。深夜に 6 時間アクセスが空いた直後、翌朝最初のリクエストで全層キャッシュミスが発生し、入力トークンが普段の 3 倍以上に跳ねたことがあります。
回避策は「ウォームアップリクエスト」です。エージェントを使う直前に、軽量なリクエストを 1 回だけ投げてキャッシュを温め直します。
const warmupCache = async (
systemPrompt: string,
references: string[],
): Promise<void> => {
// 最小限のメッセージでキャッシュ層に触れる
await anthropic.messages.create({
model: "claude-haiku-4-5-20251001",
max_tokens: 1,
system: [
{ type: "text", text: systemPrompt, cache_control: { type: "ephemeral" } },
],
messages: [
{
role: "user",
content: references.map((r) => ({
type: "text" as const,
text: r,
cache_control: { type: "ephemeral" as const },
})).concat([{ type: "text" as const, text: "OK", cache_control: undefined }]),
},
],
});
};
このウォームアップを「エージェントが本番タスクに入る前 3 秒」で挟むだけで、初回リクエストのコスト跳ねが起きなくなりました。ウォームアップ自体のコストは max_tokens=1 なので 1 円未満です。
もう一つの落とし穴は、参照ドキュメントの更新です。Layer 2 を更新したつもりが、内部のバイト列が 1 文字でも変わるとキャッシュキーが変わって全層リビルドになります。私は参照ドキュメントの更新を 6 時間に 1 回のバッチに集約し、更新後はウォームアップを必ず走らせる運用にしています。
AdMob 収益と並走させたときの損益分岐
ここからは数字の話です。個人開発でアプリ事業を回している身としては、エージェントの API コストは AdMob 収益から差し引く固定費として見えます。
| 項目 | 改善前 | 改善後 |
| 1リクエストあたり入力トークン | 12,400 | 4,800 |
| 1リクエストあたりコスト(日本円換算) | ¥4.8 | ¥0.6 |
| 月次総リクエスト数 | 14,000 | 14,000 |
| 月次総コスト | $480 | $112 |
| AdMob 月次収益に対する比率 | 約 38% | 約 9% |
改善前は「AdMob 収益の 38% がエージェントの API 費用に消える」状態で、これは率直に言ってビジネスとして持続可能ではありませんでした。改善後の 9% であれば、エージェントを動かして自分の作業時間を 1 日 2〜3 時間浮かせることの価値が、明確に収益を上回ります。
ここで重要なのは「ヒット率を 78% まで上げる」ことと「圧縮で 60% 削る」を組み合わせることです。どちらか片方だけだと、せいぜい 30〜40% の改善で止まります。両方を入れて初めて、月額が桁違いに下がります。
個人開発者が踏みやすい3つの罠
私が実際にハマった罠を3つ共有します。本番で踏むと数日〜数週間の請求に跳ねるタイプの罠です。
1つ目は、タイムスタンプ混入です。システムプロンプトに「現在時刻」を埋め込むと、毎回キャッシュキーが変わって Layer 1 が死にます。時刻が必要ならツール経由で取得する設計にすべきです。私はこれで 3 日間キャッシュヒット率がほぼ 0% に張り付いていたことがありました。
2つ目は、ストリーミング応答中の早期キャンセルです。ストリーミング途中で接続を切ると、それまで読み込んだトークンは課金されるのに、キャッシュは書き込まれません。私は「ユーザーが早く読めるから」とストリーミングを多用していましたが、エージェントの内部処理ではバッチ応答(非ストリーミング)の方がキャッシュ効率は良いと判断しました。
3つ目は、並列実行時のキャッシュレースコンディションです。同一プロンプトを 2 つのリクエストでほぼ同時に投げると、両方ともキャッシュミスとして処理される場合があります。並列実行する場合は、初回の 1 リクエストだけ先に走らせてキャッシュを書き、それ以降を並列化する運用が確実です。
これら3つは公式ドキュメントには書かれていませんが、本番運用すると必ずどこかで踏みます。先に対策を入れておくと請求の暴れが減ります。
状況別の推奨アプローチ
すべてのエージェントに3層キャッシュ + コンテキスト圧縮が必要なわけではありません。状況によって推奨パターンが変わります。
短時間バースト型(1 セッション数分で完結)の場合は、Layer 1 と Layer 2 だけで十分です。Layer 3 のキャッシュは TTL より短いセッションでは効果が出ません。コンテキスト圧縮も基本的に不要です。
長時間運用型(数十分〜数時間)の場合は、本記事の3層構成 + コンテキスト圧縮を全部入れます。私の手元はこのパターンが大半です。
夜間バッチ型(深夜にまとめて走らせる)の場合は、ウォームアップを必ず挟みます。深夜帯の最初のリクエストは高確率でキャッシュミスになるので、これを忘れるとバッチ全体のコストが跳ねます。
複数アプリで同じシステムプロンプトを使う場合は、Layer 1 を共有のサービスとして切り出すと、横展開でキャッシュヒット率が上がります。私は壁紙アプリと癒し系アプリで同じ運用テンプレートを使っているので、この共有が効きました。
次に試したい人へ — 最小ステップ
明日から導入するなら、まず Layer 1 だけを ephemeral キャッシュに切り替えてください。コードの変更は数行で、それだけでキャッシュヒット率が 30% → 60% に上がります。そこから 1 週間運用して数字を見て、効果が出ていれば Layer 2、Layer 3 と段階的に増やします。
最初から全部入れようとすると、どこが効いているか分からなくなります。Layer 1 だけ、Layer 2 を追加、Layer 3 を追加、と段階的に入れて、それぞれの効果を実測しながら積み上げるのが結局一番早い経路です。
個人開発でアプリ事業を回しながら、エージェントを「攻めの道具」として使い続けるには、コストを読めるようにすることが何より重要です。本記事の設計が、同じ立場でエージェントを本気で運用したい方の参考になれば幸いです。お読みいただきありがとうございました。