- BrainTools - https://www.braintools.ru -
Это продолжение первой статьи про Briefka [1] — там я описывал самого бота и базовую архитектуру каскада LLM-провайдеров. За прошедшие 4 месяца бот органически вырос с 59 до 84 пользователей, и именно на этом масштабе бесплатный каскад начал срываться на платного провайдера. Расскажу, почему так вышло и как я вернул расходы к нулю — с цифрами и кодом.
Код ниже — реальные фрагменты из боевого Briefka, слегка сокращённые для читаемости: убраны логирование и сбор статистики.
Вместо одного платного провайдера — лесенка из пяти, с автоматическим фолбэком при rate limit:
Groq #1 (бесплатно, 12K TPM)
→ Groq #2 (бесплатно, второй аккаунт)
→ Mistral (бесплатно)
→ Cerebras (бесплатно, быстрый)
→ DeepSeek (платный — якорь, чтобы не было полного отказа)
Идея простая: пока хоть один бесплатный провайдер не уперся в лимит, мы не платим. DeepSeek внизу — страховка от полного отказа, а не основной рабочий вариант.
Сам каскад собирается из тех ключей, что есть в окружении, — DeepSeek всегда добавляется последним:
python
# src/llm/client.py
def build_llm_chain(deepseek_key, groq_key=None, groq_key_2=None,
mistral_key=None, cerebras_key=None):
"""Собираем каскад из доступных ключей. DeepSeek всегда последний (якорь)."""
clients = []
if groq_key: clients.append(LLMClient(groq_key, provider="groq")) # бесплатный
if groq_key_2: clients.append(LLMClient(groq_key_2, provider="groq")) # второй аккаунт
if mistral_key: clients.append(LLMClient(mistral_key, provider="mistral")) # бесплатный
if cerebras_key: clients.append(LLMClient(cerebras_key, provider="cerebras")) # бесплатный
clients.append(LLMClient(deepseek_key, provider="deepseek")) # платный якорь
return LLMChainClient(clients)
А вот сам перебор. Важная деталь, которой не было в первой статье, — circuit breaker: провайдер, словивший 429, уходит на cooldown и какое-то время просто пропускается, чтобы не долбиться в исчерпанный лимит на каждом запросе.
python
# src/llm/client.py
class LLMChainClient:
# Ошибки доступности, по которым уходим к следующему провайдеру
FALLBACK_ERRORS = ("rate_limit", "429", "quota", "capacity",
"overloaded", "timeout", "connection", "503", "502", "500")
COOLDOWN_SECONDS = 600 # после 429 провайдер «отдыхает» 10 минут
async def complete(self, prompt, **kw):
last_error = None
for i, client in enumerate(self.clients):
# circuit breaker: пропускаем «остывающих», кроме якоря в самом конце
if self._is_cooling(i) and i < len(self.clients) - 1:
continue
try:
return await client.complete(prompt, **kw)
except Exception as e:
last_error = e
if not self._is_fallback_error(e):
raise # контентная/auth-ошибка — не фолбэчим
if self._is_rate_limit_error(e):
self._set_cooldown(i) # 429 → провайдера на cooldown
if i == len(self.clients) - 1: # упал даже якорь
raise
# иначе — молча пробуем следующего
raise last_error
До оптимизации затрат был отдельный сюрприз. Первый VPS был с российским IP — и обращения к зарубежным LLM с него блокируются. После деплоя всё встало. Пришлось переехать на зарубежный VPS с чистым IP; только тогда провайдеры заработали стабильно, без проксей и обёрток. Если гоняете иностранные модели — закладывайте это сразу.
При росте до 80+ пользователей вылез неприятный эффект. Ежедневная рассылка стартовала по расписанию, и все LLM-запросы уходили практически одновременно. Бесплатные провайдеры дружно выбивали свои rate-limit’ы — и каскад массово сваливался на DeepSeek.
В цифрах за май — 915 вызовов DeepSeek. По деньгам это смешные ~$0.10, дело не в сумме: проблема в том, что «бесплатный» каскад переставал быть бесплатным, и расход рос бы линейно с числом пользователей. Я по сути сам себя DDOS-ил против собственных бесплатных лимитов.
1. Разнос пользователей во времени. Пауза ~10 секунд между пользователями. Весь цикл рассылки растягивается примерно с минуты до ~13 минут — зато бесплатные лимиты успевают восстанавливаться, и пик нагрузки размазывается.
python
# src/scheduler/jobs.py
for user in users:
if self._parse_digest_hour(user.digest_time) == current_hour:
try:
await self._send_digest_to_user(user.telegram_id, session)
except Exception as e:
logger.error(f"Failed to send digest to {user.telegram_id}: {e}")
await asyncio.sleep(10) # Stagger LLM load: 10s between users
2. Сериализация LLM-вызовов внутри дайджеста. Вместо параллельных запросов — последовательные, через семафор на 1. Снижает пиковую конкуренцию за лимиты. Бонусом — кросс-юзерный кэш: один и тот же пост, который читают несколько подписчиков, через LLM прогоняется один раз.
python
# src/scheduler/jobs.py
semaphore = asyncio.Semaphore(1) # Sequential LLM calls to avoid TPM rate limits
async def process_one(item):
db_post, content, channel_name = item
# кросс-юзерный кэш: общий для подписчиков пост не гоняем через LLM дважды
cache_key = hashlib.md5(content.encode("utf-8", errors="replace")).hexdigest()
if cache_key in self._post_cache:
return (self._post_cache[cache_key], channel_name)
async with semaphore:
result = await self.processor.analyze_post(content, channel_name)
if result:
self._post_cache[cache_key] = result
return (result, channel_name)
results = await asyncio.gather(*(process_one(p) for p in posts_to_process))
26 мая — первый день с нулём вызовов DeepSeek. Весь цикл из 81 дайджеста прошёл целиком на бесплатных провайдерах.
|
Период |
Вызовов DeepSeek |
Стоимость |
|---|---|---|
|
Февраль–март |
0 |
$0 |
|
Апрель |
88 |
~$0.02 |
|
Май (до фикса) |
915 |
~$0.10 |
|
Май, 26+ (после фикса) |
0 |
$0 |
Главный инсайт контринтуитивный: на малом масштабе параллелизм бесплатен, а на границе бесплатных тарифов он становится врагом. Когда упираешься в rate-limit бесплатных провайдеров, спасает не «сделать быстрее», а наоборот — размазать во времени и сериализовать. Ты разменваешь латентность на стоимость.
И этот размен почти всегда выгоден, если задача не интерактивная. Для ежедневного дайджеста никто не заметит разницы между «готово за минуту» и «готово за 13 минут» — а расходы падают до нуля. Будь это чат в реальном времени, размен был бы невозможен, и пришлось бы платить за пропускную способность.
Текущие цифры: 84 пользователя, 1 806 отправленных дайджестов, 237 уникальных каналов, аптайм без перезапуска — 11 дней.
Бот — @Briefka_bot [2]. Пишу про такие штуки в Tezarium [3].
Автор: Tezarium
Источник [4]
Сайт-источник BrainTools: https://www.braintools.ru
Путь до страницы источника: https://www.braintools.ru/article/31385
URLs in this post:
[1] первой статьи про Briefka: https://habr.com/ru/articles/996940/
[2] @Briefka_bot: https://t.me/Briefka_bot
[3] Tezarium: https://tezarium.com
[4] Источник: https://habr.com/ru/articles/1044546/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1044546
Нажмите здесь для печати.