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で以下の一行を追加し、許容時間を延長しました。
// タイムアウト対策(Gemini Flash 2.0は超高速なので余裕ですが念のため)
export const maxDuration = 60;
Gemini 2.0 Flashの生成速度は凄まじく速いですが、これを入れておくことで安定性が劇的に向上しました。
② セキュリティ:APIキーを埋め込まない「キーレス認証」
GCPのサービスアカウントキー(JSONファイル)を環境変数にベタ書きするのはセキュリティ的に怖いですし、Vercelの環境変数のサイズ制限にも引っかかりがちです。
そこで、Workload Identity Federation を使用し、Vercel OIDCトークンを使ってキーレスでVertex AIを叩く構成にしました。
// 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