Request Coalescing

Устранение вызовов модели, которых вообще не должно было быть: одинаковые параллельные запросы схлопываются в один инференс, промежуточный ввод не улетает в API, мусор отсекается детерминированным кодом до LLM. Экономия не за счёт удешевления токена, а за счёт исчезновения запроса.

Суть

Оптимизация стоимости обычно сводится к «сделаем токен дешевле» — через кэш, каскад, модель поменьше. Есть слой раньше: часть трафика физически лишняя. Три приёма на разных уровнях стека.

SingleFlight на шлюзе

Классический паттерн из бэкенда, перенесённый на модельный шлюз. Пять пользователей одновременно просят суммаризацию одной и той же горячей новости — выполняется один инференс-таск, его результат раздаётся всем пятерым.

async def coalesced_call(key: str, fn):
    if key in inflight:              # такой запрос уже считается
        return await inflight[key]   # ждём чужой результат, своего вызова не делаем
    task = asyncio.create_task(fn())
    inflight[key] = task
    try:
        return await task
    finally:
        del inflight[key]

Отличие от Semantic Cache: кэш работает с уже завершёнными запросами, coalescing — с одновременными. На всплеске трафика кэш ещё пуст, потому что первый ответ не готов, и без coalescing система делает N одинаковых вызовов сразу. Фактически это защита от самодоса на популярном контенте.

Debounce на уровне интерфейса

Поле поиска с подсказками от модели генерирует вызов на каждое нажатие клавиши. Пауза 400 мс перед отправкой срезает около 90% промежуточных вызовов автокомплита: пользователь допечатывает слово, и в API уходит один запрос вместо десяти.

Самая дешёвая оптимизация во всём FinOps-наборе — правится во фронтенде, не требует изменений в архитектуре и не влияет на качество.

Детерминированная пре-валидация

Regex и правила перед вызовом модели: пустой ввод, бессмысленный набор символов, заведомо не относящиеся к домену запросы, известные атакующие паттерны. Всё это отсекается за микросекунды и ноль токенов.

Двойная польза: экономия сходится с безопасностью — тот же слой ловит часть попыток инъекций до того, как они дойдут до модели (см. Guardrails, Injection Detection).

Порядок слоёв

Ввод → debounce (UI) → пре-валидация (regex) → coalescing (шлюз) → кэш → каскад → LLM

Каждый следующий слой дороже предыдущего, поэтому порядок именно такой. Ошибка — ставить семантический кэш первым: он требует эмбеддинга запроса, то есть уже стоит денег, тогда как regex-проверка бесплатна.

Ключ схлопывания и цена debounce

Включать ли user_id в ключ. Правило жёсткое и не подлежит оптимизации: всё, от чего зависит содержание ответа, входит в ключ. Если ответ зависит от прав доступа — в ключ идёт не сам user_id, а набор прав или его хеш: тогда два пользователя с одинаковыми правами разделят один вызов, а пользователь с другими правами не получит чужой ответ. Это и экономнее, чем ключ по user_id (схлопывание всё-таки происходит), и безопаснее, чем общий ключ. Соблазн «схлопнуть по тексту запроса» здесь — прямой путь к утечке между пользователями, того же класса, что ложное попадание семантического кэша (Semantic Cache).

Ломает ли debounce отзывчивость. Да, если применять его к финальной отправке — в чате пользователь ждёт реакции сразу, и задержка читается как «подвисло». Поэтому debounce ставят не на отправку, а на промежуточные срабатывания: автодополнение, предпросмотр, реакция на набор текста. Для явной отправки правильный приём другой — не откладывать вызов, а показывать, что он начался: стриминг первых токенов снимает вопрос отзывчивости лучше любой экономии, потому что воспринимаемая скорость определяется временем до первого токена, а не до последнего.

Связано с

  • Agent CostControl — общая картина управления затратами, куда встраиваются эти слои
  • Semantic Cache — соседний слой, работающий с завершёнными запросами
  • Cascade Routing — следующий по порядку слой, уже с вызовом модели
  • Guardrails — пре-валидация как точка контроля входа