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

Пустые и обрезанные ответы модели

Модель вернула пустую строку или ответ обрезался на полуслове (finish_reason=length) — по отдельности это норма, но всплеск почти всегда означает проблему: кто-то выставил max_tokens=16, провайдер деградировал, или новая версия модели «замолчала» на вашем промпте. Пользователь при этом видит пустой экран, а в метриках ошибок — тишина, потому что HTTP-статус 200.

1. Считаем пустые и обрезанные в скользящем окне

Заголовок раздела «1. Считаем пустые и обрезанные в скользящем окне»

Оборачиваем каждый вызов модели и ведём счётчик по последним N ответам. Состояние держим в Redis, чтобы пережить рестарты и охватить все инстансы.

import os, json, requests, redis
R = redis.from_url(os.environ["REDIS_URL"])
KEY = "llm-completions" # список из "ok" / "empty" / "trunc"
WIN = 200 # размер окна
MIN = 50 # не алёртим, пока мало данных
def observe(resp):
"""resp — объект ответа OpenAI-совместимого API."""
choice = resp.choices[0]
content = (choice.message.content or "").strip()
reason = choice.finish_reason
if reason == "length":
tag = "trunc"
elif not content:
tag = "empty"
else:
tag = "ok"
R.lpush(KEY, tag)
R.ltrim(KEY, 0, WIN - 1)
window = R.lrange(KEY, 0, WIN - 1)
if len(window) < MIN:
return
empty = sum(1 for x in window if x == b"empty")
trunc = sum(1 for x in window if x == b"trunc")
n = len(window)
if empty / n > 0.10:
notify("🕳️ Пустые ответы модели",
f"Пустой content: {empty}/{n} ({int(empty/n*100)}%).\n"
"Проверьте промпт, stop-последовательности и статус провайдера.",
priority=8)
elif trunc / n > 0.20:
notify("✂️ Ответы обрезаются",
f"finish_reason=length: {trunc}/{n} ({int(trunc/n*100)}%).\n"
"Скорее всего, слишком маленький max_tokens.",
priority=7)
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)

Всплеск длится минутами — не хочется получать push на каждый вызов. Ставим флаг с TTL, чтобы алёртить не чаще раза в 10 минут:

def notify_once(title, message, priority, cooldown=600):
flag = "alert:" + title
if R.set(flag, "1", nx=True, ex=cooldown):
notify(title, message, priority)

Не ждём реального трафика — раз в минуту cloud-функция с timer-trigger дёргает модель фиксированным промптом, который обязан дать непустой ответ:

def handler(event, context):
r = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": "Ответь одним словом: тест"}],
max_tokens=20,
)
c = (r.choices[0].message.content or "").strip()
if not c or r.choices[0].finish_reason == "length":
notify("🕳️ Канарейка: пустой ответ",
f"finish_reason={r.choices[0].finish_reason}, len={len(c)}",
priority=8)
return {"statusCode": 200}
  • доля пустых / обрезанных в окне и его размер;
  • значение max_tokens, с которым идут запросы;
  • модель и провайдер;
  • последний ненулевой ответ для контраста.