Skip to Content
デザインAdaptive Output Token Escalation出力トークン制限とエスカレーション設計

出力トークン制限とエスカレーション設計

ユーザーまたは環境によって max_tokens が設定されていない限り、モデルの宣言された出力制限をデフォルトとし、それでも応答が MAX_TOKENS に達した場合にのみエスカレーションとマルチターンリカバリーを使用します。

問題

すべての API リクエストは、max_tokens に比例した固定の GPU スロットを予約します。デフォルト値を低く設定するとスロット予約を減らせますが、通常の大きな応答が切り詰められる可能性も高くなります。ファイル書き込みワークフローでは、これにより不完全なツール呼び出し引数が生成され、スケジューラーが部分的な書き込みを拒否せざるを得なくなる可能性があります。

解決策

デフォルトではモデルの宣言された出力制限を使用します。応答が切り詰められた場合(モデルが max_tokens に達した場合):

  1. モデルの完全な出力制限までエスカレートする(現在の制限がそれより低い場合、64K を下限とする)
  2. それでも切り詰められる場合は、部分的な応答を履歴に保持し、継続メッセージを挿入して最大 3 回リカバリーする
  3. リカバリーが尽きた場合は、ツールスケジューラーの切り詰めガイダンスにフォールバックする

これにより、大規模な生成タスクやファイル編集タスクの正確性が優先されます。より低い予約を必要とするオペレーターは、引き続き QWEN_CODE_MAX_OUTPUT_TOKENS を設定でき、その明示的な値が尊重されます。

アーキテクチャ

Request (max_tokens = user/env value or model output limit) ┌─────────────────────────┐ │ Response truncated? │──── No ──▶ Done ✓ │ (MAX_TOKENS) │ └───────────┬──────────────┘ │ Yes ┌──────────────────────────────────────────────────┐ │ Layer 1: Escalate to model output limit │ │ ┌────────────────────────────────────────────┐ │ │ │ Pop partial response from history │ │ │ │ RETRY (isContinuation: false → reset UI) │ │ │ │ Re-send at max(64K, model output limit) │ │ │ └────────────────────────────────────────────┘ │ └───────────┬──────────────────────────────────────┘ ┌─────────────────────────┐ │ Still truncated? │──── No ──▶ Done ✓ │ (MAX_TOKENS) │ └───────────┬──────────────┘ │ Yes ┌──────────────────────────────────────────────────┐ │ Layer 2: Multi-turn recovery (up to 3×) │ │ ┌────────────────────────────────────────────┐ │ │ │ Keep partial response in history │ │ │ │ Push user message: "Resume directly..." │ │ │ │ RETRY (isContinuation: true → keep UI buf) │ │ │ │ Re-send with updated history │ │ │ │ Model continues from where it left off │ │ │ └──────────────┬─────────────────────────────┘ │ │ │ │ │ ┌──────┴──────┐ │ │ │ Succeeded? │── Yes ──▶ Done ✓ │ │ └──────┬──────┘ │ │ │ No (still truncated) │ │ ▼ │ │ attempt < 3? ── Yes ──▶ loop back ↑ │ └───────────┬──────────────────────────────────────┘ │ No (exhausted) ┌──────────────────────────────────────────────────┐ │ Layer 3: Tool scheduler fallback │ │ ┌────────────────────────────────────────────┐ │ │ │ Reject truncated Edit/Write tool calls │ │ │ │ Return guidance: "You MUST split into │ │ │ │ smaller parts — write skeleton first, │ │ │ │ then edit incrementally." │ │ │ └────────────────────────────────────────────┘ │ └──────────────────────────────────────────────────┘

トークン制限の決定

有効な max_tokens は、以下の優先順位で解決されます:

