Multi-provider LLM: архитектура роутинга в production

Автор: Обновлено

Что такое multi-provider роутинг LLM?

Multi-provider роутинг LLM — это управляющий слой, который выбирает допустимую модель для каждой задачи, соблюдает лимиты времени и стоимости, обрабатывает временные сбои провайдера и проверяет результат по единому контракту приложения.

TL;DR

  • -Список провайдеров — ещё не стратегия роутинга: сначала определите контракт задачи и правила допуска модели
  • -Повторяйте только временные сбои, держите все попытки в одном deadline и не делайте retry невалидного запроса
  • -Fallback безопасен, только если запасная модель проходит тот же контракт схемы и качества
  • -Сбой стрима нельзя скрыть после отправки данных клиенту: сценарий перезапуска или восстановления нужен заранее
  • -Подключайте второго провайдера через shadow-режим и canary, а не сразу через автоматический failover

Multi-provider роутинг LLM — не список «если модель A упала, попробуй B». Это слой совместимости и правил между задачей приложения и несколькими меняющимися API.

Полезный вопрос звучит не «какой провайдер лучше?», а так:

Какие провайдеры допустимы для этой задачи и что обязано остаться неизменным при смене маршрута?

Здесь разобран слой провайдеров и gateway. В руководстве по production LLM stack он связан с промптами, evals, observability и управлением релизами.

Нужен ли вам второй провайдер

Не начинайте с multi-provider инфраструктуры по умолчанию. У неё есть реальная цена поддержки:

  • разные форматы запросов и streaming-семантика;
  • разное поведение tool calls и structured output;
  • ещё один набор ключей и условий обработки данных;
  • увеличенная матрица evals;
  • более сложный разбор инцидентов.

Сначала спрячьте одного провайдера за внутренним адаптером. Второго добавляйте, когда измерим хотя бы один существенный риск:

  • задача не выдерживает наблюдаемое окно недоступности;
  • capacity или rate limits блокируют ожидаемый трафик;
  • требования к региону или данным исключают основной маршрут;
  • другая модель доказала преимущество качества к стоимости на конкретной задаче;
  • миграция при снятии модели с поддержки займёт слишком много времени.

Если фоновая задача может подождать десять минут, отложенный retry часто безопаснее смены модели. Если ответ подтверждает платёж или меняет данные клиента, лучше не вернуть ответ, чем принять семантически другой fallback.

Начните с контракта задачи

Приложение должно запрашивать задачу, а не коммерческое имя модели. Требования удобно хранить в реестре задач:

interface TaskPolicy {
  task: 'support_reply' | 'invoice_extract' | 'trip_plan';
  requiredCapabilities: Array<'tools' | 'json_schema' | 'vision'>;
  allowedRegions: string[];
  maxInputTokens: number;
  deadlineMs: number;
  maxAttempts: number;
  outputSchema: string;
  evaluationSuite: string;
}

Названия моделей не должны расползаться по продуктовому коду. Отдельный реестр связывает стабильные внутренние имена с deployment провайдеров:

DeploymentВозможностиРегион данныхДопущенные задачиСтатус
reply-primarytools, streamingEUответ поддержкиactive
reply-secondarytools, streamingEUответ поддержкиcanary
extract-primaryJSON schemaUSразбор счётаactive

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

Нормализуйте минимально полезный контракт

Gateway должен скрывать транспортные различия, но не притворяться, что все модели ведут себя одинаково. Нормализуйте только то, на что приложение действительно может опереться:

interface LlmRequest {
  requestId: string;
  task: TaskPolicy['task'];
  messages: Array<{ role: 'system' | 'user' | 'assistant'; content: string }>;
  responseFormat: { type: 'text' } | { type: 'json'; schema: string };
  stream: boolean;
}

interface LlmResult {
  text: string;
  provider: string;
  model: string;
  routeReason: string;
  attempt: number;
  inputTokens?: number;
  outputTokens?: number;
}

