Как заменить внешний AI-провайдер на собственный стек: кейс за полдня

В 11:00 утра 20 июня у меня одновременно упали 6 сервисов. Не по технической причине — просто кончился баланс на Kimi/Moonshot, китайском AI-движке, на котором висели переводчик для WhatsApp, два разборщика объявлений и модератор чатов. Все они начали сыпать HTTP 402 и таймаутами. К 14:30 все шесть работали на новом стеке: claude как основной провайдер, codex как резерв. Ниже разбираю, как именно — по шагам, без пропусков.

Сам Kimi/Moonshot — качественный китайский LLM-провайдер. Я использовал его около 8 месяцев: хорошо работал с азиатскими языками, дешёвый по токенам, OpenAI-совместимый API. Проблема не в качестве продукта, а в том, что я положил 6 продакшн-сервисов на один внешний endpoint и не подготовил резервный маршрут. Это моя ошибка, и её последствия я разгребал 3,5 часа.

Если у вас есть хотя бы один сервис, который ходит к внешнему LLM-провайдеру без fallback — эта статья про вас. Не потому что с вашим провайдером что-то пойдёт не так, а потому что когда это произойдёт, план лучше иметь заранее.

Почему внешний AI-провайдер обрывается внезапно — и что это значит для ваших сервисов

Есть три типовых сценария обрыва внешнего AI-провайдера:

  • Кончился баланс — самый обидный, потому что легко предотвратить. У меня именно этот.
  • Провайдер поменял политику — rate limits, geoblocking, закрытие для вашего региона. Часто без предупреждения за 24 часа.
  • Провайдер умер — закрылся, был поглощён конкурентом, сменил бизнес-модель. В AI-пространстве это происходит чаще, чем в enterprise-софте: за 2024-2025 годы прекратили работу или существенно изменили условия несколько десятков мелких LLM-провайдеров.

Что значит «упали 6 сервисов» на практике: переводчик WhatsApp не отвечает — входящие лиды получают тишину вместо первого ответа. Разборщик объявлений не работает — база обновляется с задержкой 8+ часов. Модератор чатов молчит — в Telegram-чатах начинается спам про онлайн-казино. Это не «технический сбой в логах», это прямые операционные потери в реальном времени.

Отдельный момент: все эти сервисы работали у меня без алертов на HTTP 402. Я узнал об аварии не из мониторинга, а потому что сам попробовал воспользоваться переводчиком и получил тишину. В здоровой архитектуре у каждого AI-зависимого сервиса должен быть healthcheck с алертом на 4xx от провайдера — отдельно от общего uptime-мониторинга, потому что сам сервис может быть «живым», но ходить к мёртвому внешнему API. После этого инцидента я такие проверки добавил на все шесть сервисов.

Главный урок: зависимость от одного внешнего AI-провайдера — это единая точка отказа. Вопрос не «случится ли», а «когда». И когда это происходит, нужен план действий на 3-4 часа, а не на 3-4 дня.

Шаг 0. Аудит зависимостей: карта мест, где живёт внешний AI

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

grep -r "moonshot" /opt --include="*.py" --include="*.js" --include="*.env" -l

Вышло 11 файлов. Шесть из них — активные продакшн-сервисы, остальные — старые скрипты, тесты, файлы с документацией. По каждому активному файлу смотрю: что именно использует этот endpoint? Это один из трёх паттернов:

  • Генерация текста — перевод, суммаризация, классификация входящих сообщений
  • Structured output — JSON из неструктурированного ввода (разбор объявлений, квалификация лидов)
  • Embeddings — поиск по смыслу, семантическая близость между документами

Это разграничение важно, потому что не все паттерны одинаково легко мигрировать. Claude закрывает первые два паттерна хорошо — он понимает инструкции на русском, работает со structured output через tool use или инструкцию в промпте, справляется с классификацией. Для embeddings нужен отдельный провайдер — text-embedding-3-small от OpenAI или локальный nomic/e5 через Ollama. У меня embeddings через Kimi не было. Все 6 сервисов делали генерацию или классификацию. Это существенно упростило задачу.

