Как заменить внешний 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-8k→claude-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