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

Деградация latency LLM

LLM-провайдеры редко падают полностью. Гораздо чаще — 200 OK, но time-to-first-token вместо привычных 300 мс становится 5 секунд, и ваш агент начинает таймаутиться и крутить retry. Стандартные uptime-мониторы это не замечают: 200 ок, статус green.

Используем активный монитор с собственной cloud-функцией: дёргаем дешёвый endpoint и алёртим при превышении порога latency.

Положите этот код в Yandex Cloud Function с timer-trigger * * * * ? * (раз в минуту):

import os, time, statistics, requests
import 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 — гарантия независимо от холодных стартов; см. архитектуру.

Для streaming-агентов важнее не суммарное время, а время до первого токена. Если работаете с SSE — измеряйте именно его:

t0 = time.time()
ttft = None
with 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-эндпоинты ломаются отдельно от чата (часто это другой деплоймент). Если у вас RAG — мониторьте /v1/embeddings отдельным замером, иначе индексация и поиск будут «висеть» молча.