Итоговый список 6 сервисов для миграции:

  • wa_translator — перевод входящих сообщений WhatsApp с малайского и индонезийского
  • ad_parser_bali — разборщик объявлений об аренде из Telegram-каналов по Бали
  • ad_parser_ru — то же самое для российских объявлений с Авито и аналогов
  • chat_moderator — классификация сообщений в Telegram-чатах (спам / не спам / под вопросом)
  • lead_classifier — квалификация входящих лидов по уровню интереса и готовности к сделке
  • summary_bot — суммаризация длинных диалогов для записи в CRM

На аудит ушло 20 минут. Именно столько нужно, чтобы знать масштаб задачи перед тем, как начинать что-то чинить. Это важный момент: в аварийной ситуации первый инстинкт — сразу броситься чинить первый сломавшийся сервис. Но без общей карты вы будете чинить одно, пока другое продолжает падать, и не будете видеть общего прогресса. Чем больше у вас AI-зависимостей — тем дороже работать без карты, и тем ценнее эти 20 минут вложенных заранее.

Шаг 1. Docker-мост: единая точка маршрутизации вместо правки 6 кодовых баз

Здесь стандартный совет «просто поменяй API-ключ в .env» не работает по двум причинам.

Первая: часть сервисов живёт в Docker-контейнерах. Переменные окружения в контейнере задаются при старте — в docker-compose.yml или в Dockerfile. Поменять .env на хосте без рестарта контейнера бесполезно: он читает своё окружение только при запуске. Рестарт каждого контейнера — 2-5 минут простоя на сервис плюс риск побочных эффектов при перезапуске взаимосвязанных компонентов (особенно если один сервис зависит от другого через внутреннюю сеть).

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

Решение: bridge-прокси на хосте. Поднять лёгкий HTTP-сервер, который слушает на внутреннем Docker-шлюзе (172.17.0.1 — стандартный bridge gateway для Docker-сетей), принимает запросы в формате старого Kimi API и перенаправляет их в claude с нужной трансформацией. Меняешь маршрут в одном месте — все контейнеры подхватывают без рестарта.

Разница между Kimi API (OpenAI-совместимый) и Claude API в трёх местах:

  • Имя модели: moonshot-v1-8kclaude-sonnet-4-6
  • Заголовок авторизации: тот же Bearer-формат, другой ключ
  • System prompt: у Kimi идёт как сообщение с role: system в массиве messages, у claude лучше передавать в отдельное поле system для корректной обработки

Мост написан на FastAPI за 40 минут:

from fastapi import FastAPI, Request
import anthropic, json

app = FastAPI()
client = anthropic.Anthropic(api_key="sk-ant-...")

@app.post("/v1/chat/completions")
async def proxy(request: Request):
    body = await request.json()
    messages = body.get("messages", [])

    system = None
    filtered = []
    for m in messages:
        if m["role"] == "system":
            system = m["content"]
        else:
            filtered.append(m)

    resp = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=body.get("max_tokens", 2048),
        system=system,
        messages=filtered
    )

    return {
        "choices": [{
            "message": {
                "role": "assistant",
                "content": resp.content[0].text
            }
        }]
    }

Запускаем на хосте, привязываем к Docker-шлюзу:

uvicorn kimi_bridge:app --host 172.17.0.1 --port 8000 &

В каждом контейнере меняем одну переменную окружения:

KIMI_API_BASE=http://172.17.0.1:8000

Рестарт не нужен, если сервис читает эту переменную через os.environ.get() при каждом вызове функции — а не на уровне инициализации модуля. Большинство моих сервисов так устроены. Где не так — достаточно обернуть инициализацию клиента в функцию с ленивой загрузкой, это 3 строки кода.

Шаг 2. Structured output без json_object: адаптация промпта

Отдельная проблема — сервисы, которые просят LLM вернуть JSON-объект. В Kimi (как и в OpenAI API) это делается через параметр response_format: {type: "json_object"}. Claude этот параметр в таком виде не принимает — у него есть tool_use для структурированного вывода или явная инструкция в промпте.

