この記事でできること
LLM APIには、1分あたりだけでなく1日あたりのトークン数・リクエスト数の上限(日次レート制限)を持つプロバイダがあります。日次の上限は1分単位の上限と違い、一度当たると同じ日のうちは回復しません。ここでバックオフ(上限到達時にリクエストを止めて待つ処理)を入れずに定期実行のジョブなどが動き続けると、上限到達を示すエラーを無視してリクエストを送り続け、無駄なエラーを長時間出し続けることになります。
この記事は特定の実装コードではなく、LLM呼び出しにバックオフを設計する/見直すときに確認すべき3つの観点を整理したものです。プロバイダやフレームワークが違っても、「複数の経路からLLMを呼び出す処理があり、かつプロセスを複数動かして運用している」構成であれば当てはまります。
観点1: バックオフの判定は、呼び出し経路ごとではなく合流点に置く
同じLLM呼び出しに、複数の経路からたどり着く構成はよくあります。例えば次のようなものです。
- 定期実行で未処理分(バックログ)をまとめて処理するジョブ
- ユーザー操作をきっかけにその場で行う分析
- 手動での再実行
- CLIからの実行
このとき、バックオフの判定を経路ごとに書き足すと、新しい経路が増えるたびに書き忘れが起こります。実際に狙って直したはずの箇所以外の経路が素通りしてしまう、というのはレビューで見つかりやすい典型的な抜け漏れです。
判定は経路の数だけ置くのではなく、複数の経路が最終的に合流してLLMを呼び出す共通の処理の入口に、1箇所だけ置きます。ここに置けば、経路がどれだけ増えてもバックオフが効きます。
観点2: バックオフの状態をプロセス内のメモリだけに置かない
うっかり見落としやすいのが、バックオフの状態(いつまで待つか)をプロセス内の変数だけで持ってしまうケースです。本番をシングルプロセスで動かしているうちは問題になりません。しかし複数プロセス・複数ワーカーで運用していると、次のようなズレが起きます。
- レート制限を検知したプロセスA: 「まだ回復していない」と覚えていて、リクエストを止められる
- 検知していないプロセスB: 何も覚えていないので、そのままリクエストを送ってしまう
これを避けるには、バックオフの状態を全プロセスから見える場所(共有ファイル、Redisなど)にも記録します。そのうえで、プロセス内の記録が無い、または古い場合はそちらを確認する、という二段構えにします。「1プロセスでは動く設計」と「複数プロセスで動く設計」は別物だと意識しておく必要があります。
観点3: バックオフの記録先がテストに副作用を持つ場合は隔離する
バックオフの状態を共有ファイルのような実体のある場所に記録する実装にすると、テスト実行時にも同じ場所へ書き込んでしまうことがあります。LLMの資格情報がある環境でテストを実行すると、あるテストが実際にレート制限を踏んで共有ファイルに記録を残してしまうことがあるからです。そうなると、後から実行される全く無関係な別のテストまで「バックオフ中」と誤判定され、失敗します。
テストの実行順序によって結果が変わるテストは、原因の切り分けが一番難しいタイプの不具合です。これを避けるため、バックオフの記録先はテスト実行時には一時ディレクトリなど本番と別の場所に差し替え、テスト同士が状態を共有しないようにしておきます。
まとめ
- 呼び出し経路が複数ある処理にバックオフを入れるときは、経路ごとではなく、それらが合流する共通の入口に置く
- プロセスを複数動かして運用している場合、バックオフの状態をプロセス内のメモリだけに置かない。全プロセスから見える場所にも記録する
- バックオフの記録先が実体を持つ場合、テスト実行時は本番と別の場所に差し替えて、テスト同士の副作用を防ぐ
実際にこの3つの観点を見落とし、2段階に分けて直すことになった経緯はZenn版にまとめています。