Деградация latency LLM
LLM-провайдеры редко падают полностью. Гораздо чаще — 200 OK, но
time-to-first-token вместо привычных 300 мс становится 5 секунд, и ваш
агент начинает таймаутиться и крутить retry. Стандартные uptime-мониторы
это не замечают: 200 ок, статус green.
Используем активный монитор с собственной cloud-функцией: дёргаем дешёвый endpoint и алёртим при превышении порога latency.
Скелет: scheduled-функция на YC
Заголовок раздела «Скелет: scheduled-функция на YC»Положите этот код в Yandex Cloud Function с timer-trigger
* * * * ? * (раз в минуту):
import os, time, statistics, requestsimport anthropic, openai
NOTIFLY_URL = os.environ["NOTIFLY_URL"]NOTIFLY_TOKEN = os.environ["NOTIFLY_TOKEN"]THRESHOLD_MS = 3000 # порог латентностиWINDOW = 5 # сколько подряд медленных запросов = алёрт
def measure_anthropic(): c = anthropic.Anthropic() t0 = time.time() c.messages.create(model="claude-haiku-4-5", max_tokens=1, messages=[{"role": "user", "content": "ping"}]) return (time.time() - t0) * 1000
def measure_openai(): c = openai.OpenAI() t0 = time.time() c.chat.completions.create(model="gpt-4o-mini", max_tokens=1, messages=[{"role": "user", "content": "ping"}]) return (time.time() - t0) * 1000
PROVIDERS = {"anthropic": measure_anthropic, "openai": measure_openai}
# Простое окно в /tmp — подойдёт, потому что инстанс YC# обычно живёт несколько минут между холодными стартами.def state_path(name): return f"/tmp/latency-{name}.txt"
def push_window(name, value): p = state_path(name) arr = [] if os.path.exists(p): arr = [float(x) for x in open(p).read().split() if x] arr.append(value) arr = arr[-WINDOW:] open(p, "w").write(" ".join(map(str, arr))) return arr
def notify(title, msg, prio): requests.post(f"{NOTIFLY_URL}/message", params={"token": NOTIFLY_TOKEN}, json={"title": title, "message": msg, "priority": prio}, timeout=5)
def handler(event, context): for name, fn in PROVIDERS.items(): try: ms = fn() except Exception as e: notify(f"❌ {name} error", str(e), 9) continue arr = push_window(name, ms) if len(arr) >= WINDOW and statistics.median(arr) > THRESHOLD_MS: notify( f"⏱️ {name} latency деградация", f"Медиана за {WINDOW} запросов: {int(statistics.median(arr))} мс " f"(порог {THRESHOLD_MS}). Последний: {int(ms)} мс.", priority=7, ) return {"statusCode": 200}Вместо /tmp можно сложить окно в YDB — гарантия независимо от холодных
стартов; см. архитектуру.
Time-to-first-token (TTFT) важнее, чем total
Заголовок раздела «Time-to-first-token (TTFT) важнее, чем total»Для streaming-агентов важнее не суммарное время, а время до первого токена. Если работаете с SSE — измеряйте именно его:
t0 = time.time()ttft = Nonewith client.messages.stream(...) as stream: for ev in stream.text_stream: if ttft is None: ttft = (time.time() - t0) * 1000 breakАлёртить при ttft > N мс — гораздо чувствительнее, чем при «полном» latency,
потому что выход полного ответа доминирует объёмом.
Не забудьте про embeddings
Заголовок раздела «Не забудьте про embeddings»Embeddings-эндпоинты ломаются отдельно от чата (часто это другой деплоймент).
Если у вас RAG — мониторьте /v1/embeddings отдельным замером, иначе
индексация и поиск будут «висеть» молча.
Связанные рецепты
Заголовок раздела «Связанные рецепты»- Доступность LLM-провайдеров — простой uptime + 5xx;
- Vector DB / RAG — другая половина latency-цепочки;
- Своя cloud-функция integrity-проверки — общий скелет.