Я выбрал промпт-решение на уровне моста, чтобы не менять код клиентских сервисов. Логика простая: если входящий запрос содержит response_format с типом json_object — добавляю к system prompt инструкцию «Отвечай ТОЛЬКО валидным JSON без пояснений, без markdown-блоков, без лишнего текста». Работает в ~95% случаев — claude хорошо следует этому ограничению.

Для оставшихся 5% (когда claude всё равно добавляет поясняющий текст до или после JSON) добавил парсер в мост:

def extract_json(text: str) -> dict:
    text = text.replace("```json", "").replace("```", "")
    start = text.find("{")
    end = text.rfind("}") + 1
    if start >= 0 and end > start:
        return json.loads(text[start:end])
    return json.loads(text.strip())

Этот парсер добавлен в обработчик ответа в мосту — прозрачно для всех клиентских сервисов. Ни один из 6 сервисов не знает, что ответ прошёл через дополнительную обработку. Это важный принцип при работе с прокси: прозрачность для клиентов, трансформация внутри.

Шаг 3. Перенос каждого из 6 сервисов: что конкретно менялось

wa_translator — простейший случай. Один endpoint, одна задача — перевод с малайского и индонезийского на русский. Поменял base URL на мост, проверил ответ на тестовом сообщении. Работает через 3 минуты. Единственный нюанс: у Kimi было больше обучающих данных на азиатских языках из-за специфики китайской исследовательской команды. Малайский разговорный в WhatsApp у claude чуть хуже — добавил в system prompt явное указание: «переводи с малайского и индонезийского разговорного языка, не с литературного, сохраняй тон, сленг и эмодзи из оригинала». Качество вернулось к приемлемому уровню за счёт более детального промпта.

ad_parser_bali и ad_parser_ru — оба разборщика используют structured output: просят вернуть JSON с полями тип объявления, площадь, цена, контакт, локация, срок. Проблема решена через промпт-инструкцию и парсер из предыдущего шага. Тестировал на 20 реальных объявлениях — точность сопоставимая с Kimi. На русскоязычных объявлениях с произвольной структурой claude даже чуть лучше извлекает нестандартные поля. На оба сервиса суммарно 25 минут включая тестирование на реальных данных.

chat_moderator — здесь стояла проблема задержки. Модератор должен классифицировать каждое сообщение менее чем за 500 мс, иначе накапливается очередь и Telegram-чаты начинают лагать. С Kimi задержка была 200-250 мс из-за близости серверов (Азия → Азия). Claude через Anthropic API из Европы даёт 400-600 мс на claude-sonnet — это за порогом.

Решение: переключить именно этот сервис на claude-haiku-4-5. Haiku примерно вдвое быстрее при минимальных задачах — single-label classification с тремя вариантами ответа. Промпт тоже упростил до минимума, убрав все лишние объяснения:

System: Classify this Telegram message.
Reply with exactly one word: spam, not_spam, or uncertain.

User: [текст сообщения]

С haiku задержка — 180-220 мс. Лучше исходного. Попутно стоимость на этом сервисе снизилась примерно втрое: haiku дешевле sonnet по входным токенам, а промпт стал короче.

lead_classifier — самая сложная миграция. В этом сервисе через Kimi был настроен fine-tuned checkpoint под профиль входящих лидов: характеристики типичного покупателя виллы на Бали, частые возражения, сигналы серьёзного намерения против прощупывания цен. Claude fine-tuning через Anthropic API пока недоступен в том же формате. Переход на базовую модель без адаптации давал деградацию: примерно 15% лидов классифицировались некорректно — «горячие» попадали в «тёплые».

Решение: few-shot промпт с реальными примерами. Взял 24 реальных диалога с пометками «горячий» (упомянул бюджет и срок), «тёплый» (интересуется, но не готов к цифрам), «спам» (автоматические рассылки, боты). Добавил их в system prompt в формате диалог → категория → причина. С 24 примерами качество вышло сопоставимым с fine-tuned моделью. Но промпт стал длиннее: ~2200 токенов против ~400 при fine-tuning. При потоке 35-50 лидов в сутки это даёт примерно 4-кратный рост стоимости по этому конкретному сервису. Для lead_classifier приемлемо. Для высокочастотных сервисов с тысячами запросов в сутки надо считать заранее — few-shot может оказаться дороже, чем сохранить fine-tuned чекпоинт у прежнего провайдера.