優先度ソース値 (既知のモデル)値 (不明なモデル)エスカレーション動作
1 (最高)ユーザー設定 (samplingParams.max_tokens)min(userValue, modelLimit)userValueエスカレーションなし
2環境変数 (QWEN_CODE_MAX_OUTPUT_TOKENS)min(envValue, modelLimit)envValueエスカレーションなし
3 (最低)モデル/デフォルト出力制限modelLimitDEFAULT_OUTPUT_TOKEN_LIMIT = 32Kモデル制限までエスカレート (下限64K) + リカバリー

「既知のモデル」とは、OUTPUT_PATTERNS に明示的なエントリを持つモデルです(hasExplicitOutputLimit() 経由でチェック)。既知のモデルの場合、API エラーを回避するため、有効な値は常にモデルの宣言された出力制限でキャップされます。不明なモデル(カスタムデプロイメント、セルフホストエンドポイント)は、バックエンドがより大きな制限をサポートしている可能性があるため、ユーザーの値を直接渡します。

このロジックは、3つのコンテンツジェネレーターで実装されています:

  • DefaultOpenAICompatibleProvider.applyOutputTokenLimit() — OpenAI互換プロバイダー
  • DashScopeProvider — デフォルトプロバイダーから applyOutputTokenLimit() を継承
  • AnthropicContentGenerator.buildSamplingParameters() — Anthropicプロバイダー

エスカレーションメカニズム

エスカレーションロジックは geminiChat.ts にあり、メインのリトライループの外側に配置されています。これは意図的なものです:

  1. リトライループは過渡的なエラー(レート制限、無効なストリーム、コンテンツ検証)を処理します
  2. 切り詰めはエラーではありません — 途中で切り上げられた成功した応答です
  3. エスカレートされたストリームからのエラーは、リトライロジックでキャッチされるのではなく、呼び出し元に直接伝播する必要があります

エスカレーションステップ (geminiChat.ts)

1. ストリームが正常に完了する (lastError === null) 2. 最後のチャンクの finishReason === MAX_TOKENS である 3. ガードチェックに合格する: - maxTokensEscalated === false (無限エスカレーションを防止) - hasUserMaxTokensOverride === false (ユーザーの意図を尊重) 4. エスカレートされた制限を計算する: max(ESCALATED_MAX_TOKENS, tokenLimit(model, 'output')) 5. チャット履歴から部分的なモデル応答をポップする 6. RETRY イベントを yield する (isContinuation: false) → UI は部分的な出力を破棄し、バッファをリセットする 7. maxOutputTokens: escalatedLimit で同じリクエストを再送信する

リカバリーステップ (geminiChat.ts)

エスカレートされた応答も切り詰められた場合(finishReason === MAX_TOKENS)、リカバリー ループは MAX_OUTPUT_RECOVERY_ATTEMPTS (3) 回まで実行されます:

1. 部分的なモデル応答はすでに履歴にある (processStreamResponse によってプッシュされた) 2. リカバリーユーザーメッセージをプッシュする: OUTPUT_RECOVERY_MESSAGE 3. RETRY イベントを yield する (isContinuation: true) → UI は継続のためにテキストバッファを保持する 4. 更新された履歴で再送信する (モデルは自身の部分的な出力 + リカバリー指示を見る) 5. まだ切り詰められており、かつ試行回数が残っている場合は、ステップ 1 にループして戻る 6. リカバリー試行がスローされた場合 (空の応答、ネットワークエラー): - 履歴から浮いたリカバリーメッセージをポップする - リカバリー ループを抜ける

RETRY 時の状態クリーンアップ (turn.ts)

Turn クラスが RETRY イベントを受信すると、不整合を防ぐために蓄積された状態をクリアします:

  • pendingToolCalls — 最初の切り詰められた応答に完了したツール呼び出しが含まれており、それがエスカレートされた応答で繰り返される場合の重複ツール呼び出しを避けるためにクリアされます
  • pendingCitations — 引用の重複を避けるためにクリアされます
  • finishReason — 新しい応答の finish reason が使用されるように undefined にリセットされます

