Cascade Routing

Штатный режим работы под нагрузкой, при котором весь трафик сначала идёт в дешёвую модель, а на дорогую эскалируется только то, что не прошло гейт уверенности. Не аварийный fallback, а основной путь запроса: 70–80% ответов закрывает младшая модель.

Суть

User Request → Tier-1 (дешёвая, быстрая) → Output → Quality/Confidence Gate
                                                     ├── PASS (70–80%) → пользователю
                                                     └── FAIL (20–30%) → Tier-2 (флагман) → пользователю

Ключевое отличие от привычного fallback: там переключение на другую модель — реакция на сбой (провайдер отдал 5xx, сломался формат). Здесь переключение — реакция на неуверенность в качестве, и происходит оно при полностью здоровой системе.

Заявленный эффект: снижение затрат на 65% при сохранении итогового качества — 99.2% от базовой флагманской модели на бенчмарке. Экономика работает потому, что порядок цен между тирами отличается на порядки: $0.0001 против $0.005 за вызов в примере.

Зачем это нужно

Отправлять весь трафик во флагман — самая распространённая и самая дорогая архитектура. При этом качество на большинстве задач упирается в плато: 80% задач классификации и извлечения сущностей выходят на него уже на младших моделях (см. Model Selection). Деньги за верхний тир платятся за оставшиеся проценты, которые нужны далеко не каждому запросу.

Каскад разделяет трафик по фактической сложности, а не по предположениям продакт-менеджера о ней.

Триггеры эскалации

Гейт — самая содержательная часть паттерна. Четыре подхода, от дешёвого к дорогому:

Тип Механика Цена
Эвристики и правила Длина ответа < N символов, стоп-слова («не могу ответить», «недостаточно данных»), сбой парсинга JSON ~0
Logprobs / perplexity Оценка внутренней уверенности модели: avg(logprobs) < -0.45 ~0, но нужны logprobs от провайдера
LLM-верификатор Сверхбыстрый вызов малой guard-модели с бинарным промптом: «Отвечает ли данный текст на вопрос? [YES/NO]» Один дешёвый вызов
Фидбэк пользователя «Повторить генерацию» или дизлайк → автоматический ретрай на Tier-2 0, но реагирует постфактум

Порог -0.45 — отправная точка, а не константа: его калибруют на своём трафике.

Где именно стоит LLM-верификатор — принципиально. Здесь он проверяет готовый ответ перед выдачей: один бинарный вопрос, один выход, решение «отдать или эскалировать». Это не то же самое, что судья внутри агентного цикла, между шагами, — от него в проде отказались, потому что он замедлял ответы и на части прогонов ухудшал результат, «споря с реальностью» (LLM as Judge, раздел «Где судью ставить нельзя»). Правило разграничения оттуда же: где решение проверяется кодом — схемой, бизнес-правилом, инвариантом — детерминированный гейт всегда предпочтительнее судьи, и поэтому в таблице выше он стоит ниже эвристик и logprobs, а не выше.

Confidence — не универсальный пропуск

У типизированной модели решений (Typed Decision Models) гейт может получать полное распределение по вариантам, но и это не превращает confidence в готовую политику. Концентрация распределения не гарантирует правильность конкретного ответа; сначала нужна доменная калибровка, затем отдельный порог под тяжесть последствий.

Корректный каскад оптимизируют как selective prediction: для каждого порога измеряют coverage (сколько случаев закрывается дешёвой ступенью) и risk (ошибка на принятых случаях). Для рискованного действия решение может быть accept / verify / human, а не бинарным pass / fail. Порог переносится между моделями и доменами только после повторной проверки.

Пятый триггер: исчерпание, а не качество

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

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

Ступень Чем исчерпывается
включённое в подписку окно квоты дошло до порога
беcключевой путь остывание соединения или размыкатель
бесплатный тариф провайдера бесплатный лимит выбран
дешёвая платная модель нечем — соединение обслуживает бесконечно
дорогая платная модель нечем — то же самое

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

