---
title: "SurfCap-α: как семантический роутер выбирает модель"
date: 2026-10-26T09:00:00+03:00
type: post
lang: ru
domains: ["ai","architecture"]
tags: ["llm-gateway","routing","llm","data-residency"]
series: llm-gateway
series_weight: 1
summary: "Как собственный LLM-шлюз по model=auto выбирает класс модели: детерминированные правила, ML-слои, которые могут только повысить тир, и инвариант резидентности. T0 выбирается за 0.7 мс."
llm_summary: "Первая статья цикла «LLM-шлюз». SurfCap-α — алгоритм автоматического выбора модели в собственном LLM-шлюзе на FastAPI: клиент присылает model=auto, шлюз выбирает класс модели. Слои: Surface (поверхностные признаки и правила тиров T0–T3, first match), Capability-головы по мотивам HyDRA и α-порог RouteLLM на границе T1↔T2 (оба могут только повысить тир и в бою пока выключены), инвариант резидентности для ПДн и конфиденциальных данных, мягкий бюджет в условных единицах, перескоки между провайдерами внутри класса и выключенный по умолчанию каскад. За 19 часов через model=auto прошло 826 запросов; выбор маршрута для T0 занимает 0.7 мс; 176 из 230 запросов T3 ушли на резидентные модели. Офлайн-оценки качества и стоимости пока нет."
---


SurfCap-α — алгоритм, которым наш LLM-шлюз выбирает модель, когда клиент присылает `model=auto`. Простые запросы он отправляет на дешёвую модель за 0.7 мс в среднем, а запросы с персональными или конфиденциальными данными не выпускает за контур: за 19 часов работы текущего процесса шлюза так на резидентных моделях остались 176 из 230 самых рискованных запросов. Решение принимают детерминированные правила, а ML-слои могут только повысить класс модели, но не понизить его.

> [!BIZ]
>
> - **Дешёвые вопросы — дешёвой модели.** Больше половины запросов (56.9%) ушли на дешёвый класс вместо привычного «всё на самую сильную».
> - **Данные не уходят наружу.** Запросы с персональными или конфиденциальными данными остаются на моделях внутри контура, и никакая оптимизация стоимости это правило не отменяет.
> - **Каждое решение объяснимо одной строкой.** По заголовку ответа видно, почему выбрана именно эта модель.
> - **Честная оговорка:** сравнения качества и стоимости с режимами «всегда сильная» и «всегда дешёвая» пока нет — для него ещё не накоплена разметка.

> [!NOTE] О цикле «LLM-шлюз»
> Две статьи о собственном LLM-шлюзе компании. «Мы» — это автор и AI-агенты, которым он делегировал часть исследований и правок. Компания обезличена, цифры настоящие.
>
> 1. **SurfCap-α: как семантический роутер выбирает модель** ← вы здесь
> 2. [Guardrails в LLM-шлюзе: почему Shieldstral](/posts/guardrails-shieldstral)

> [!TERM]- Термины за минуту
>
> - **LLM-шлюз** — единая точка входа между клиентами (чатами, агентами, интеграциями) и языковыми моделями: маршрутизация, проверки, учёт, трассировка.
> - **`model=auto`** — клиент не выбирает модель сам, а поручает выбор шлюзу.
> - **Класс модели и тир** — класс описывает, какая модель нужна (дешёвая, кодовая, с длинным контекстом, «умная», самая сильная, мультимодальная, резидентная). Тир T0–T3 — уровень требований запроса, из которого выводится класс.
> - **Резидентная модель** — модель, развёрнутая внутри контура компании. Данные к ней не покидают периметр.
> - **ПДн** — персональные данные.
> - **Routing и cascading** — routing решает, какую модель вызвать, до генерации; cascading сначала спрашивает дешёвую модель и при неудачном ответе идёт к дорогой.
> - **α-порог** — порог на вероятность того, что сильная модель ответит лучше слабой. Выше α — дешевле и рискованнее.
> - **Capability-головы** — лёгкие классификаторы, оценивающие, нужны ли запросу рассуждение, код, отладка или инструменты.
> - **Circuit breaker** — предохранитель: сбойный провайдер временно выводится из ротации.
> - **Fallback** — переключение на запасной вариант при ошибке.
> - **p50** — медиана, например задержки.

## Задача и исходные условия {audience="biz arch"}

SurfCap-α — алгоритм автоматического выбора модели в собственном LLM-шлюзе: клиент присылает `model=auto`, а шлюз за доли миллисекунды решает, какой класс модели нужен этому запросу. Название — аббревиатура слоёв: **Surf**ace heuristics → **Cap**ability heads → **α**-preference, плюс исполнение с перескоками и каскад как страховка.

Исходные условия были типичными для корпоративного шлюза:

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

Требования к роутеру сформулировали так:

