ということで、この記事はAIを使ってデバッグした内容を、最後にAIにまとめてもらったものです。
はじめに
採点支援システムで Gemini API(@google/genai SDK)を使い、生徒の回答画像を含むプロンプトを generateContent に送って AI 採点を行っている。ある時期から、特定の回答データに対してのみ generateContent が応答を返さずハングし、最終的に fetch failed(undici の ECONNRESET)でタイムアウトする現象が繰り返し発生するようになった。
本記事はその原因調査と対処の記録である。
システム構成
- Next.js + Inngest によるバッチ採点基盤
- Gemini Files API で回答画像(PDF→PNG変換済み、1-2MB程度)をアップロードし、
generateContentに画像URIとプロンプトテキストを渡す - 1試験あたり最大5並列で採点を実行(Inngest の
concurrency.limit: 5) - リトライは
Promise.raceによる単体タイムアウト(120秒)+ 指数バックオフで最大5回
症状
- 15件の回答のうち、特定の1件(id: 101465)だけが常にタイムアウトする
- 他の14件は50-70秒で正常に完了する
- タイムアウト後にリトライしても同じ回答が同じように失敗する
- 回答データを削除して再作成すると即座に成功する
- 回答画像のサイズや枚数は他と同程度(1枚、約2MB)で、データ上の異常はない
調査で行ったこと
1. 構造化ログの導入
Files API のアップロード、ポーリング、generateContent の各フェーズにトレースログを埋め込み、どのフェーズでハングしているかを特定した。
結果、ファイルアップロードは正常に完了しており、generateContent の呼び出し自体がハングしていることが確認できた。
2. DB(llm_token_logs)による時系列分析
採点リクエストごとにDBに started / end / error のログを記録しており、エラーが発生する回答の request_id とタイムスタンプを突き合わせた。
このデータから、同一の回答でも並列度や実行タイミングによって成否が変わることがわかった。
3. 試行錯誤で得た手がかり
試行した対策と、その結果を時系列で記す。
核心:Gemini の内部ルーティングとコンテンツベースのキャッシュ
ソルト付与で改善したことから、Gemini の内部でリクエストのコンテンツ(テキスト+画像)に基づくルーティングまたはキャッシュが行われていると推測した。
具体的には以下の挙動が観測された:
- 同一プロンプト + 同一画像の組み合わせで
generateContentを呼ぶと、Gemini 内部で同一の処理パイプラインにルーティングされる - そのパイプラインが何らかの理由でスタックすると、以降の同一コンテンツリクエストも全て同じスタック先に送られる
- テキスト部分を変えると別のパイプラインにルーティングされ、正常に処理される
- 回答データを削除・再作成すると成功するのは、画像の再アップロードにより Files API 上の URI が変わるため
この仮説は以下の事実と整合する:
displayNameの変更は効果なし → Gemini はメタデータではなくコンテンツ本体でルーティングしている- テキストソルト付与は効果あり → テキストが変われば別ルートに乗る
- データ再作成で治る → 画像URI(Files API 上の参照先)が変わるため
最終的な対処
プロンプトテキストの末尾に、毎回ユニークなHTMLコメントをソルトとして付与する。
ポイントは、このソルト生成をリトライの operation コールバック内で行うことである。
当初はソルトをリトライループの外で1回だけ生成していたが、これでは1回目のリクエストがスタックした場合、2回目以降のリトライも同一コンテンツとなり、同じスタックしたパイプラインに送られてしまう。
こうすることで、仮に1回目がスタックしても、2回目は別のソルトで別のルーティングパスを通る。
所感
- Gemini API(に限らず LLM API 全般)には、公式ドキュメントに記載されていない内部的なルーティングやキャッシュの挙動がある。同一コンテンツの繰り返しリクエストが同一の処理パスに固着する現象は、通常のHTTPリトライの前提を崩す
- リトライでは「同じリクエストを再送する」のが一般的だが、LLM API の場合は「意味的に同等だがバイト列が異なるリクエスト」に変えることが有効な場合がある
- ソルトの挿入位置は、LLM の出力品質に影響しない場所(HTMLコメント等)を選ぶ必要がある
- 構造化ログと DB ログの突き合わせが、フェーズ特定に最も有効だった。外部 API のブラックボックス部分を調査する場合、まず「どこまで自分のコードが正常に動いているか」の境界を明確にすることが重要