summary_bot — суммаризация длинных диалогов для CRM. Здесь claude объективно сильнее Kimi: лучше удерживает контекст на длинных диалогах (200k токенов против 32k у Kimi v1), точнее выделяет ключевые договорённости и следующие шаги, реже добавляет детали которых не было в исходном тексте. Поменял endpoint и имя модели, проверил что контекстного окна хватает (наши диалоги — максимум 12-15k токенов, far под лимитом). Работает без проблем с первого запроса, и качество суммаризаций стало лучше.

Fallback-архитектура: claude primary + codex как резерв от следующей аварии

Менять один провайдер на другой — это лечение симптома. Реальное решение — fallback-архитектура, где у каждого запроса есть резервный маршрут и система никогда не зависит от одного источника.

После миграции я обновил мост: теперь у него два бэкенда. Claude — primary, codex (OpenAI o3) — fallback. Логика переключения срабатывает при любой ошибке от primary: 429 (rate limit), 503 (сервис недоступен), таймаут больше 10 секунд:

async def call_with_fallback(messages, system=None, max_tokens=2048):
    try:
        resp = anthropic_client.messages.create(
            model="claude-sonnet-4-6",
            max_tokens=max_tokens,
            system=system,
            messages=messages
        )
        return resp.content[0].text, "claude"
    except Exception as primary_error:
        try:
            msgs = []
            if system:
                msgs.append({"role": "system", "content": system})
            msgs.extend(messages)
            resp = openai_client.chat.completions.create(
                model="o3", messages=msgs, max_tokens=max_tokens
            )
            return resp.choices[0].message.content, "codex"
        except Exception as fallback_error:
            raise RuntimeError(
                f"Both providers failed. Primary: {primary_error} / Fallback: {fallback_error}"
            )

Второй аргумент возвращаемого кортежа — имя провайдера, который ответил. Мост логирует это в каждом ответе. Через неделю работы у меня будет реальная метрика надёжности по каждому провайдеру — фактические данные, а не маркетинговые SLA в документации.

Overhead от fallback: около 200 мс дополнительно — и только когда primary недоступен. В нормальном режиме клиент не замечает прокси-прослойки.

Третий уровень у меня — Ollama с локальной моделью на сервере. Это не real-time fallback, а пул для некритичных задач: ночные батчи, предобработка данных, задачи которые могут подождать несколько минут. Если оба облачных провайдера недоступны одновременно — ночные задачи уходят в Ollama, критичные встают в очередь с retry через 5 минут. Три независимых источника — нормальная архитектура для solo founder с автоматизированной операционкой без команды поддержки.

Юрий Солар, Solar: «Всё, что я сделал — это не rocket science. Grep по кодовой базе, 40 минут на FastAPI-прокси и 3 часа точечных правок. Сложность была не техническая, а организационная — остановиться и сделать карту зависимостей вместо того, чтобы чинить сервисы по одному в панике.»

Результаты через 3,5 часа: что изменилось, что деградировало, что улучшилось

К 14:30 все 6 сервисов ответили OK на healthcheck. Детали по каждому:

  • wa_translator — работает. Малайский разговорный немного хуже без дообучения на азиатских данных, промпт-адаптация компенсирует большую часть разницы.
  • ad_parser_bali — работает без деградации качества. На части объявлений извлечение полей стало точнее.
  • ad_parser_ru — работает. На русскоязычных объявлениях заметное улучшение структурирования нестандартных форматов.
  • chat_moderator — работает. Задержка улучшилась: 200 мс вместо 250 мс (claude-haiku-4-5 быстрее Kimi). Стоимость снизилась.
  • lead_classifier — работает. Стоимость выросла в ~4 раза на этом сервисе из-за длинного few-shot промпта. Качество классификации сопоставимое с fine-tuned вариантом.
  • summary_bot — работает и объективно лучше. Claude сильнее в суммаризации длинных диалогов с удержанием контекста.

Суммарное изменение стоимости AI по всей системе: +28%. Это плата за независимость от одного провайдера и за fallback-архитектуру с двумя резервными маршрутами. С учётом того, что неожиданный простой одного дня в продакшне обошёлся бы дороже в виде потерянных лидов и пропущенного спама в чатах — разница приемлемая.

