🩺

AIモデルのサポート切れを自動検知する - E2Eテスト×ヘルスチェックで本番障害を未然に防ぐ

に公開

はじめに

生成AIを活用したプロダクトでは、複数のAIプロバイダー(Google Gemini、OpenAI、AWS Bedrock等)のモデルを組み合わせて使うケースが増えています。

しかし、ある日突然こんなことが起きます。

「本番でAI機能が全部500エラーになってる!」

原因は、使っていたモデルがプロバイダー側でサポート終了(deprecated)になっていたこと。実際に我々のプロダクトでも、gemini-2.0-flash がVertex AIから404を返すようになり、E2Eテストで初めて気づきました。

この経験から、AIモデルのサポート切れを自動で検知し、Slackに通知する仕組みを構築しました。本記事ではその設計と実装を解説します。

課題: なぜ人間のチェックでは不十分か

AIモデルのサポート状況は各プロバイダーのドキュメントに記載されていますが、以下の理由で人間の監視は漏れやすいです。

  • 複数プロバイダー × 複数モデルの組み合わせを全て追跡するのは現実的でない
  • deprecation通知がメールに埋もれる
  • 担当者の異動・退職で監視が途切れる
  • モデルによっては事前通知なく突然使えなくなるケースもある

解決策: E2Eテスト + モデルヘルスチェックの定期自動実行

全体アーキテクチャ

ポイントは2層構造です。

  1. E2Eテスト: 実際のユーザーフローを通してAI機能の動作を確認(一部モデルをカバー)
  2. モデルヘルスチェック: 各モデルに最小リクエストを送って生存確認(全8モデル)

なぜ2層必要か

E2Eテストだけでは、音声処理モデル(カメラ・マイク必須)やベクトル埋め込みモデル(検索機能経由でしか呼ばれない)など、ブラウザテストでは到達困難なモデルがカバーできません。

一方、ヘルスチェックだけでは「モデルは応答するが、プロンプトとの組み合わせで期待通り動かない」といった機能レベルの問題を検知できません。

実装

1. バックエンドにヘルスチェックエンドポイントを追加

各プロバイダーのモデルに最小コストのリクエストを送り、応答を確認します。

@router.get("/model-health")
async def model_health(
    _api_key: str = Depends(_verify_api_key),  # E2E用APIキーで認証
) -> ModelHealthResponse:
    results = await check_all_models()
    all_ok = all(r.ok for r in results)
    return ModelHealthResponse(all_ok=all_ok, results=results)

レスポンスには各モデルの名前・プロバイダー・レイテンシも含まれます。

class ModelHealthResult(BaseModel):
    name: str          # e.g. "gemini-2.5-flash"
    provider: str      # e.g. "Vertex AI"
    ok: bool
    latency_ms: int
    error: str | None = None

各モデルのチェックは非同期で並行実行し、全体で120秒のタイムアウトを設定しています。

async def check_all_models() -> list[ModelHealthResult]:
    check_funcs = [
        ("gemini-2.5-flash", "Vertex AI", _check_gemini("GEMINI_LOW_MODEL", "gemini-2.5-flash")),
        ("gemini-2.5-pro", "Vertex AI", _check_gemini("GEMINI_HIGH_MODEL", "gemini-2.5-pro")),
        ("Claude", "Bedrock", _check_bedrock_claude()),
        ("Titan Embedding", "Bedrock", _check_bedrock_titan_embedding()),
        ("o3", "OpenAI", _check_openai_o_model()),
        ("gpt-4o-audio", "OpenAI", _check_openai_audio()),
        ("gpt-4o-transcribe", "OpenAI", _check_openai_stt()),
        ("gpt-4o (chat)", "OpenAI", _check_openai_chat()),
    ]
    tasks = [asyncio.create_task(coro) for _, _, coro in check_funcs]
    try:
        await asyncio.wait_for(asyncio.gather(*tasks), timeout=120)
    except asyncio.TimeoutError:
        logger.warning("モデルヘルスチェック全体がタイムアウトしました(120秒)")
    # 完了済みの結果を収集、未完了はエラーとして返す
    # ...

各チェック関数は最小限のリクエストに抑えます。例えばGeminiのチェックは以下のようになります。

