Всплеск стоимости embeddings
Стоимость embeddings прячется в общем счёте за LLM, а ведёт себя иначе:
чат-токены зависят от трафика пользователей, а embedding-токены выстреливают
пачками при переиндексации. Один случайный полный reindex в цикле, забытый
for doc in docs: reindex() или ретраи ингеста — и за ночь вы векторизуете
всю базу заново несколько раз. Считаем embedding-расход отдельно.
1. Отдельный счётчик токенов на embeddings
Заголовок раздела «1. Отдельный счётчик токенов на embeddings»Оборачиваем каждый вызов embeddings и копим токены за скользящее окно (например, за час). Состояние — в Redis.
import os, time, requests, redis
R = redis.from_url(os.environ["REDIS_URL"])
# $ за 1M токенов — под вашу модель embeddingsPRICE_PER_1M = 0.02HOURLY_BUDGET = 1.50 # тревога, если за час больше этого
def track_embeddings(resp, model="text-embedding-3-small"): tokens = resp.usage.total_tokens bucket = f"emb-tokens:{int(time.time() // 3600)}" # ведро на текущий час total = R.incrby(bucket, tokens) R.expire(bucket, 7200)
cost = total / 1_000_000 * PRICE_PER_1M if cost > HOURLY_BUDGET and R.set("alert:emb-cost", "1", nx=True, ex=1800): notify("💸 Всплеск стоимости embeddings", f"За текущий час: {total:,} токенов ≈ ${cost:.2f}\n" f"Бюджет: ${HOURLY_BUDGET:.2f}/час, модель: {model}.\n\n" "Проверьте: не крутится ли лишняя переиндексация или цикл ретраев.", priority=8)
def notify(title, message, priority): requests.post(f"{os.environ['NOTIFLY_URL']}/message", params={"token": os.environ["NOTIFLY_TOKEN"]}, json={"title": title, "message": message, "priority": priority}, timeout=5)Ключевая мысль — не смешивать этот счётчик с чат-токенами: иначе всплеск векторизации утонет в шуме пользовательского трафика.
2. Защита от повторной векторизации одного и того же
Заголовок раздела «2. Защита от повторной векторизации одного и того же»Самый частый источник лишних трат — повторный embedding уже проиндексированных чанков. Кэшируем по хешу содержимого:
import hashlib
def embed_cached(text: str): h = hashlib.sha256(text.encode()).hexdigest() cached = R.get(f"emb-cache:{h}") if cached: return decode_vec(cached) vec = embed(text) # платный вызов R.set(f"emb-cache:{h}", encode_vec(vec), ex=30 * 86400) return vecЕсли после включения кэша расход не падает — значит контент реально меняется каждый прогон (обычно баг чанкинга), и это уже само по себе повод для push.
3. Дневной дайджест по расписанию
Заголовок раздела «3. Дневной дайджест по расписанию»Раз в сутки cloud-функция с timer-trigger шлёт итог за день — чтобы видеть тренд, а не только пожары:
def handler(event, context): hours = [int(R.get(f"emb-tokens:{int(time.time()//3600) - i}") or 0) for i in range(24)] total = sum(hours) notify("📊 Embeddings за сутки", f"Токенов: {total:,} ≈ ${total/1_000_000*PRICE_PER_1M:.2f}\n" f"Пик за час: {max(hours):,}", priority=3) return {"statusCode": 200}Что положить в текст алёрта
Заголовок раздела «Что положить в текст алёрта»- токены и деньги за окно, отдельно от чата;
- модель embeddings и цена за 1M;
- подозреваемый источник: reindex, ингест, цикл ретраев;
- hit-rate кэша, если он включён.
Связанные рецепты
Заголовок раздела «Связанные рецепты»- Расходы на LLM API — общий бюджет на токенах.
- Перестройка индекса завершена — плановый источник всплеска.
- Сбои ингеста документов — ретраи, которые жгут токены.