Специфичные функции провайдера оформляйте как явные capabilities. Если при fallback незаметно пропал параметр reasoning, caching или tool call, это ошибка корректности. Задача, которой нужен такой параметр, не должна попасть на маршрут без его поддержки.

Нужна и стабильная внутренняя классификация ошибок:

  • invalid_request — приложение отправило плохой запрос;
  • authentication — неверные ключи или права;
  • policy_rejection — провайдер отклонил контент;
  • rate_limited — capacity может восстановиться позже;
  • provider_unavailable — соединение или сервер провайдера недоступны;
  • deadline_exceeded — бюджет времени исчерпан;
  • output_invalid — ответ нарушил контракт приложения.

Эта классификация важнее исходного HTTP-статуса. Провайдеры возвращают разные тела ошибок, а одинаковые 4xx или 5xx не всегда требуют одинаковой реакции.

Сделайте роутинг детерминированным

Причину production-маршрута нужно уметь восстановить после запроса. Практический порядок:

  1. Загрузить policy задачи и текущую версию маршрута.
  2. Убрать deployment без обязательной capability.
  3. Применить ограничения региона, хранения данных и tenant.
  4. Убрать варианты, куда не помещаются input или оставшийся deadline.
  5. Убрать модели, не прошедшие evaluation gate этой задачи.
  6. Применить rollout percentage и состояние circuit breaker.
  7. Выбрать по объявленному правилу: приоритету, измеренной latency или потолку стоимости.

На каждом вызове сохраняйте версию и причину маршрута. Ответ «так выбрал router» бесполезен во время инцидента.

Managed gateway может реализовать часть policy. Например, в документации Cloudflare AI Gateway у Dynamic Routing есть версионируемые маршруты с условиями, процентным распределением, выбором модели, rate и budget nodes. С таким маршрутом нужно работать как с production-конфигурацией: review изменений, осознанное продвижение версии и готовый rollback.

Retry и fallback — разные решения

Retry повторяет запрос на том же deployment. Fallback меняет deployment, а иногда и поведение модели. Не смешивайте эти решения.

СбойRetry там же?Другой deployment?Комментарий
Обрыв соединения до ответаОдин раз, если есть времяДаВременный transport-сбой
Таймаут запросаОбычно нетВозможноПервый запрос может ещё выполняться
Rate limitПосле указанной задержки или никакДаУчитывайте backoff провайдера
Серверная ошибка провайдераНе более одного разаДаПри серии ошибок разомкните circuit
Невалидный запросНетНетИсправьте caller
Ошибка авторизацииНетНетИсправьте или замените credentials
Policy rejectionНетТолько по явному правилуНе обходите safety policy
Невалидный outputНе вслепуюТолько на проверенный вариантСохраните контракт ответа

Все попытки делят один end-to-end deadline. Три провайдера с таймаутом 30 секунд дают не 30-секундный сервис, а худший сценарий на 90 секунд.

async function runTask(
  request: LlmRequest,
  policy: TaskPolicy,
  candidates: ProviderAdapter[],
): Promise<LlmResult> {
  const startedAt = Date.now();
  const attempts = candidates.slice(0, policy.maxAttempts);

  for (const [index, adapter] of attempts.entries()) {
    const remainingMs = policy.deadlineMs - (Date.now() - startedAt);
    if (remainingMs <= 0) throw new Error('deadline_exceeded');

    try {
      const result = await adapter.complete(request, remainingMs);
      await validateOutput(result.text, policy.outputSchema);
      return { ...result, attempt: index + 1 };
    } catch (error: unknown) {
      const kind = classifyProviderError(error);
      if (!isFallbackEligible(kind)) throw error;
    }
  }

  throw new Error('provider_unavailable');
}

Для краткости здесь нет логов и abort signal. В рабочем коде отменяйте просроченный fetch через AbortSignal, переносите request ID через все попытки и не записывайте в лог сырые промпты с персональными данными.

Circuit breaker защищает от шторма retries

