Перейти к содержимому

Всплеск стоимости embeddings

Стоимость embeddings прячется в общем счёте за LLM, а ведёт себя иначе: чат-токены зависят от трафика пользователей, а embedding-токены выстреливают пачками при переиндексации. Один случайный полный reindex в цикле, забытый for doc in docs: reindex() или ретраи ингеста — и за ночь вы векторизуете всю базу заново несколько раз. Считаем embedding-расход отдельно.

Оборачиваем каждый вызов embeddings и копим токены за скользящее окно (например, за час). Состояние — в Redis.

import os, time, requests, redis
R = redis.from_url(os.environ["REDIS_URL"])
# $ за 1M токенов — под вашу модель embeddings
PRICE_PER_1M = 0.02
HOURLY_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.

Раз в сутки 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 кэша, если он включён.