Skip to Content
ДизайнAdaptive Output Token EscalationАрхитектура адаптивного увеличения лимита выходных токенов

Архитектура адаптивного увеличения лимита выходных токенов

Снижает избыточное резервирование GPU-слотов примерно в 4 раза за счёт стратегии «низкий лимит по умолчанию + увеличение при обрезке» для выходных токенов, а также многошагового восстановления для ответов, превышающих даже увеличенный лимит.

Проблема

Каждый API-запрос резервирует фиксированный GPU-слот, пропорциональный max_tokens. Предыдущее значение по умолчанию в 32K токенов означало, что каждый запрос резервировал слот на 32K выходных токенов, хотя 99% ответов занимают менее 5K токенов. Это приводит к избыточному резервированию GPU-ресурсов в 4–6 раз, ограничивая параллелизм сервера и увеличивая затраты.

Решение

Использовать ограниченный лимит по умолчанию в 8K выходных токенов. Если ответ обрезается (модель достигает max_tokens):

  1. Увеличить лимит до максимального значения модели (с минимальным порогом 64K для неизвестных моделей)
  2. Если ответ всё ещё обрезается, восстановить генерацию, сохранив частичный ответ в истории и добавив сообщение-продолжение (до 3 раз)
  3. Если попытки восстановления исчерпаны, применить рекомендации планировщика инструментов по обработке обрезанных ответов

Поскольку обрезается менее 1% запросов, это значительно снижает среднее резервирование слотов, сохраняя качество вывода для длинных ответов.

Архитектура

Request (max_tokens = 8K) ┌─────────────────────────┐ │ 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 (наименьший)Ограниченное значение по умолчаниюmin(modelLimit, 8K)min(32K, 8K) = 8KУвеличивается до лимита модели (мин. 64K) + восстановление

«Известная модель» — это модель, имеющая явную запись в OUTPUT_PATTERNS (проверяется через hasExplicitOutputLimit()). Для известных моделей эффективное значение всегда ограничивается заявленным лимитом вывода модели, чтобы избежать ошибок API. Для неизвестных моделей (кастомные развёртывания, self-hosted эндпоинты) значение пользователя передаётся напрямую, так как бэкенд может поддерживать большие лимиты.

Эта логика реализована в трёх генераторах контента:

  • DefaultOpenAICompatibleProvider.applyOutputTokenLimit() — OpenAI-совместимые провайдеры
  • DashScopeProvider — наследует applyOutputTokenLimit() от провайдера по умолчанию
  • AnthropicContentGenerator.buildSamplingParameters() — провайдер Anthropic

Механизм увеличения лимита

Логика увеличения находится в geminiChat.ts и размещена за пределами основного цикла повторных попыток. Это сделано намеренно:

  1. Цикл повторных попыток обрабатывает временные ошибки (лимиты запросов, некорректные стримы, валидация контента)
  2. Обрезка не является ошибкой — это успешный ответ, который был прерван
  3. Ошибки из увеличенного стрима должны напрямую передаваться вызывающему коду, а не перехватываться логикой повторных попыток

Шаги увеличения (geminiChat.ts)

1. Stream completes successfully (lastError === null) 2. Last chunk has finishReason === MAX_TOKENS 3. Guard checks pass: - maxTokensEscalated === false (prevent infinite escalation) - hasUserMaxTokensOverride === false (respect user intent) 4. Compute escalated limit: max(ESCALATED_MAX_TOKENS, tokenLimit(model, 'output')) 5. Pop the partial model response from chat history 6. Yield RETRY event (isContinuation: false) → UI discards partial output and resets buffers 7. Re-send the same request with maxOutputTokens: escalatedLimit

Шаги восстановления (geminiChat.ts)

Если увеличенный ответ также обрезается (finishReason === MAX_TOKENS), цикл восстановления выполняется до MAX_OUTPUT_RECOVERY_ATTEMPTS (3) раз:

1. Partial model response is already in history (pushed by processStreamResponse) 2. Push a recovery user message: OUTPUT_RECOVERY_MESSAGE 3. Yield RETRY event (isContinuation: true) → UI keeps text buffer for continuation 4. Re-send with updated history (model sees its partial output + recovery instruction) 5. If still truncated and attempts remain, loop back to step 1 6. If recovery attempt throws (empty response, network error): - Pop the dangling recovery message from history - Break out of recovery loop

Очистка состояния при RETRY (turn.ts)

Когда класс Turn получает событие RETRY, он очищает накопленное состояние для предотвращения несоответствий:

  • pendingToolCalls — очищается, чтобы избежать дублирования вызовов инструментов, если первый обрезанный ответ содержал завершённые вызовы, которые повторяются в увеличенном ответе
  • pendingCitations — очищается, чтобы избежать дублирования ссылок
  • debugResponses — очищается, чтобы избежать устаревших отладочных данных
  • finishReason — сбрасывается в undefined, чтобы использовалась причина завершения нового ответа

Флаг isContinuation передаётся в UI, чтобы он мог решить, сбросить текстовые буферы (увеличение) или сохранить их (восстановление).

Константы

Определены в geminiChat.ts и tokenLimits.ts:

КонстантаЗначениеНазначение
CAPPED_DEFAULT_MAX_TOKENS8 000Лимит выходных токенов по умолчанию, если пользователь не задал своё значение
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?

  • 99% ответов занимают менее 5K токенов
  • 8K даёт достаточный запас для немного более длинных ответов без запуска ненужных повторных попыток
  • Снижает среднее резервирование слотов с 32K до 8K (улучшение в 4 раза)

Почему увеличивать до лимита модели, а не до фиксированных 64K?

  • Модели с более высокими лимитами вывода (Claude Opus 128K, GPT-5 128K) необоснованно ограничивались 64K
  • Использование фактического лимита модели позволяет обработать подавляющее большинство длинных ответов без второй повторной попытки
  • ESCALATED_MAX_TOKENS (64K) служит минимальным порогом для неизвестных моделей, где tokenLimit() возвращает стандартные 32K

Почему многошаговое восстановление, а не постепенное увеличение?

  • Постепенное увеличение (8K → 16K → 32K → 64K) требует полной регенерации ответа каждый раз
  • Многошаговое восстановление сохраняет частичный ответ и позволяет модели продолжить генерацию, экономя токены и снижая задержку
  • Сообщения восстановления дёшевы (~40 токенов каждое) по сравнению с регенерацией больших ответов
  • Лимит в 3 попытки предотвращает бесконечные циклы, покрывая при этом большинство практических сценариев

Почему увеличение вынесено за пределы цикла повторных попыток?

  • Обрезка — это успешный сценарий, а не ошибка
  • Ошибки из увеличенного стрима (лимиты запросов, сетевые сбои) должны передаваться напрямую, а не молча повторяться с некорректными параметрами
  • Позволяет циклу повторных попыток оставаться сфокусированным на своей исходной задаче (восстановление после временных ошибок)
  • Ошибки восстановления обрабатываются отдельно, чтобы не прерывать весь диалог
Last updated on