Если провайдер деградировал, пропускать каждый новый запрос через тот же таймаут опасно. Circuit breaker временно убирает deployment из роутинга после заданного окна ошибок. После паузы ограниченный probe-трафик проверяет восстановление.

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

Пороги берите из SLO и реального объёма трафика, а не из чужой статьи. При малом трафике пять последовательных сбоев могут быть информативнее минутного процента. При большом — устойчивее rolling rate с минимальным размером выборки.

Streaming меняет контракт сбоя

До первого байта gateway может отбросить неудачную попытку и выбрать другой допустимый deployment. После отправки данных клиенту незаметное переключение уже небезопасно: вторая модель не знает скрытое состояние первой генерации.

Заранее выберите поведение приложения:

  • показать, что ответ оборван, и кнопку «повторить»;
  • начать весь ответ заново с новым generation ID;
  • для критичной задачи сначала буферизовать полный ответ;
  • ставить контрольные точки между нестриминговыми шагами workflow.

Не дописывайте вывод второй модели к оборванному стриму и не называйте это восстановлением. Особенно опасны tool calls: клиент мог уже выполнить действие до разрыва соединения.

Защитите side effects через idempotency

LLM-запрос может завершиться у провайдера, хотя gateway получил таймаут. Слепой retry способен дважды сгенерировать ответ, дважды списать оплату или дважды выполнить tool.

У каждой логической операции должен быть idempotency key на уровне приложения. Состояние исполнения tools храните отдельно от генерации. Перед действием проверяйте, не завершена ли уже эта операция. Модель может повторить текст; платёж, письмо или изменение базы — нет.

Для agent workflow граница выглядит так:

  1. модель предлагает типизированное действие;
  2. приложение проверяет policy и аргументы;
  3. idempotency store резервирует операцию;
  4. приложение выполняет tool;
  5. результат фиксируется до следующего шага модели.

Валидируйте каждый fallback-ответ

Transport-совместимость не означает семантическую совместимость. Две модели могут принять одну JSON Schema, но по-разному пропускать поля, выбирать tools, приводить источники или отказываться от ответа.

Проверяйте одинаковые postconditions на любом маршруте:

  • парсинг и схему structured output;
  • доменные ограничения поверх формы JSON;
  • допустимые имена tools и их аргументы;
  • обязательные ссылки или идентификаторы источников;
  • детерминированные проверки permissions и safety;
  • выборочную оценку на task-specific evaluation set.

Модель становится допустимой только после offline eval и canary на этой задаче. Подробнее об evaluation layer — в руководстве по тестированию AI-агентов.

Держите data policy на gateway

Централизованный роутинг централизует и риск. Gateway видит промпты, вложения, tenant identifiers и ключи провайдеров.

Минимальный набор мер:

  • хранить ключи в secret manager, а не в клиенте или route-файле;
  • разрешать провайдеров по tenant и классу данных;
  • по возможности удалять или токенизировать чувствительные поля до вызова;
  • явно задавать sampling и retention логов;
  • разделять внешний request ID и внутренние trace-данные;
  • не разрешать fallback нарушать регион или policy хранения;
  • аудитить изменения маршрутов и credentials.

Fallback провайдера не должен превращаться в fallback compliance.

Наблюдайте решение, а не только вызов

Latency и error rate необходимы, но их мало. Записывайте:

  • задачу и версию task policy;
  • версию и причину маршрута;
  • попытки и выбранный deployment;
  • фактические provider и model из адаптера;
  • время каждой попытки и общий расход deadline;
  • шаг fallback и нормализованный тип ошибки;
  • input/output tokens, если они доступны;
  • оценку стоимости по версионируемой таблице цен;
  • результат schema validation и eval;
  • версию промпта или workflow без сырых персональных данных.

Алерты нужны на устойчивые отклонения от baseline: исчерпанные deadline, открытые circuits, долю fallback, невалидные ответы и стоимость успешной задачи. Низкий provider error rate ничего не значит, если ответы fallback нарушают контракт.

Проектирование traces разобрано в руководстве по LLM observability.

Выберите минимально достаточный control layer

