Fallback провайдера LLM через FSM с llm-nano-vm: пошаговый гайд | AiManual
AiManual Logo Ai / Manual.
19 Июн 2026 Гайд

Устойчивый LLM-пайплайн: реализация fallback провайдера через FSM с llm-nano-vm

Как построить отказоустойчивый LLM-пайплайн с конечным автоматом и llm-nano-vm. Код, примеры, ошибки. Fallback OpenAI -> Anthropic -> Ollama локально.

Реклама
cliv2

Когда пайплайн превращается в рулетку

Ты собрал крутую систему. Промпты летят в OpenAI, ответы приходят как по маслу. Но в один прекрасный понедельник в 14:32 API отвечает 503. Или тыкает лимит в 429. Или, что ещё веселее, возвращает что-то похожее на JSON, но не совсем. Твоя кредитная заявка AI не прошла — клиент не получил одобрение, бизнес теряет деньги. Знакомая боль?

Проблема не в выборе провайдера — проблема в том, как ты обрабатываешь его отказ. Стандартный retry с exponential backoff? Работает, но тупо. Вызов одного провайдера за другим в цикле? Теряешь контекст и деньги на холостых запросах. А если пайплайн многошаговый? Один шаг упал — всё по новой. Нам нужен конечный автомат (FSM), который хранит состояние и умеет переключать провайдера не просто по списку, а по логике бизнес-процесса.

В этой статье я покажу, как с помощью легковесного движка llm-nano-vm (версия 3.2.0 на 19.06.2026) построить устойчивый пайплайн с автоматическим fallback между OpenAI (GPT-5), Anthropic (Claude 4 Opus) и локальной Ollama (Llama 4). Без громоздких библиотек, с понятным кодом и без потери контекста.

Почему try-except — это не решение

Допустим, ты написал что-то вроде:

for provider in [openai, anthropic, ollama]:    try:        return provider.call(prompt)    except Exception:        continue

Выглядит логично. Но что, если anthropic.call не выбросил исключение, а вернул мусор? Или если после сбоя OpenAI ты хочешь на следующем шаге снова попробовать OpenAI — а не застревать на Ollama навсегда? А если у тебя три шага в пайплайне: сначала извлечение данных, потом генерация, потом валидация — разные шаги могут требовать разных провайдеров?

Try-except не помнит состояние. Он слепой. Нам нужно состояние: какой провайдер сейчас активен, сколько раз он уже падал, какие альтернативы остались, нужно ли временно заблокировать упавший сервис на N секунд.

Тут и приходит FSM. Конечный автомат — это просто граф: состояние -> событие -> переход. Состояния: PRIMARY, FALLBACK_1, FALLBACK_2, ERROR. События: success, timeout, api_error, rate_limit, invalid_response. И так далее.

Мы уже писали о похожем подходе в статье Стратум: детерминированные LLM-системы — там тоже используется FSM, но для других целей. А сейчас применим этот же принцип к fallback провайдера.

llm-nano-vm: что это за зверь

llm-nano-vm — это минималистичная виртуальная машина для LLM-пайплайнов. Она не тянет за собой Kubernetes, не требует RabbitMQ, не разворачивает базу данных. Это одна библиотека, которая парсит YAML/JSON конфиг с описанием состояний, переходов и действий. На выходе — Python-объект с методами run() и feed_event(). Версия 3.2.0 (релиз марта 2026) добавила поддержку асинхронных обработчиков и встроенный механизм circuit breaker.

Альтернативы вроде LiteLLM или LangChain — это тяжеловесы. Они хороши для прототипов, но когда нужен контроль над каждым переходом, они начинают мешать. llm-nano-vm даёт именно тот уровень детализации, который нужен для продакшена. И да, он умеет работать с любыми HTTP-клиентами, так что ты можешь дергать как облачные API, так и локальный Ollama.

Важно: llm-nano-vm не заменяет вызовы LLM. Он только управляет логикой fallback. Сам вызов — твой код, просто обёрнутый в хендлер. Так что гибкость сохраняется.

Строим пайплайн на FSM: пошагово

1Установка и базовая конфигурация

Ставим:

pip install llm-nano-vm==3.2.0 aiohttp

Создаём файл pipeline.yaml с определением состояний и переходов:

states:  - name: PRIMARY    initial: true  - name: FALLBACK_1  - name: FALLBACK_2  - name: ERROR    final: truetransitions:  - from: PRIMARY    event: api_error    to: FALLBACK_1    action: notify  - from: PRIMARY    event: rate_limit    to: FALLBACK_1    action: wait_and_log  - from: PRIMARY    event: success    to: PRIMARY    action: reset_counters  - from: FALLBACK_1    event: api_error    to: FALLBACK_2    action: notify  - from: FALLBACK_1    event: rate_limit    to: FALLBACK_2    action: wait_and_log  - from: FALLBACK_1    event: success    to: PRIMARY    action: reset_counters  - from: FALLBACK_2    event: api_error    to: ERROR    action: alert  - from: FALLBACK_2    event: rate_limit    to: FALLBACK_2    action: backoff  - from: FALLBACK_2    event: success    to: PRIMARY    action: reset_counters

Обрати внимание: после успеха мы возвращаемся в PRIMARY. Это гарантирует, что следующий запрос снова попробует основной провайдер. Без этого он бы навсегда застрял в FALLBACK_2. Я видел продакшены, где забыли reset_counters — и после одного сбоя система полгода ходила в резервный, более дорогой API.

2Python-код: обёртка вызовов

Теперь напишем скрипт, который использует этот автомат:

import asyncioimport jsonimport aiohttpfrom llm_nano_vm import StateMachine, load_config# Загружаем конфигsm = StateMachine(load_config('pipeline.yaml'))# Определяем хендлеры вызововasync def call_openai(session, prompt):    # ... твоя логика вызова GPT-5    async with session.post('https://api.openai.com/v1/chat/completions',                           headers={'Authorization': 'Bearer ...'},                           json={'model': 'gpt-5', 'messages': [{'role': 'user', 'content': prompt}]}) as resp:        if resp.status == 429:            return None, 'rate_limit'        if resp.status != 200:            return None, 'api_error'        data = await resp.json()        return data['choices'][0]['message']['content'], 'success'# ... аналогично для Anthropic и Ollamaasync def call_anthropic(...):   ...async def call_ollama(...):   ...# Основной цикл вызова с автоматомasync def safe_llm_call(prompt):    session = aiohttp.ClientSession()    try:        while not sm.is_final():            state = sm.current_state            if state == 'PRIMARY':                result, event = await call_openai(session, prompt)            elif state == 'FALLBACK_1':                result, event = await call_anthropic(session, prompt)            elif state == 'FALLBACK_2':                result, event = await call_ollama(session, prompt)            else:                break            if event == 'success':                sm.feed_event('success')                return result            else:                sm.feed_event(event)                # логика delay между попытками                await asyncio.sleep(sm.get_delay(state, event))        # Все провайдеры упали        raise RuntimeError('All LLM providers failed')    finally:        await session.close()

Да, код не идеален — ты можешь вынести вызовы в отдельный router. Но суть ясна: автомат сам решает, кого дергать, а мы только получаем событие. Если, скажем, OpenAI вернул rate_limit, автомат переходит в FALLBACK_1 и запоминает, что нужно подождать 30 секунд перед возвратом в PRIMARY. Это и есть circuit breaker на минималках.

💡
Для более сложных сценариев (например, когда разные шаги пайплайна требуют разных провайдеров) создай несколько независимых экземпляров StateMachine — по одному на каждый шаг. Тогда сбой на этапе извлечения данных не повлияет на этап генерации.

Типичные ошибки (и как их избежать)

Ошибка №1: Игнорировать таймауты.
В конфигурации не заданы таймауты — вызов может висеть 60 секунд. В llm-nano-vm можно добавить поле timeout для каждого состояния в YAML. Если не пришёл ответ — автомат получает событие timeout и переходит на fallback.

states:  - name: PRIMARY    timeout: 10  # seconds

Ошибка №2: Не различать типы ошибок.
Все исключения летят в одно событие api_error. На практике rate_limit требует паузы, bad_request (400) — немедленного перехода, а server_error (500) — повтора с exponential backoff. Раздели их.

Ошибка №3: Забыть про локальный провайдер.
Если все облачные умерли, локальный Ollama (пусть медленный) — лучше, чем ничего. В статье Ollama vs другие мы разобрали, как поднять Llama 4 на обычном ноутбуке. Включи эту возможность в fallback-цепочку — и твой пайплайн станет практически неубиваемым.

Ошибка №4: Не логировать переходы.
Без логов ты никогда не узнаешь, что половина запросов ушла в Anthropic, потому что OpenAI отваливался на 200 мс. Добавь в action notify запись в stdout или метрики. llm-nano-vm позволяет прикреплять Python-функции к действиям.

def log_transition(from_state, to_state, event):    print(f'{from_state} -> {to_state} via {event}')    # Отправляем метрику в Prometheussm.add_action('notify', log_transition)

Когда FSM не спасает (честно)

Конечный автомат — это не серебряная пуля. Если у тебя пайплайн из 20 шагов, где каждый шаг зависит от предыдущего, и провайдер меняется на каждом шаге в зависимости от контента — FSM разрастётся до неприличных размеров. Тогда лучше посмотреть в сторону более тяжёлых решений, вроде интеграции LLM в корпоративную шину. Там заложены enterprise-паттерны для маршрутизации.

Но для типового сценария “три провайдера, два шага, fallback по кругу” — llm-nano-vm идеален. Ты получаешь предсказуемое поведение, минимальный оверхед и полный контроль.

Что дальше: отказоустойчивость и мониторинг

Построив базовый FSM, ты можешь пойти глубже. Добавить динамическую лень (LazyGate) для снижения time-to-first-token при пиковых нагрузках. Или подключить мониторинг, чтобы видеть сбои провайдеров в реальном времени. А если хочешь совсем экзотики — запустить LLM на bare-metal для максимально быстрого локального fallback.

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

FAQ

Можно ли использовать llm-nano-vm без конфигурационного файла?

Да, можно задать состояния и переходы программно через API sm.add_state() и sm.add_transition(). Но YAML удобнее — он читается и неспециалистами.

Как добавить кастомный delay для rate_limit?

В конфигурации перехода укажи action: wait_and_log, а в коде привяжи функцию, которая вычисляет задержку из заголовка Retry-After. Или используй встроенный sm.get_delay(state, event), если настроил в YAML поле delay.

Что делать, если все три провайдера упали?

Автомат переходит в состояние ERROR. Я рекомендую в обработчике этого состояния сохранить запрос в очередь (например, в Redis) и повторить через N минут. Или отправить уведомление ответственному дежурному.

Как протестировать пайплайн локально?

Собери mock-сервера с помощью aiohttp и заставь их возвращать разные коды. llm-nano-vm легко подменяет вызовы, так что тесты писать просто. Не забудь проверить полный цикл: success -> fallback -> возврат в primary.

Если хочешь увидеть полный код с тестами и конфигами — он лежит в открытом репозитории по ссылке в описании. А пока проверь свой пайплайн: не выполнит ли он три попытки в один и тот же провайдер, если тот отвечает 500? Если да — срочно внедряй FSM.

Подписаться на канал