Три вещи, которые стоило сделать до аварии

1. Карта зависимостей. Grep по кодовой базе на все внешние AI-endpoints. Одна таблица: сервис, провайдер, тип использования (генерация / structured output / embeddings). 20 минут работы. Делать раз в квартал или каждый раз когда добавляете новую AI-интеграцию. Без этой карты в момент аварии вы не знаете масштаб и рискуете чинить одно, пока падает другое.

2. Bridge-прокси. Не подключать сервисы к провайдерам напрямую — всё через единую прослойку. Меняете провайдера в одном месте вместо того, чтобы обходить 6-10 кодовых баз по одной. 40 минут на настройку FastAPI-моста, которые экономят часы при следующей аварии. Дополнительный бонус: в прокси можно добавить логирование, rate limiting, fallback — всё в одном месте.

3. Fallback provider. Два независимых LLM-провайдера с автопереключением. Страховка не от качества ответов, а от доступности. Стоит примерно 10-15% к AI-расходам за счёт чуть дороже fallback-провайдера. Убирает единую точку отказа полностью. При потоке аварий типа «кончился баланс», «rate limit», «временная недоступность API» — это то, что превращает инцидент из «сервисы упали на 3 часа» в «клиент ничего не заметил».

Всё это не требует команды и не требует большого бюджета. Один человек, 3-4 часа, стандартный Python-стек. После аварии я это сделал. До аварии было бы дешевле — и в деньгах, и в нервах.

Рабочий код моста с fallback-логикой, промпт-адаптер для structured output без response_format, конфиг docker-compose для bridge-сети и чеклист аудита зависимостей — в клубе «Solar — внутрянка». Там же разборы других инцидентов стека и то, как устроена автоматизация у меня сейчас в деталях. От 2 500 ₽/мес, бери и адаптируй: 4bos.ru/inside/

— Solar OS.

Связанные материалы: Как устроена автоматизация у меня: обзор стека 2026

Частые вопросы

Как быстро реально мигрировать с одного AI-провайдера на другой?
При наличии bridge-прокси — 3-4 часа на 6-8 сервисов. Без прокси каждый сервис правится отдельно: 20-40 минут на каждый плюс тестирование. Самое долгое — сервисы с fine-tuned моделями: у них нет готового аналога, переход на few-shot занимает 1-2 часа на адаптацию промпта и калибровку качества. В кейсе выше: 3,5 часа на 6 сервисов, из которых один был fine-tuned.
Зачем нужен Docker-мост при смене AI-провайдера, если можно просто поменять переменные?
Docker-контейнеры читают переменные окружения при старте, не при каждом запросе. Поменять .env на хосте без рестарта контейнера бесполезно. Bridge-прокси на 172.17.0.1 решает это без рестарта: один Python-файл принимает запросы в формате старого API и перенаправляет в новый. Меняешь один конфиг вместо 6. Дополнительный бонус — в мост можно добавить fallback-логику, rate limiting и логирование провайдера.
Чем claude отличается от Kimi/Moonshot для бизнес-автоматизации?
Claude сильнее в суммаризации длинного текста, структурированных рассуждениях и задачах с длинным контекстом (200k токенов против 32k у Kimi v1). Kimi исторически лучше работал с азиатскими языками (малайский, индонезийский, китайский). На русском и английском claude явно лучше. Для классификации и перевода разница небольшая — компенсируется адаптацией системного промпта.
Как настроить автоматический fallback между двумя AI-провайдерами?
Нужен bridge-прокси с логикой: сначала пробуем primary, при ошибке (429, 503, таймаут больше 10 секунд) — переходим на fallback. Важно логировать какой провайдер ответил — это даёт реальную метрику надёжности. На стеке claude+codex overhead fallback составляет около 200 мс, только когда primary недоступен. В нормальном режиме клиент не замечает прослойки вообще.

Читайте также

Подписаться на блог в Telegram

Читайте свежие кейсы об AI-автоматизации, системной архитектуре и масштабировании бизнеса.

Подписаться