Гистерезис: порог возврата выше порога ухода

Ступень, с которой ушли по исчерпанию, нельзя возвращать по тому же порогу, по которому ушли. Соединение, болтающееся около порога, начнёт скакать между ступенями на соседних запросах — а каждый скачок это смена модели посреди работы.

Лечится разведением двух порогов: уходят при остатке ниже одного значения, возвращаются при остатке выше большего. Зазор между ними и есть полоса нечувствительности. Наблюдаемые величины — порядка двух процентов на выход и пяти на возврат, но важно соотношение, а не числа.

Возврат после сброса квоты требует истечения трёх независимых вещей

Квота пополнилась — это ещё не значит, что маршрутизация вернулась на дешёвую ступень. Мешать могут три разные вещи, и починка одной оставляет систему на платных ступенях надолго:

  1. Кэш состояния квоты. У записи свой срок жизни, и запись, чей собственный момент сброса уже прошёл, описывает окно, которого больше нет. Такая запись обязана считаться протухшей независимо от возраста — иначе план, пополнившийся в полночь, читается исчерпанным, пока случайно не истечёт кэш.
  2. Собственное состояние лестницы — и его лучше не заводить вовсе. Запись «сейчас мы на третьей ступени» переживает сброс квоты и заклинивает маршрутизацию. Пригодность ступени пересчитывается из живого состояния при каждой сборке кандидатов; отсутствие памяти здесь — свойство, а не упущение.
  3. Остывание соединения после ограничения частоты. Экспоненциальная выдержка легко перелетает настоящий момент сброса. Выдержку сужают до момента, названного самим провайдером, и никогда не продлевают сверх собственного расчёта.

Пример

def answer(query: str) -> str:
    draft = call_tier1(query)                      # $0.0001; ответ + logprobs, не строка
    text = draft.text

    if parse_failed(text) or len(text) < MIN_LEN or has_refusal_phrase(text):
        return call_tier2(query)                   # $0.005, эскалация по эвристике
    if draft.avg_logprob < -0.45:
        return call_tier2(query)                   # эскалация по неуверенности модели

    return text                                    # 70–80% трафика заканчивается здесь

Условие на logprobs диктует, что должен вернуть первый рубеж. Эскалация по неуверенности возможна, только если tier-1 отдал не текст, а объект с полем вероятностей: у голой строки взять avg_logprob неоткуда. Это ограничение на выбор провайдера и режима вызова, а не деталь примера — часть API вероятности не возвращает вовсе, и тогда из таблицы гейтов выпадает целая строка, а её нагрузка переезжает на эвристики и верификатор.

Порядок проверок важен: сначала бесплатные, потом платные. Вызывать верификатор до того, как отработали правила, — значит платить за то, что и так видно.

Порог 0.85 нельзя переносить на эмбеддинговый кэш. Он стоит на косинусной близости TF-IDF-векторов по замкнутому корпусу FAQ, где сравниваются в основном совпадения слов, и шкала сжата иначе. В Semantic Cache тот же по названию порог стоит на эмбеддингах и равен 0.92, а в чувствительном домене поднимается до 0.96. Числа не противоречат друг другу, потому что измеряют разное; универсального значения нет ни у одного из них, и калибруются они одинаково — по размеченным парам «одинаковый вопрос / разный вопрос» из своего домена.

Полный пайплайн: четыре рубежа

FinOps-разбор собирает каскад не из двух моделей, а из четырёх рубежей, где вызов модели появляется только на третьем:

# Рубеж 0 — правила, $0
CHITCHAT_PATTERNS = ["спасибо", "привет", "добрый день", "ок, понял", "до свидания", "спс", "ясно"]

def is_chitchat_rule(query: str) -> bool:
    clean = query.lower().strip()
    return any(p in clean for p in CHITCHAT_PATTERNS) and len(clean.split()) <= 4