Есть три распространённых варианта.

ВариантСильная сторонаЦена
Адаптер внутри приложенияМаленький, явный, легко отлаживатьЛогика повторяется между сервисами
Managed AI gatewayБыстрый запуск, hosted routing и analyticsЧужая модель policy и ещё один control plane
Self-hosted proxyЕдиные policy и граница credentialsНа вас обновления, scaling и availability

Для первых двух провайдеров часто хватает адаптера в приложении. Managed gateway подходит команде, которой нужны централизованные правила без эксплуатации proxy. Self-hosted proxy вроде LiteLLM полезен, если нескольким сервисам нужен один слой совместимости. Но сам proxy становится production-инфраструктурой: его failure domain, обновления и авторизация требуют обычной инженерной дисциплины.

Не стройте новую интеграцию на deprecated-механизме. Например, Cloudflare сейчас направляет новые сценарии роутинга в Dynamic Routing, а не в старый формат fallback через Universal Endpoint. Перед копированием gateway-конфига из старой статьи проверяйте актуальную документацию продукта.

Как безопасно подключить второго провайдера

Используйте поэтапную миграцию:

  1. Выделите адаптер. Сохраните поведение за стабильным внутренним API.
  2. Соберите evaluation set задачи. Добавьте обычные, граничные, refusal и tool cases из реальных сбоев.
  3. Подключите кандидата без live-трафика. Нормализуйте ошибки и output.
  4. Запустите shadow для допустимых запросов. Не показывайте и не исполняйте теневые результаты.
  5. Сравните contract pass rate, latency и стоимость успешной задачи. Средняя цена токена сама по себе ничего не решает.
  6. Дайте небольшой обратимый canary. Сохраните основной маршрут.
  7. Включите bounded fallback для одной задачи. Следите за total deadline и долей невалидных ответов.
  8. Продвиньте или откатите версию маршрута. Зафиксируйте причину.

Shadow-трафик дублирует чувствительные данные и расходы провайдера. На него распространяются те же consent, retention и региональные правила, что и на production-трафик.

Production checklist

  • Продуктовый код вызывает стабильные задачи, а не модели провайдеров.
  • У задачи заданы capabilities, регион, deadline, попытки и контракт output.
  • Provider-specific функции объявлены явно и не теряются при fallback.
  • Retry и fallback используют нормализованную классификацию ошибок.
  • Все попытки делят один deadline и поддерживают отмену.
  • Circuit breaker работает на уровне deployment и игнорирует ошибки caller.
  • У обрыва streaming есть понятный пользовательский сценарий.
  • Side effects tools защищены application-level idempotency.
  • Каждая допустимая модель прошла один task-specific evaluation suite.
  • Fallback не обходит data, safety и tenant policy.
  • Версии и причины маршрута, попытки и валидность output наблюдаемы.
  • Есть проверенный rollback-маршрут.

Первоисточники

Архитектура работает, когда смена провайдера не меняет контракт задачи, не нарушает policy и не превращает один временный сбой в цепочку безграничных попыток. Остальное — детали реализации.

Часто задаваемые вопросы

Каждому LLM-приложению нужны несколько провайдеров?
Нет. Второй провайдер добавляет работу с интеграцией, evals, безопасностью и эксплуатацией. Начните с одного провайдера за внутренним адаптером. Добавляйте следующего, когда это оправдано измеримым риском доступности, лимитов, compliance или стоимости.
Какие ошибки должны включать LLM-fallback?
Обычно это таймауты, ошибки соединения, rate limits и некоторые серверные ошибки. Невалидный запрос, проблема авторизации, policy rejection и нарушение контракта ответа требуют отдельной обработки. Перебор всех провайдеров в этих случаях часто только увеличивает расходы.
Может ли gateway незаметно восстановить оборванный streaming-ответ?
Не после того, как первые байты ушли клиенту. Приложение должно показать прерванное состояние и либо начать генерацию заново, либо продолжить с собственной контрольной точки. Автоматический fallback безопаснее до отправки первого байта.