Когда пайплайн превращается в рулетку
Ты собрал крутую систему. Промпты летят в 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 на минималках.
Типичные ошибки (и как их избежать)
Ошибка №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.