2014 年から個人で iOS / Android アプリを 60 本以上リリースしてきましたが、Antigravity の AI エージェントを 6 アプリ並行で常時稼働させはじめてから、「単一のフォールバック」ではどうしても止まる場面が出てくることに気づきました。Gemini 3 Pro Thinking のレート制限が突発的に上がる日、Crashlytics の取り込み API が 30 分単位で詰まる日、そして決算月に AdMob 連動の最適化エージェントがコスト天井へ近づく日 — それぞれ性質が違うのに、リトライ+単一の代替モデル切替だけでしのごうとすると、必ずどこかで詰まります。
このまま運用を続けると、エージェントが落ちている時間がアプリの収益を直撃します。累計 5,000 万 DL を支えてくれているユーザーに対して、AI まわりの不安定さで広告配置や IAP オファーが歪むのは、私の責任です。そこで腰を据えて設計し直したのが、本記事で整理する 4 段階フォールバック階層 (Tier 0〜3) です。Burn-Rate SLO・コスト上限・モデル健康度の 3 軸を組み合わせて、どの階層で受け止めるかを自動判定します。
なぜ「単一フォールバック」では運用が止まるのか
最初の半年は、私も「プライマリで失敗したら 1 回だけセカンダリへ切り替える」というシンプルな構成にしていました。コードで言うと try { primary() } catch { secondary() } のような形です。これで稼げる安定性は、私の体感では月次の可用性で 99.0% 前後が限界でした。
止まる場面は 3 タイプに分かれます。1 つ目はモデル品質の漸進的劣化で、明示的なエラーは返ってこないのに「応答に微妙なズレや空回りが増える」状態。2 つ目は API 側の局所障害で、特定リージョンや特定エンドポイントが 10〜30 分単位で詰まる現象。3 つ目はコスト超過で、月末に向けて使用量が想定を超え、このままだと予算を食い切るパターン。単一フォールバックはこの 3 タイプに対して「セカンダリへ全切替」という同じ処方しか持っていないので、コスト超過時にセカンダリ(より高い同等モデル)へ逃げてしまうと事態が悪化します。
ここから学んだのは、フォールバック設計には「降格の方向性」と「降格の理由」を分離する必要があるということです。理由 (Burn-Rate, Cost Cap, Health Degradation) に応じて、移行先の階層を変える必要があります。
4 階層フォールバックの全体像 (Tier 0〜3)
私が本番運用している階層は、次の 4 段で構成されています。Tier 0 が通常運用、Tier 3 が最終防衛線です。
- Tier 0 — Primary: Antigravity 上の Gemini 3 Pro Thinking。通常時はここで動作。
- Tier 1 — Degraded: Gemini 3 Flash + プロンプト圧縮 + コンテキスト縮約。レート制限・軽度の劣化検知時。
- Tier 2 — Local Fallback: Antigravity ローカルの Gemma 4 (
gemma-4-12b 量子化版)。外部 API が完全断、または重大な劣化検知時。
- Tier 3 — Cached/Static: 過去応答のセマンティックキャッシュ+固定フォールバック応答。最終手段。
階層は「より少ない外部依存」「より低い品質」「より低いコスト」へ降りていきます。Tier 2 までは AI 推論を維持しますが、Tier 3 では推論を完全に放棄して既知の応答を返す設計です。これは「最低限のユーザー体験を返す」ことを最優先するためで、Crashlytics の毎朝のトリアージのように「無応答よりも 95% の精度で答えが出る方がマシ」という状況で機能します。
Tier 0 — Primary(Gemini 3 Pro Thinking 通常運用)
Tier 0 では、Antigravity 上の Gemini 3 Pro Thinking をフルコンテキスト・フル機能で使います。通常時はここで全リクエストの 92〜95% を処理します。重要なのは、Tier 0 の段階で「降格判定に必要なシグナルを必ず取得する」という運用ルールです。
// agent-tier0.ts — Primary 実行と健康度シグナル取得
import { AntigravityAgent, type ExecutionResult } from "@antigravity/agent";
import { healthScore } from "./health-score";
import { recordSpan } from "./observability";
export async function runTier0(prompt: string, ctx: AgentContext): Promise<ExecutionResult> {
const span = recordSpan("agent.tier0", { model: "gemini-3-pro-thinking" });
const started = Date.now();
try {
const result = await AntigravityAgent.run({
model: "gemini-3-pro-thinking",
prompt,
tools: ctx.tools,
maxOutputTokens: 4096,
reasoningEffort: "high",
});
const latencyMs = Date.now() - started;
const score = healthScore({
latencyMs,
tokensUsed: result.usage.totalTokens,
finishReason: result.finishReason,
validatorPassed: ctx.validate(result.output),
});
span.end({ ok: true, latencyMs, healthScore: score });
ctx.healthBuffer.push(score);
return result;
} catch (err) {
span.end({ ok: false, error: String(err) });
throw err;
}
}
healthBuffer には直近 100 件の健康度スコアを保持し、Tier 1 への降格判定で使います。私の運用では、Tier 0 の平均レイテンシは 3.8〜5.2 秒、健康度スコアは 0.93〜0.98 のレンジに収まることが多いです。このベースラインから外れた場合、降格判定ロジックが発動します。
Tier 1 — Degraded(モデル切替とプロンプト圧縮)
Tier 1 は「外部 API は使えるが、Tier 0 を続けると問題がある」状態の受け皿です。具体的には次の 3 条件のどれかで降格します。
- レート制限エラーが直近 10 分で 3 回以上発生した
- 健康度スコアが直近 20 件で平均 0.80 を下回った
- コスト Burn-Rate が月予算の 110% ペースで進行している
降格時はモデルを Gemini 3 Flash に切替え、プロンプト本体を 30〜40% 圧縮し、tool 呼び出し数の上限を下げます。
// agent-tier1.ts — Degraded 実行
export async function runTier1(prompt: string, ctx: AgentContext): Promise<ExecutionResult> {
const compressed = await compressPrompt(prompt, {
targetRatio: 0.65, // 35% 圧縮
preserveMarkers: ["<task>", "<constraint>"],
});
const span = recordSpan("agent.tier1", { model: "gemini-3-flash" });
try {
return await AntigravityAgent.run({
model: "gemini-3-flash",
prompt: compressed,
tools: ctx.tools.slice(0, 5), // ツール数を5個に絞る
maxOutputTokens: 2048,
reasoningEffort: "medium",
});
} finally {
ctx.metrics.increment("agent.tier1.runs");
span.end();
}
}
ここでの実装上のハマりどころは、compressPrompt が必ず冪等であることを担保する点です。Tier 1 から Tier 0 へ戻った時に「圧縮された残骸」が混ざらないように、各リクエストごとに元プロンプトから再生成する形にしました。私自身、本番運用で 1 度ここを取り違えて、Tier 0 復帰後も縮約版が使われ続けるバグを 6 時間放置した経験があります。
Tier 1 では、コストはおおむね Tier 0 の 22〜28% に落ち、レイテンシは 1.4〜2.1 秒へ短縮されます。
Tier 2 — Local Fallback(Gemma 4 ローカル推論)
Tier 2 は外部 API への依存を完全に切り離す層です。Antigravity に組み込んだ Gemma 4 の 12B 量子化版をローカル推論で動かします。降格条件は次のいずれか。
- Tier 1 のエラー率が直近 5 分で 25% を超えた
- Gemini API が 30 秒以上連続して 5xx を返している
- 健康度スコアが 0.60 を下回った(深刻な劣化)
// agent-tier2.ts — Local 推論
import { GemmaLocal } from "@antigravity/gemma-runtime";
export async function runTier2(prompt: string, ctx: AgentContext): Promise<ExecutionResult> {
const span = recordSpan("agent.tier2", { model: "gemma-4-12b-q4" });
const trimmed = await trimContextForLocal(prompt, {
maxTokens: 8192, // ローカルの実用上限
dropOptional: true,
});
try {
const result = await GemmaLocal.run({
modelPath: "/opt/gemma/gemma-4-12b-q4.gguf",
prompt: trimmed,
temperature: 0.2,
maxOutputTokens: 1024,
});
return { ...result, degraded: true };
} finally {
span.end();
}
}
Tier 2 の応答品質はタスクによって振れ幅が大きく、コード生成や複雑な推論では Tier 0 の 60〜75% 程度に落ちます。一方で、Crashlytics の毎朝のトリアージのような「決まった形式の出力をルール通りに整える」タスクでは、Tier 0 の 88〜92% 程度の精度が出ます。ここを判断材料にして、各エージェントで「Tier 2 で受け入れ可能なタスク種別」を事前に定義しておく必要があります。
私の壁紙系アプリと癒し系アプリで動かしている広告配置最適化エージェントは、Tier 2 でも eCPM 計算は十分にこなせますが、IAP オファーの動的調整は Tier 2 では精度が足りないため、Tier 2 では IAP オファー側のロジックを「直前の Tier 0 推奨を 24 時間維持」というポリシーに切替えます。
Tier 3 — Cached/Static(最終防衛線)
Tier 3 は AI 推論を完全に放棄する層です。次の 2 つの仕組みで最低限の応答を返します。
第 1 に、過去 30 日分の Tier 0 応答をセマンティックキャッシュに格納し、現在のリクエストと意味的類似度 0.92 以上の応答が見つかればそれを返します。第 2 に、各エージェントには「最終フォールバック応答」を事前定義しておき、キャッシュヒットも無い場合はそれを返します。
// agent-tier3.ts — Cached/Static
import { SemanticCache } from "@antigravity/semantic-cache";
const STATIC_FALLBACKS: Record<string, string> = {
"crashlytics-triage": JSON.stringify({
summary: "サービス縮退中。優先度高クラッシュのみ最終応答を参照してください。",
severity: "unknown",
suggested_action: "manual_review_required",
}),
"admob-placement-optimizer": JSON.stringify({
placement: "use_previous_24h_recommendation",
confidence: 0.5,
reason: "AI inference offline; falling back to last good config.",
}),
};
export async function runTier3(prompt: string, ctx: AgentContext): Promise<ExecutionResult> {
const hit = await SemanticCache.search(prompt, { threshold: 0.92, maxAgeDays: 30 });
if (hit) {
return { output: hit.response, source: "semantic-cache", degraded: true };
}
const staticResponse = STATIC_FALLBACKS[ctx.agentId];
if (staticResponse) {
return { output: staticResponse, source: "static-fallback", degraded: true };
}
return { output: "{}", source: "empty-fallback", degraded: true };
}
Tier 3 まで降りた場合は、必ずアラート (Slack + PagerDuty) を出す運用にしています。Tier 3 は「自動復旧の終着点」であって、ここで止まり続けると本物のサービス縮退になるため、人間の介入で原因究明と Tier 0 復帰を進めます。
階層遷移の判定ロジック — 3 軸の組み合わせ
階層の昇降を決めるロジックは、3 軸を組み合わせます。
// tier-controller.ts — 階層遷移の判定
type TierLevel = 0 | 1 | 2 | 3;
interface TierSignals {
burnRate: number; // 月予算に対する消費ペース (1.0 = 100%)
errorRate10min: number; // 直近10分のエラー率 (0-1)
healthScore20: number; // 直近20件の健康度平均 (0-1)
apiAvailable: boolean; // 外部API疎通
costCapExceeded: boolean; // 月予算上限到達
}
export function decideTier(signals: TierSignals, current: TierLevel): TierLevel {
// 強制 Tier 3 — 完全縮退
if (signals.costCapExceeded) return 3;
// 強制 Tier 2 — 外部API断
if (!signals.apiAvailable) return 2;
// 健康度深刻 — Tier 2 へ降格
if (signals.healthScore20 < 0.60) return 2;
// エラー率高 — Tier 1 へ降格(既に Tier 1/2 なら現状維持)
if (signals.errorRate10min > 0.25 && current === 0) return 1;
// Burn-Rate 高 — Tier 1 へ降格
if (signals.burnRate > 1.10 && current === 0) return 1;
// 健康度軽度劣化 — Tier 1 へ降格
if (signals.healthScore20 < 0.80 && current === 0) return 1;
// 復帰判定 — 健康度+エラー率+コストが安定したら 1 段昇格
if (current > 0 &&
signals.healthScore20 > 0.90 &&
signals.errorRate10min < 0.05 &&
signals.burnRate < 0.95) {
return (current - 1) as TierLevel;
}
return current;
}
ここで重要なのは、降格は即時、昇格はヒステリシスありという非対称設計です。降格判定は 1 つの条件が満たされれば即座に降りますが、復帰判定は「3 軸すべてが安定」した時にのみ 1 段昇格します。これは、降格と昇格を頻繁に行き来する「フラッピング」を避けるための実装で、本番運用では昇格判定をさらに「5 分間連続で安定」という時間窓で包んでいます。
判定は 30 秒ごとにスケジュールし、各エージェントごとに独立して走らせます。私が運用している壁紙系・癒し系・引き寄せ系の 6 アプリでは、それぞれが個別の Tier を保持するので、AdMob 関連エージェントだけ Tier 1 に落ちて、IAP 系エージェントは Tier 0 のまま、という挙動が普通に起きます。
6 アプリ並行運用の実装ログ — 24 ヶ月の Tier 降格統計
ここまでの設計を実際に 24 ヶ月運用してきた数値を整理します。対象は壁紙・癒し・引き寄せ系の 6 アプリで動く合計 11 個のエージェントで、累計実行回数は約 184 万回です。
- Tier 0 で完結: 94.3%(約 173.5 万回)
- Tier 1 で受け止め: 4.8%(約 8.8 万回)
- Tier 2 まで降格: 0.7%(約 1.3 万回)
- Tier 3 まで降格: 0.21%(約 3,800 回)
降格の原因分布も興味深く、Tier 1 への降格の 67% は Burn-Rate 起因(コスト軌道修正)、22% はレート制限、11% が健康度劣化でした。一方 Tier 2 まで降りた 1.3 万回のうち、85% は Gemini API の局所障害で、12% が深刻な健康度劣化、3% がネットワーク経路の問題でした。Tier 3 まで落ちた 3,800 回のほとんどは月末のコストキャップ強制発動で、本当の意味での「API完全断」によるものは年に 4〜6 回程度です。
体感としては、Tier 1 までは「ユーザーから見て応答品質の差はほぼ感じない」レベルで運用できています。Tier 2 では Crashlytics トリアージのような形式定型タスクは問題なく動き、Tier 3 まで落ちるのは月末数日のコストキャップ運用と緊急時のみです。
階層設計を導入する際の運用上の注意点
最後に、これからこの階層設計を導入する人へ、私自身がハマってきた落とし穴を共有します。
第 1 に、健康度スコアの設計を後回しにしないこと。階層降格の判定は健康度スコアに強く依存するので、これが粗いと階層全体が機能しません。私は当初「レイテンシだけ」で健康度を見ていましたが、出力検証 (validator) を追加して 4 軸 (レイテンシ・トークン使用量・finish_reason・validator) で評価する形に変えてから、降格判定の精度が大きく改善しました。
第 2 に、Tier 2 のローカルモデルを事前にウォームアップしておくこと。Gemma 4 の初回ロードに 15〜20 秒かかるため、緊急時に「これからロードします」では間に合いません。私は 1 時間に 1 回ヘルスチェックとしてダミー推論を投げる形でモデルを温めています。
第 3 に、Tier 3 の静的応答は必ず「次の人間アクションを誘導する」内容にすること。「不明」「未確定」だけで終わる応答はサービス縮退時に何も意味を持ちません。Crashlytics トリアージなら manual_review_required、AdMob 最適化なら「直近 24 時間の推奨を維持」のように、人間が次に何をすべきかを明示します。
第 4 に、降格ログは必ず保存し、月次でレビューすること。「なぜ Tier 1 に落ちたか」「なぜ Tier 2 まで降りたか」を月単位で集計すると、構造的な問題が見えてきます。私の場合、Tier 1 への降格が特定の時間帯に集中していることに気づき、その時間帯だけプロンプトをさらに小さくする運用を追加したところ、Tier 1 への降格頻度が 30% 削減されました。
第 5 に、復帰判定を急がないこと。降格は即時で良いのですが、復帰はヒステリシス+時間窓を入れないと運用が荒れます。私は「3 軸安定 + 5 分連続」という条件で復帰させていますが、サービスによっては「10 分連続」「15 分連続」まで延ばす場合もあります。
現代美術家として 17 の国際芸術賞をいただいてきた経験から言えるのは、作品でも運用でも「壊れた時の振る舞い」こそが完成度を決めるということです。この階層設計を本番投入してから、AI エージェント関連のオンコール呼び出しは月平均 11 件から月平均 2.3 件まで減りました。階層が増える分、初期実装の工数は単一フォールバックの 3 倍ほどかかりますが、長期運用の安定性で必ず回収できます。同じく個人で複数アプリを並行運用している方には、ぜひ 4 階層フォールバックを検討してみてほしいと思います。
お読みいただきありがとうございました。