Проектирование адаптивного увеличения лимита выходных токенов
Снижает избыточное резервирование GPU-слотов примерно в 4 раза за счёт стратегии «низкий лимит по умолчанию + увеличение при обрезке» для выходных токенов.
Проблема
Каждый API-запрос резервирует фиксированный GPU-слот, пропорциональный max_tokens. Предыдущее значение по умолчанию в 32K токенов означало, что каждый запрос резервировал слот на 32K выходных токенов, хотя 99% ответов содержат менее 5K токенов. Это приводит к избыточному резервированию GPU-ресурсов в 4–6 раз, ограничивая параллелизм сервера и увеличивая затраты.
Решение
Использовать ограниченный лимит по умолчанию в 8K выходных токенов. Если ответ обрезается (модель достигает max_tokens), автоматически выполнить одну повторную попытку с увеличенным лимитом в 64K. Поскольку обрезается менее 1% запросов, это значительно снижает среднее резервирование слотов, сохраняя качество вывода для длинных ответов.
Архитектура
┌─────────────────────────┐
│ Начало запроса │
│ max_tokens = 8K │
└───────────┬─────────────┘
│
▼
┌─────────────────────────┐
│ Потоковый ответ │
└───────────┬─────────────┘
│
┌─────────┴─────────┐
│ │
finish_reason finish_reason
!= MAX_TOKENS == MAX_TOKENS
│ │
▼ ▼
┌───────────┐ ┌─────────────────────┐
│ Готово │ │ Проверка условий: │
└───────────┘ │ - Нет override от │
│ пользователя? │
│ - Нет override от │
│ env? │
│ - Лимит уже не │
│ увеличен? │
└─────────┬───────────┘
ДА │ НЕТ
┌─────────┴────┐
│ │
▼ ▼
┌─────────────┐ ┌──────────┐
│ Удалить │ │ Готово │
│ частичный │ │ (обрезан)│
│ ответ модели│ └──────────┘
│ из истории │
│ │
│ Сгенерировать│
│ событие │
│ RETRY │
│ │
│ Повторно │
│ отправить │
│ max_tokens │
│ = 64K │
└─────────────┘Определение лимита токенов
Эффективное значение 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 и размещена за пределами основного цикла повторных попыток. Это сделано намеренно:
- Цикл повторных попыток обрабатывает временные ошибки (лимиты запросов, некорректные потоки, валидация контента)
- Обрезка не является ошибкой — это успешный ответ, который был прерван
- Ошибки из потока с увеличенным лимитом должны напрямую передаваться вызывающей стороне, а не перехватываться логикой повторных попыток
Шаги увеличения лимита (geminiChat.ts)
1. Потоковая передача завершена успешно (lastError === null)
2. Последний чанк имеет finishReason === MAX_TOKENS
3. Проверки guard-условий пройдены:
- maxTokensEscalated === false (предотвращение бесконечного увеличения)
- hasUserMaxTokensOverride === false (учет намерений пользователя)
4. Удаление частичного ответа модели из истории чата
5. Генерация события RETRY → UI отбрасывает частичный вывод
6. Повторная отправка того же запроса с maxOutputTokens: 64KОчистка состояния при RETRY (turn.ts)
Когда класс Turn получает событие RETRY, он очищает накопленное состояние для предотвращения несогласованностей:
pendingToolCalls— очищается, чтобы избежать дублирования вызовов инструментов, если первый обрезанный ответ содержал завершенные вызовы, которые повторяются в ответе с увеличенным лимитомpendingCitations— очищается, чтобы избежать дублирования цитатdebugResponses— очищается, чтобы избежать устаревших отладочных данныхfinishReason— сбрасывается вundefined, чтобы использовалась причина завершения нового ответа
Константы
Определены в tokenLimits.ts:
| Константа | Значение | Назначение |
|---|---|---|
CAPPED_DEFAULT_MAX_TOKENS | 8 000 | Лимит выходных токенов по умолчанию при отсутствии переопределения пользователем |
ESCALATED_MAX_TOKENS | 64 000 | Лимит выходных токенов, используемый при повторной попытке после обрезки |
Принятые проектные решения
Почему лимит по умолчанию — 8K?
- 99% ответов содержат менее 5K токенов
- 8K обеспечивает достаточный запас для немного более длинных ответов без запуска ненужных повторных попыток
- Снижает среднее резервирование слотов с 32K до 8K (улучшение в 4 раза)
Почему увеличенный лимит — 64K?
- Покрывает подавляющее большинство длинных выводов, которые обрезались на 8K
- Соответствует лимиту вывода многих современных моделей (Claude Sonnet, Gemini 3.x, Qwen3.x)
- Более высокие значения (например, 128K) нивелировали бы преимущества оптимизации слотов для <1% запросов, требующих увеличения лимита
Почему не используется прогрессивное увеличение (8K → 16K → 32K → 64K)?
- Каждая повторная попытка добавляет задержку (полный ответ должен быть сгенерирован заново)
- Одна повторная попытка — самый простой подход, покрывающий почти все случаи
- Частота обрезки <1% при лимите 8K означает, что увеличение лимита требуется почти ни для каких запросов; тем же, кому оно нужно, скорее всего, потребуется значительно больше 16K
Почему увеличение лимита вынесено за пределы цикла повторных попыток?
- Обрезка — это успешный сценарий, а не ошибка
- Ошибки из потока с увеличенным лимитом (лимиты запросов, сбои сети) должны передаваться напрямую, а не молча повторяться с некорректными параметрами
- Позволяет циклу повторных попыток оставаться сфокусированным на своей исходной задаче (восстановление после временных ошибок)