isContinuation フラグは UI に渡され、テキストバッファをリセットするか(エスカレーション)、保持するか(リカバリー)を決定できるようにします。

定数

geminiChat.ts および tokenLimits.ts で定義されています:

定数目的
ESCALATED_MAX_TOKENS64,000モデル制限が低い場合のエスカレーションの下限
MAX_OUTPUT_RECOVERY_ATTEMPTS3エスカレーション後の最大マルチターンリカバリー試行回数

有効なエスカレート制限は max(ESCALATED_MAX_TOKENS, tokenLimit(model, 'output')) です:

モデルエスカレート制限
Claude Opus 4.6131,072 (128K)
GPT-5 / o-series131,072 (128K)
Qwen3.x65,536 (64K)
不明なモデル64,000 (下限)

設計上の決定

なぜデフォルトで 8K を使用しないのか?

  • 8K デフォルトは正確性の要件ではなく、スロット予約/容量の最適化です。これは、バックエンドのスループット(リクエストは max_tokens に比例した GPU スロットを予約するため、低い値ほど過剰予約が少なくなる)のために、正確性(大きな応答が切り詰められる)を犠牲にするものです。
  • 大規模なファイル生成や編集ツール呼び出しは正当に 8K を超える可能性があるため、8K デフォルトは通常のリクエストを「切り詰め → エスカレート」の往復(最悪の場合、リトライループ)に変えてしまいます。
  • Claude Code も同じ 8K キャップを維持していますが、サードパーティプロバイダーではデフォルトでオフになる機能フラグ(tengu_otk_slot_v1)の背後にそれを隠しています(「Bedrock/Vertex では未検証」)。つまり、ファーストパーティ以外のサービスに対するデフォルト動作は、まさに「モデルの宣言された制限を使用する」です。qwen-code のプロバイダーはすべてサードパーティ / OpenAI互換 / セルフホストであるため、そのデフォルトオフの動作に一致させるのが安全な選択です。低いデフォルトがすべてのバックエンドで安全であると仮定することはできません。
  • 容量のトレードオフは失われたのではなく、オプトインにしただけです。容量が制約されているセルフホストバックエンドのオペレーターは、QWEN_CODE_MAX_OUTPUT_TOKENS(例: 8000)を設定して、リクエストごとの低い予約を復元できます。GrowthBook スタイルの機能フラグは意図的に再導入されていません。qwen-code にはそのようなインフラストラクチャがなく、環境変数ですでにニーズがカバーされているためです。

なぜ固定の 64K ではなくモデル制限までエスカレートするのか?

  • より高い出力制限を持つモデル(Claude Opus 128K、GPT-5 128K)が不必要に 64K に制約されていた
  • モデルの実際の制限を使用することで、2回目のリトライなしで非常に長い出力の大部分をカバーできる
  • ESCALATED_MAX_TOKENS (64K) は、tokenLimit() がデフォルトの 32K を返す不明なモデルの下限として機能する

なぜ段階的なエスカレーションではなくマルチターンリカバリーなのか?

  • 段階的なエスカレーション(例: 16K -> 32K -> 64K)では、毎回完全な応答を再生成する必要がある
  • マルチターンリカバリーは部分的な応答を保持し、モデルに続行させることで、トークンとレイテンシを節約する
  • リカバリーメッセージは、大きな応答を再生成するのに比べてコストが安い(各約 40 トークン)
  • 3 回の試行制限は、無限ループを防ぎながら、ほとんどの実用的なケースをカバーする

なぜエスカレーションはリトライループの外側にあるのか?

  • 切り詰めは成功ケースであり、エラーではない
  • エスカレートされたストリームからのエラー(レート制限、ネットワーク障害)は、誤ったパラメータでサイレントにリトライされるのではなく、直接伝播されるべきである
  • リトライループを元の目的(過渡的なエラーのリカバリー)に集中させる
  • リカバリーエラーは、会話全体を中止しないように個別にキャッチされる
Last updated on