🐈

Gemini 2.0 Flashで「絶対に返金しない理不尽なAIクレーマー」を作ったら、Vercelのタイムアウトと幻覚に苦しめられた話

に公開

1. はじめに

こんにちは、個人開発者のSuiです。
最近、**「理不尽なクレーム対応をAIでシミュレーションしたら、人間のストレス耐性をテストできるのではないか?」と思いつき、『Silver Tongue(銀の舌)』**というブラウザゲームを作りました。

相手は、激怒するクレーマー「田中」。
「ショートケーキが箱の中で崩れていた(客の急ブレーキが原因)」という理不尽な言いがかりに対し、返金せずに怒りを鎮めなければならないという無理ゲーです。

リリース直後、友人にプレイしてもらうと**勝率0%**を叩き出し、あまりの理不尽さに「胃が痛い」と言われました。
この記事では、そんな「田中」を実装するにあたって直面した技術的な課題と、Gemini 2.0 Flash を使った解決策について共有します。

🍰 完成品はこちら(スマホ対応):
[https://silvertongue.vercel.app]


2. 技術スタック:爆速の狂気を作る

ネタアプリですが、技術選定は割と真面目にやっています。

  • Frontend: Next.js (App Router)
  • Infrastructure: Vercel (Serverless Functions)
  • AI Model: Google Vertex AI (Gemini 2.0 Flash)
  • Database: Supabase (PostgreSQL)

なぜ Gemini 2.0 Flash なのか?
理由は**「速さ」**です。激怒している客(田中)を待たせるとさらにキレるので、レスポンス速度はUXの生命線でした。開発初期はgpt-4o-miniを使用しており、タイムアウトになることが多発していました。(後述の問題)


3. 苦労した点と「泥臭い」解決策

① Vercelの「10秒の壁」とタイムアウト対策

VercelのHobbyプランでは、Serverless Functionsの実行時間が10秒を超えるとタイムアウトで強制終了します。
初期の実装では、AIが長考したりネットワークが混雑すると、田中が突然黙り込む(504 Gateway Timeout)現象が多発しました。

これでは「電話がいきなり切れる」という最悪の体験になります。そこで以下の対策を入れました。

対策:maxDuration の設定
Next.jsのRoute Handlerで以下の一行を追加し、許容時間を延長しました。

src/app/api/chat/route.ts
// タイムアウト対策(Gemini Flash 2.0は超高速なので余裕ですが念のため)
export const maxDuration = 60; 

Gemini 2.0 Flashの生成速度は凄まじく速いですが、これを入れておくことで安定性が劇的に向上しました。

② セキュリティ:APIキーを埋め込まない「キーレス認証」

GCPのサービスアカウントキー(JSONファイル)を環境変数にベタ書きするのはセキュリティ的に怖いですし、Vercelの環境変数のサイズ制限にも引っかかりがちです。

そこで、Workload Identity Federation を使用し、Vercel OIDCトークンを使ってキーレスでVertex AIを叩く構成にしました。

src/app/api/chat/route.ts
// Vercel上で動いている場合のみOIDC認証を使用
if (process.env.VERCEL_OIDC_TOKEN && process.env.GOOGLE_WORKLOAD_IDENTITY_PROVIDER) {
  const credentialConfig = {
    type: 'external_account',
    audience: `//iam.googleapis.com/${process.env.GOOGLE_WORKLOAD_IDENTITY_PROVIDER}`,
    subject_token_type: 'urn:ietf:params:oauth:token-type:jwt',
    token_url: 'https://sts.googleapis.com/v1/token',
    credential_source: {
      file: '/tmp/vercel-oidc-token.txt',
    },
    // ...
  };
  // 一時ファイルにトークンを書き出してGoogleAuthに読ませる泥臭い実装
  fs.writeFileSync('/tmp/vercel-oidc-token.txt', process.env.VERCEL_OIDC_TOKEN);
  // ...
}

これで、万が一コードが漏れても秘密鍵が含まれていないので安心です。

③ AIの「幻覚」と「JSON崩れ」の制御

AIに「必ずJSONで返して」と指示しても、親切心でMarkdownのコードブロック(json ... )を付けてくることがあり、そのまま JSON.parse すると落ちます。

そこで、正規表現によるクリーニング処理を挟みました。

// AIの過剰な親切(Markdown)を剥ぎ取る
if (cleanedText.startsWith('```')) {
  cleanedText = cleanedText.replace(/```json\n?/g, '').replace(/```\n?/g, '').trim();
}

また、AIが文脈を無視して「さっきから待たせやがって!(初手)」と怒り出す**幻覚(Hallucination)もありました。
これにはプロンプトで
「直前のユーザーの発言」**を明示的に挿入し、「今言われたことだけに反応しろ」と強く制約をかけることで解決しました。

④ エラーを「演出」に変える

どんなに対策しても、APIエラーは起こり得ます。
そこで、try-catch ブロックでエラーを捕捉した際、単に500エラーを返すのではなく、**「電波が悪いフリ」**をするJSONを返すようにしました。

} catch (error) {
  console.error('Error:', error);
  return NextResponse.json({ 
    reply_text: "(...電波が悪いようだ。もう一度言ってくれ)", 
    anger_level: currentAngerLevel, // ペナルティなし
    game_status: "continue",
    internal_thought: "システムエラー発生によるリカバリー"
  });
}

これにより、システムが落ちてもユーザーは「もう一度送信ボタンを押す」だけでゲームを続行でき、離脱を防げます。


4. ログから見えた「攻略の糸口」

当初、田中の怒りパラメータが強すぎて「勝率0%」だったのですが、ログ(Supabaseに保存)を分析すると、ある一人のユーザーが奇跡的に勝利していました。

その勝因は、**「娘への共感」**でした。
ひたすら謝るのではなく、「娘さんの誕生日だったんですね」と相手の背景(コンテキスト)に寄り添うことで、田中の怒りが「悲しみ(弱音)」に変わる瞬間があったのです。

これを受けて、プロンプトに**「哀愁モード」**を正式実装しました。
今は、誠心誠意向き合えば、田中とも分かり合える(かもしれない)仕様になっています。


5. さいごに

個人開発は「技術の実験場」として最高です。
Gemini 2.0 Flashの速度、Vercel×GCPの認証連携、そしてAIのプロンプト芸。これらを組み合わせて「理不尽な体験」を作るのは、とても楽しいハッカソンでした。

まだ田中を「完全論破(怒り0%)」できた人は数えるほどしかいません。
腕に覚えのあるエンジニアの皆さん、ぜひ挑戦してログを残してください。

挑戦状はこちらから👇
[https://silvertongue.vercel.app]

Discussion