async def _check_gemini(model_env_key: str, display_name: str) -> ModelHealthResult:
    model_id = os.getenv(model_env_key, "")
    start = time.monotonic()
    try:
        gemini = get_gemini_service(mode=GeminiMode.VERTEX_AI)
        response = await asyncio.to_thread(
            gemini.generate_content, model=model_id,
            contents="Reply with exactly: ok", retries=1,
        )
        latency = int((time.monotonic() - start) * 1000)
        return ModelHealthResult(name=display_name, provider="Vertex AI", ok=True, latency_ms=latency)
    except Exception as e:
        latency = int((time.monotonic() - start) * 1000)
        return ModelHealthResult(
            name=display_name, provider="Vertex AI", ok=False, latency_ms=latency, error=str(e)[:200]
        )

コストは1回あたり約$0.03(週次実行で月$0.12程度)。

2. Playwrightテストとして実装

// tests/e2e/health/model-health.spec.ts
test.describe("モデルヘルスチェック", { tag: "@model-health" }, () => {
  test("全AIモデルが応答可能であること", async ({ request }) => {
    test.setTimeout(120_000);

    const res = await request.get(`${AIMS_BE_URL}/e2e/model-health`, {
      headers: { "X-E2E-API-Key": E2E_API_KEY },
    });
    const body: ModelHealthResponse = await res.json();

    // 個別モデルの結果をログ出力(CI上でのデバッグ用)
    for (const r of body.results) {
      const status = r.ok ? "OK" : "FAIL";
      const detail = r.error ? ` — ${r.error}` : "";
      console.log(`  [${status}] ${r.name} (${r.provider}) ${r.latency_ms}ms${detail}`);
    }

    // 全モデルが正常であること
    const failedModels = body.results.filter((r) => !r.ok);
    expect(
      failedModels,
      `失敗したモデル: ${failedModels.map((r) => `${r.name} (${r.error})`).join(", ")}`
    ).toHaveLength(0);
  });
});

既存のE2Eテスト基盤(Playwright + CodeBuild)に乗せることで、追加インフラなしで実現できます。

3. EventBridgeで週次スケジュール実行

resource "aws_cloudwatch_event_rule" "e2e_model_health" {
  name                = "${var.env.project_name}-${var.env.short_name}-e2e-model-health"
  description         = "Weekly AI model health check via E2E test"
  schedule_expression = "cron(0 0 ? * MON *)"  # 毎週月曜 09:00 JST
  is_enabled          = true
}

resource "aws_cloudwatch_event_target" "e2e_model_health" {
  arn       = aws_codebuild_project.e2e.arn
  rule      = aws_cloudwatch_event_rule.e2e_model_health.name
  role_arn  = aws_iam_role.eventbridge_codebuild.arn

  # TEST_TAG を指定してヘルスチェックだけを実行
  input = jsonencode({
    environmentVariablesOverride = [
      {
        name  = "TEST_TAG"
        value = "@model-health"
        type  = "PLAINTEXT"
      }
    ]
  })
}

CodeBuildには既にSlack通知(CodeStar Notifications + AWS Chatbot)が設定済みなので、失敗すれば自動的にSlackに飛びます

4. buildspecにPhase 3を追加

- echo "=== Phase 3: Model health check ==="
- $PW test --grep "@model-health"

運用のポイント

モデル単独で確認したい場合

CodeBuildの TEST_TAG 環境変数に @model-health を設定すれば、ヘルスチェックだけ単独実行できます。

aws codebuild start-build \
  --project-name aims-stg-e2e \
  --environment-variables-override name=TEST_TAG,value=@model-health

検知後の対応フロー

  1. Slackに失敗通知が届く
  2. CodeBuildレポートでどのモデルが落ちたかを確認
  3. プロバイダーのステータスページ・ドキュメントで原因を調査
  4. 環境変数でモデルを切り替えて即時復旧(コード変更不要)
  5. コードのデフォルト値も更新してPR

なぜ環境変数でモデルを指定するか

# コードにデフォルト値を持ちつつ、環境変数で上書き可能にする
gemini_model = os.getenv("GEMINI_LOW_MODEL") or "gemini-2.5-flash"

この設計により、モデルのサポート切れが検知された場合にデプロイなしで環境変数の変更だけで即座に復旧できます。

まとめ

観点 対策
機能レベルの検証 E2Eテストで実際のユーザーフローを通して確認
モデル生存確認 ヘルスチェックAPIで全モデルにping
定期実行 EventBridge → CodeBuild(週次)
通知 CodeStar Notifications → Slack
即時復旧 環境変数でモデル切り替え(デプロイ不要)

AIモデルは「使えて当たり前」ではなく、外部依存サービスとして監視対象に含めるべきです。E2Eテスト基盤を活用すれば、追加インフラを最小限に抑えつつ、本番障害を未然に防ぐことができます。

株式会社スプリックス IT戦略部・SPRIX Enginieering Lab

Discussion