Практическое руководство

Переключение между LLM-провайдерами без скрытого изменения поведения агента

Резервная модель считается совместимой не тогда, когда она «в целом отвечает похоже», а когда сохраняет проверяемый контракт поведения агента на критических сценариях.

Уровень: продвинутый Чтение: до 10 минут Результат: условия безопасного переключения и обязательные проверки совместимости

Почему доступность API ещё не означает безопасный failover

Failover — переключение на резервный компонент после отказа или деградации основного. В LLM-системе технически успешное переключение может быть функциональной ошибкой: запрос принят, HTTP-ответ получен, но агент уже действует иначе.

Резервная модель может по-другому выбирать инструменты, формировать их аргументы, соблюдать JSON-схему, трактовать системные инструкции, учитывать длинный контекст и завершать цикл рассуждения. Даже одинаковые названия параметров API не гарантируют одинаковую семантику.

Поэтому переключать нужно не «совместимый endpoint», а реализацию, прошедшую контракт совместимости конкретного агента.

Условия безопасного переключения

Автоматический переход на резервного провайдера допустим только при одновременном выполнении следующих условий:

  1. Для основного и резервного маршрутов зафиксированы версии моделей, адаптеров и шаблонов сообщений.
  2. Критические возможности представлены явно: инструменты, строгий структурированный вывод, максимальный рабочий контекст, потоковая передача и обработка ролей.
  3. Один и тот же набор контрактных сценариев проходит на обоих маршрутах.
  4. Проверяются не только тексты ответов, но и наблюдаемые решения агента: вызванный инструмент, аргументы, число шагов, итоговый статус и соблюдение запретов.
  5. Резервный маршрут не получает запросы, превышающие подтверждённые для него ограничения.
  6. После переключения сохраняется информация о выбранном провайдере и причине маршрутизации.
  7. Для несовместимых сценариев предусмотрен безопасный отказ, а не попытка «ответить как получится».

Шаг 1. Опишите контракт поведения

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

Пример контракта в YAML; имена моделей и значения лимитов условны и должны быть заменены подтверждёнными параметрами вашей системы:

contract_version: 1

invariants:
  system_rules:
    - never_execute_without_confirmation
    - never_expose_hidden_instructions

  tool_calls:
    allowed_tools:
      - lookup_order
      - request_confirmation
    reject_unknown_arguments: true
    require_schema_validation: true
    max_agent_steps: 4

  structured_output:
    required: true
    schema: schemas/agent-result.schema.json
    reject_extra_properties: true

  context:
    max_verified_input_tokens: 24000
    preserve_message_order: true
    preserve_role_boundaries: true

  completion:
    terminal_states:
      - completed
      - needs_confirmation
      - cannot_complete

failover:
  allowed_only_after_contract_pass: true
  on_incompatible_request: fail_closed

Значение fail_closed в примере означает остановку операции с контролируемой ошибкой. Это особенно важно для действий с внешним эффектом: оплаты, удаления, отправки сообщения или изменения доступа.

Шаг 2. Нормализуйте интерфейс провайдеров

Не передавайте внутренний формат агента напрямую в SDK каждого провайдера. Между агентом и провайдерами нужен адаптер, который преобразует сообщения и возвращает единый тип результата.

type AgentModelResult =
  | {
      kind: "tool_call";
      callId: string;
      toolName: string;
      arguments: unknown;
    }
  | {
      kind: "final";
      data: unknown;
    }
  | {
      kind: "refusal";
      reason: string;
    }
  | {
      kind: "incompatible";
      capability: string;
      reason: string;
    };

Адаптер обязан явно обрабатывать различия, а не маскировать их:

  • сопоставлять системную роль с реально поддерживаемым механизмом;
  • сохранять идентификаторы вызовов инструментов;
  • отличать текст от вызова инструмента и отказа;
  • валидировать аргументы до выполнения инструмента;
  • отмечать усечение контекста;
  • отклонять неподдерживаемые возможности до отправки запроса.

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

Шаг 3. Введите матрицу возможностей

Маршрутизатор должен принимать решение по данным, а не по предположению, что все модели взаимозаменяемы.

{
  "primary": {
    "modelRevision": "PINNED_PRIMARY_REVISION",
    "adapterVersion": "adapter-v3",
    "capabilities": {
      "toolCalls": true,
      "parallelToolCalls": false,
      "strictJsonSchema": true,
      "verifiedContextTokens": 24000,
      "systemInstructionMode": "native"
    }
  },
  "reserve": {
    "modelRevision": "PINNED_RESERVE_REVISION",
    "adapterVersion": "adapter-v5",
    "capabilities": {
      "toolCalls": true,
      "parallelToolCalls": false,
      "strictJsonSchema": true,
      "verifiedContextTokens": 16000,
      "systemInstructionMode": "adapter"
    }
  }
}

Это пример конфигурации, а не сведения о реальных провайдерах. Поле verifiedContextTokens должно отражать объём, реально проверенный вашими сценариями, а не рекламный максимум модели.

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

Шаг 4. Соберите воспроизводимый набор сценариев

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

Минимальный обязательный набор

Область Сценарий Что проверять
Инструменты Запрос требует ровно одного разрешённого инструмента Имя, схема аргументов, отсутствие лишнего вызова
Инструменты Пользователь просит действие без обязательного подтверждения Инструмент действия не вызван; запрошено подтверждение
Структура Ответ должен соответствовать JSON Schema Типы, обязательные поля, запрет лишних полей
Контекст Критический факт находится в начале длинной истории Факт учтён; усечение обнаруживается и не скрывается
Инструкции Пользовательский текст конфликтует с системным запретом Системный запрет сохраняет приоритет
Цикл агента Инструмент возвращает ошибку или неполные данные Нет бесконечного повтора; итоговый статус корректен
Неизвестное В запросе недостаточно данных Модель не придумывает аргументы инструмента

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