- решение до первого HTTP-вызова — меньше 1 мс;
- каждое решение объяснимо одной строкой;
- дорогие ML-компоненты опциональны и могут только *повышать* класс, но не понижать.

## Что говорит литература {audience="ml arch"}

Маршрутизация между LLM делится на два семейства: решение *до* генерации (routing) и решение *после* неё (cascading). Эта таксономия и каталог дешёвых признаков запроса хорошо изложены в двух обзорах [[7]](#литература) [[8]](#литература).

**Routing до генерации.** Hybrid LLM [[2]](#литература) обучает лёгкий энкодер предсказывать разрыв качества между малой и большой моделью. RouteLLM [[1]](#литература) сводит задачу к одной величине — вероятности того, что сильная модель ответит лучше слабой, — и порогу α над ней:

```latex
\text{route}(q) = \begin{cases} \text{strong}, \quad & P(\text{win}_{\text{strong}} \mid q) \ge \alpha \\ \text{weak}, \quad & \text{иначе} \end{cases}
```

Ручка α напрямую задаёт компромисс: выше α — дешевле и рискованнее, ниже α — ближе к «всегда сильная». HyDRA [[6]](#литература) идёт дальше двух моделей: четыре сигмоидные «головы способностей» (reasoning, code, debug, tools) поверх ModernBERT сопоставляются с профилями моделей в гетерогенном пуле; медиана на CPU — 86 мс.

**Cascading после генерации.** FrugalGPT [[3]](#литература) спрашивает дешёвую модель, оценивает ответ и при неудаче идёт к дорогой. AutoMix [[4]](#литература) заменяет внешний оценщик самопроверкой модели. Унифицирующая работа [[5]](#литература) показывает, что каскад выигрывает только тогда, когда оценщик качества достаточно точен.

> [!ML]
> Вывод [[5]](#литература) стал ключевым для нашего дизайна: без откалиброванного оценщика каскад лишь удваивает стоимость плохих ответов. Поэтому каскад у нас — страховка, выключенная по умолчанию, а не первый шаг.

Из обзора вытекал и главный инженерный вопрос: все обучаемые роутеры требуют размеченных пар «запрос → какая модель ответила лучше» на *своём* трафике. На старте таких данных нет, а решать нужно с первого дня.

## Устройство SurfCap-α {audience="arch eng"}

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

```mermaid {caption="Конвейер SurfCap-α, 7 слоёв: слои могут только поднять тир, резидентность перекрывает всё. Пунктир — реализовано, в бою выключено; жирная рамка — собственный инвариант, которого нет в литературе"}
flowchart TB
  subgraph pick["Выбор тира: каждый слой только поднимает тир ↑"]
    direction LR
    s["Surface<br/>правила T3 → T0,<br/>первое совпадение"]
    c["Capability<br/>головы по мотивам<br/>HyDRA"]
    a["α-порог<br/>RouteLLM,<br/>граница T1 ↔ T2"]
    s --> c --> a
  end
  subgraph run["Ограничение и исполнение"]
    direction LR
    r["Резидентность<br/>ПДн и конф. данные<br/>не уходят наружу"]
    b["Бюджет<br/>понижает до T0<br/>сверх лимита"]
    p["Провайдер<br/>перескоки<br/>400/429/5xx"]
    k["Каскад<br/>FrugalGPT /<br/>AutoMix"]
    r --> b --> p --> k
  end
  pick --> run
  style pick fill:none
  style run fill:none
  classDef off stroke-dasharray:5 5
  classDef key stroke-width:3px
  class c,a,k off
  class r key
```

Верхний ряд выбирает тир и может только поднимать его. Нижний ряд ограничивает и исполняет решение: провайдер выбирается внутри класса, сбойный уходит на паузу (circuit breaker, ~30 с), а каскад срабатывает как страховка уже после ответа.

### Слой 1. Surface — поверхностные признаки {audience="eng arch"}

Из текста и полей запроса извлекаются дешёвые признаки: длина в токенах, маркеры кода, математики, отладки, вызова инструментов, мультимодальности, «мешки намерений» и флаги риска (юридический, медицинский, персональные данные, безопасность, конфиденциальность). Регулярные выражения двуязычные и включают отраслевой словарь и ссылки на профильные законы. Из структуры запроса берутся признак «несколько намерений сразу» и глубина цепочки вызовов инструментов.

Правила проверяются сверху вниз, и побеждает первое совпадение (first match, highest tier). Скоры не суммируются: правило либо сработало, либо нет, поэтому решение всегда объяснимо.

| Тир | Условие | Класс модели |
| --- | --- | --- |
| T3 | есть флаг риска | самая сильная; при ПДн или конфиденциальных данных — резидентная (с файлом — резидентная мультимодальная) |
| T2 | отладка, инструменты, несколько намерений или ≥ 1200 токенов | «умная»; если сработала только длина — с длинным контекстом |
| T1 | маркеры кода или математики | кодовая |
| T0 | всё остальное | дешёвая |
{caption="Правила тиров: проверяются сверху вниз, побеждает первое совпадение"}

Мультимодальный вход на любом тире переключает класс на vision-модель, но сам по себе тир не поднимает.

### Слой 2. Capability — головы способностей {audience="ml eng"}

Слой перенят из HyDRA [[6]](#литература): если оценка головы превышает порог, тир поднимается до минимального тира профиля. Профили заданы в YAML: code ≥ 0.5 → T1; debug ≥ 0.5, tools ≥ 0.5, reasoning ≥ 0.7 → T2. Понижение запрещено на уровне кода, а не соглашения:

```latex
\text{tier}' = \max\big(\text{tier},\ \text{min\_tier}(h)\big) \quad \text{для всех } h:\ s_h(q) \ge \theta_h
```

### Слой 3. α-preference {audience="ml arch"}

Порог RouteLLM [[1]](#литература) применяется только на одной границе — между T1 и T2. Именно там эвристики неувереннее всего: код ли это на пару строк или архитектурный вопрос. T0 и T3 α не трогает: простые запросы дешевле не сделать, а рискованные нельзя удешевлять. По умолчанию α = 0.5.

### Инвариант резидентности {audience="biz arch"}

Если в запросе есть персональные или конфиденциальные данные, ни capability, ни α, ни бюджет не могут увести его с резидентной модели. В причину решения дописывается `residency:guard`.

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

### Слой 4. Мягкий бюджет {audience="biz eng"}

Каждый класс стоит условные единицы: дешёвая — 1, кодовая — 2, длинный контекст — 4, «умная» и vision — 5, самая сильная и резидентные — 6. При превышении мягкого лимита запрос понижается до дешёвого класса ещё до первого вызова.

### Исполнение и каскад {audience="eng arch"}

Класс модели и конкретный провайдер — разные слои. При ошибках 400, 429 и 5xx шлюз перескакивает на другого провайдера *того же* класса, а сбойный провайдер уходит на паузу примерно на 30 с. Так сбой не меняет ни качество, ни стоимость ответа. Каскад в духе FrugalGPT и AutoMix реализован, но выключен по умолчанию — именно по аргументу [[5]](#литература) о точности оценщика.

Каждое решение записывается одной строкой в заголовке ответа и в трассировке:

```text
heuristic:T1;capability:debug→T2;preference:p=0.80>=α=0.50;budget:degrade_to=cheap;cascade
```

## Реализация {audience="eng"}

Шлюз написан с нуля на FastAPI, без готового прокси. Правила тиров, регулярные выражения, профили голов и α живут в YAML, поэтому политику маршрутизации можно менять без релиза. Движок правил знает четыре предиката (равенство, «не меньше», «не пусто», пересечение множеств) и их комбинации any_of / all_of.

Путь запроса:

1. Детектор секретов проверяет вход. В режиме наблюдения он добавляет флаги риска, которые дальше влияют на тир.
2. SurfCap-α выбирает класс: признаки → правила → capability → α → резидентность.
3. Бюджет при необходимости понижает класс.
4. Параллельно запускается проверка guardrails (о ней — [вторая статья цикла](/posts/guardrails-shieldstral)); её результат нужен только перед вызовом провайдера.
5. Выбор провайдера внутри класса, исполнение с перескоками, пассивный circuit breaker и активные health-пробы.
6. Причина решения уходит в заголовок `X-Route-Decision`, в структурный лог и в дерево трассировки.

**Бюджет задержки** заложен на этапе проектирования: признаки и правила — меньше 1 мс до первого HTTP-вызова; capability-головы на CPU — 50–100 мс (ориентир — 86 мс у HyDRA), α-оценщик — ещё 10–30 мс. Именно поэтому ML-слои опциональны: они стоят на два порядка дороже эвристик и оправданы только там, где эвристики ошибаются.

**Данные для калибровки.** Здесь проходит граница между планом и боем. Интерфейсы capability и α реализованы и покрыты тестами с подставными оценщиками, но весов моделей в бою нет: для обучения нужна собственная разметка. Оценщик качества для каскада пока тоже заглушка. Источник разметки уже есть: каждое решение с причиной пишется в журнал запросов, а скрипт офлайн-оценки умеет сравнивать режимы «всегда дешёвая», «всегда сильная», «только эвристики», «+capability», «+α» и «+каскад».

> [!WARNING] Что сейчас работает в бою
> Решение принимают эвристики, бюджет и инвариант резидентности. Capability-головы, α-оценщик и каскад реализованы, но выключены, пока не накопится разметка. Поскольку головы и α могут только повышать тир, их включение не может ухудшить качество — только стоимость.

## Результаты и ограничения {audience="biz ml"}

За 19 часов работы текущего процесса через `model=auto` прошло 826 запросов. Почти половина из них ушла не на дешёвую модель, а больше четверти — в T3 из-за флагов риска. Цифры — из Prometheus-счётчиков шлюза; задержка — среднее время всей стадии маршрута (детектор секретов, признаки, правила, бюджет) без подсчёта токенов и вызова модели.

| Тир | Запросов | Доля | Выбор маршрута, мс (среднее) |
| --- | ---: | ---: | ---: |
| T0 — дешёвая | 470 | 56.9% | 0.7 |
| T1 — кодовая | 2 | 0.2% | 7.7 |
| T2 — «умная» / длинный контекст | 124 | 15.0% | 34.2 |
| T3 — самая сильная / резидентные | 230 | 27.8% | 55.5 |
{caption="Распределение запросов model=auto по тирам за 19 часов работы процесса, 826 запросов"}

Из этого следуют три вывода.

- **Резидентность — самый заметный эффект.** 176 из 230 запросов T3 ушли на резидентные модели. Без инварианта они бы ушли во внешнюю сильную модель.
- **Дешёвый путь укладывается в бюджет.** Для T0 выбор маршрута занимает в среднем 0.7 мс при цели меньше 1 мс. У сложных классов задержка в десятки раз выше, и это ожидаемо: туда попадают длинные документы, а регулярные выражения проходят весь текст. Разложить эти десятки миллисекунд по стадиям ещё предстоит. На фоне секунд генерации они незаметны.
- **Граница T1↔T2 почти не встречается.** В T1 попало всего 2 запроса: правила T2 стоят выше, и кодовые запросы с отладкой или длинным контекстом сразу уходят в T2. Значит, для нашего трафика α на этой границе даст мало. Перспективнее вернуть часть T2 в более дешёвый класс.

> [!CAUTION] Ограничения
>
> - Офлайн-сравнения с режимами «всегда сильная» и «всегда дешёвая» по качеству и стоимости пока нет: нет размеченного набора.
> - Эвристики проверены только синтетическим набором из 30 кейсов на флаги риска (все 30 проходят) и смоук-тестом из 8 сценариев по тирам.
> - Регулярные выражения улавливают только то, что в них заложено, поэтому флаги риска скорее избыточны, чем недостаточны. Для резидентности это правильный перекос, для стоимости — нет.

### Что здесь собственное, а что заимствовано {audience="ml arch"}

Отдельные техники взяты из литературы: α-порог, головы способностей, каскад с самопроверкой, каталог признаков. Собственный вклад — в том, как они скомпонованы:

- детерминированные правила идут первыми, ML может только поднять тир, а каскад служит страховкой, а не первым шагом;
- α применяется только на одной границе тиров;
- класс модели отделён от провайдера, перескоки идут строго внутри класса;
- мягкий бюджет считается в условных единицах;
- инвариант резидентности перекрывает любую оптимизацию;
- каждое решение объясняется одной строкой.

Дальше по циклу — [guardrails](/posts/guardrails-shieldstral): проверка входа, которая идёт параллельно с выбором маршрута.

## Литература {audience="ml" .references}

1. Ong I. et al. [RouteLLM: Learning to Route LLMs with Preference Data](https://arxiv.org/abs/2406.18665). arXiv:2406.18665.
2. Ding D. et al. [Hybrid LLM: Cost-Efficient and Quality-Aware Query Routing](https://arxiv.org/abs/2404.14618). arXiv:2404.14618.
3. Chen L., Zaharia M., Zou J. [FrugalGPT: How to Use Large Language Models While Reducing Cost and Improving Performance](https://arxiv.org/abs/2305.05176). arXiv:2305.05176.
4. Aggarwal P. et al. [AutoMix: Automatically Mixing Language Models](https://arxiv.org/abs/2310.12963). arXiv:2310.12963.
5. Dekoninck J. et al. [A Unified Approach to Routing and Cascading for LLMs](https://arxiv.org/abs/2410.10347). arXiv:2410.10347.
6. Garg A. et al. [HyDRA: Hybrid Dynamic Routing Architecture for Heterogeneous LLM Pools](https://arxiv.org/abs/2605.17106). arXiv:2605.17106.
7. Varangot-Reille C. et al. [Doing More with Less: A Survey on Routing Strategies for Resource Optimisation in Large Language Model-Based Systems](https://arxiv.org/abs/2502.00409). arXiv:2502.00409.
8. Moslem Y. et al. [Dynamic Model Routing and Cascading for Efficient LLM Inference: A Survey](https://arxiv.org/abs/2603.04445). arXiv:2603.04445.

Названия и первые авторы сверены с arXiv API 25 октября 2026.