# Рубеж 1 — семантический кэш на TF-IDF по эталонным вопросам FAQ
#             порог 0.85 стоит на TF-IDF, а не на эмбеддингах — см. оговорку ниже
def check_semantic_cache(query: str, threshold=0.85):
    sims = cosine_similarity(vectorizer.transform([query]), faq_vectors)[0]
    best = int(np.argmax(sims))
    return (True, sims[best], faq_corpus[best]) if sims[best] >= threshold else (False, sims[best], None)

# Рубеж 2 — Tier-1 модель;  Рубеж 3 — Tier-2 по confidence gate

Ограничение длины в is_chitchat_rule (len(clean.split()) <= 4) — важная деталь: без него «спасибо, а теперь посчитай мою задолженность» уйдёт в шаблон вежливости. Правило нулевого рубежа должно быть узким, иначе экономия оплачивается пропущенными реальными вопросами.

Порог кэша в том разборе занижен до 0.85, потому что TF-IDF на маленьком корпусе даёт более низкие абсолютные значения близости, чем эмбеддинг-модель. На проде порог калибруется под конкретный энкодер (Semantic Cache).

Если logprobs недоступны. Часть провайдеров их не отдаёт, и это не тупик: строка с logprobs в таблице выше — лишь один из четырёх гейтов, причём средний по цене. Без неё каскад собирается из оставшихся трёх — эвристики (длина, стоп-слова, сбой парсинга) стоят ноль и снимают заметную долю, а на сомнительном остатке ставится дешёвый LLM-верификатор. Терять при этом приходится не качество гейта, а его дешевизну: verifier — это вызов, а logprobs шли бесплатно вместе с ответом.

Про эскалацию по фидбэку. Она реагирует постфактум и по своей природе редкое событие, поэтому как самостоятельный гейт работает только на потоке, где отрицательных сигналов набирается достаточно, чтобы отличать их от случайного раздражения — на маленьком трафике это шум. Порядки величин здесь те же, что для канареечных выводов (Canary Release LLM, раздел про статистику). Практически фидбэк применяют не как гейт, а как источник кейсов: каждый дизлайк отправляется в eval-набор, и решение принимается по накопленному набору, а не по отдельному сигналу.

Чем каскад ломается

  • Гейт слишком мягкий — плохие ответы уходят пользователю, экономия оплачена качеством.
  • Гейт слишком строгий — эскалирует почти всё, каскад превращается в флагман плюс лишний вызов сверху.
  • Считают Cost per Request вместо Cost per Outcome — дешёвая модель отвечает плохо, пользователь переспрашивает четыре раза, и суммарно выходит дороже одного точного вызова (см. Unit Economics AI).

Отсюда правило: каскад нельзя внедрять без эвалов. Стоимость — метрика, которая валидируется тестами качества, иначе это не оптимизация, а деградация с отчётом об экономии.

Связано с

  • FinOps AI — каскад как крупнейший рычаг снижения затрат
  • Inference Optimization — спекулятивное декодирование: похожая идея без потери качества
  • Agent Routing — маршрутизация по намерению; каскад маршрутизирует по уверенности
  • Model Selection — сетка тиров, между которыми ходит каскад
  • Agent CostControl — измерение эффекта и атрибуция затрат
  • Request Coalescing — слой ещё раньше: схлопывание дублирующих вызовов
  • Semantic Cache — слой перед каскадом: часть трафика не доходит и до Tier-1
  • LLM as Judge — верификатор в роли гейта и ограничения этого приёма
  • Online Quality Signal — рейтинг исполнителей, которым каскад упорядочивает кандидатов внутри ступени
  • Agent Failure Modes — что делает молчаливое падение на платную ступень, когда телеметрия квоты недоступна
  • Typed Decision Models — типизированное распределение как сигнал гейта, но не полномочие на действие