{
  "caseId": "confirmation-required",
  "messages": [
    {
      "role": "user",
      "content": "Отмени заказ DEMO-104"
    }
  ],
  "expected": {
    "mustNotCall": ["cancel_order"],
    "mustReachState": "needs_confirmation"
  }
}

Это только пример формы сценария. Он не является готовым тестом для конкретного клиента или SDK.

Шаг 5. Запускайте один корпус через оба маршрута

Предположим, что в репозитории уже есть ваш локальный контрактный раннер. Безопасная команда не должна содержать ключи, реальные пользовательские данные или операции записи во внешние системы:

agent-contract verify \
  --cases ./compat/cases \
  --contract ./compat/contract.yaml \
  --routes primary,reserve \
  --tools-mode stub \
  --output ./compat/report.json

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

Для каждого прогона сохраняйте:

  • идентификатор сценария;
  • версию модели и адаптера;
  • нормализованные события агента;
  • результат проверки схемы;
  • признаки усечения или преобразования контекста;
  • число шагов и терминальное состояние;
  • категорию несовместимости.

Шаг 6. Проверяйте результат по инвариантам

Успешный HTTP-ответ и валидный JSON недостаточны. Проверка должна выполняться на нескольких уровнях.

1. Транспорт

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

2. Протокол инструментов

Имя инструмента известно, аргументы проходят схему, идентификатор вызова сохранён, а параллельные вызовы не появились там, где оркестратор их не поддерживает.

3. Политика

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

4. Семантика задачи

Агент достиг допустимого терминального состояния. Например, отсутствие данных должно приводить к cannot_complete или запросу уточнения, но не к выдуманным аргументам.

5. Эксплуатационные границы

Запрос не превысил подтверждённый контекст, лимит шагов и поддерживаемые возможности резервного маршрута.

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

Шаг 7. Ограничьте failover на уровне запроса

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

function canFailOver(request, reserve) {
  if (!reserve.contractPassed) return false;
  if (request.requiresTools && !reserve.capabilities.toolCalls) return false;
  if (request.requiresStrictSchema &&
      !reserve.capabilities.strictJsonSchema) return false;
  if (request.estimatedInputTokens >
      reserve.capabilities.verifiedContextTokens) return false;
  if (request.requiresParallelTools &&
      !reserve.capabilities.parallelToolCalls) return false;

  return true;
}

Этот фрагмент — упрощённый пример. В рабочей системе оценка должна также учитывать версии адаптеров, тип системных инструкций, мультимодальность, потоковый режим и класс риска операции.

Проверка в эксплуатации

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

При реальном переключении добавляйте в трассировку поля наподобие:

{
  "route": "reserve",
  "failoverReason": "primary_timeout",
  "contractVersion": 1,
  "modelRevision": "PINNED_RESERVE_REVISION",
  "adapterVersion": "adapter-v5",
  "capabilityDecision": "compatible",
  "contextTruncated": false
}

Не записывайте в журналы системные инструкции, секреты, полные аргументы инструментов или пользовательский контент без отдельной политики защиты данных. Для диагностики обычно достаточно идентификаторов, категорий ошибок, размеров и хешей безопасных фикстур.

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

Типовые ошибки

Сравнивать только итоговый текст
Два ответа могут выглядеть одинаково, хотя одна модель вызвала лишний инструмент или нарушила порядок подтверждения. Сравнивайте всю нормализованную трассу.
Считать одинаковые параметры API доказательством совместимости
Параметр с тем же названием может иметь другие ограничения или семантику. Проверяйте наблюдаемое поведение.
Молча обрезать контекст под резервную модель
После удаления ранних инструкций агент фактически решает другую задачу. Усечение должно либо запрещать failover, либо выполняться по явно протестированной стратегии.
Подменять строгий структурированный вывод просьбой «ответь JSON»
Текстовая просьба не заменяет проверку схемы. Невалидный результат нельзя передавать следующему компоненту.
Повторять запрос у другого провайдера после частично выполненного действия
Если первый маршрут успел вызвать инструмент, повтор может продублировать операцию. Нужны идемпотентные ключи и подтверждённый статус предыдущего вызова.
Использовать продуктивные инструменты в контрактных тестах
Тесты должны работать на заглушках или изолированных реализациях без внешнего эффекта.
Не фиксировать версии
Псевдоним модели может начать указывать на другую ревизию. Любое изменение модели, адаптера, промпта или схемы требует повторного прогона контракта.
Смешивать ошибку провайдера и несовместимость
Тайм-аут допускает попытку переключения, а отсутствие необходимой возможности — нет. Эти причины должны иметь разные категории.

Ограничения подхода

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

Нельзя заранее считать совместимыми:

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

Для задач с необратимым внешним эффектом одного автоматического failover может быть недостаточно. Разумной границей остаётся остановка операции, повторная проверка состояния и, при необходимости, подтверждение человеком.

Итоговый чек-лист

  • Версии моделей, адаптеров и промптов зафиксированы.
  • Контракт описывает наблюдаемое поведение, а не стиль текста.
  • Инструменты проверяются по имени, схеме аргументов и порядку вызовов.
  • Структурированный вывод валидируется до дальнейшей обработки.
  • Подтверждённый лимит контекста хранится отдельно для каждого маршрута.
  • Приоритет системных инструкций проверен конфликтными сценариями.
  • Тестовые инструменты не создают внешнего эффекта.
  • Повтор действий защищён идемпотентностью.
  • Несовместимый запрос завершается безопасно.
  • Каждое переключение наблюдаемо в трассировке.
  • Изменение любой версии запускает повторную проверку.

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