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

RAG-ретривал возвращает пустоту

Ретривал вернул ноль чанков выше порога похожести — и модель отвечает «в предоставленном контексте нет информации» либо, что хуже, галлюцинирует. Единичная пустая выдача нормальна (запрос вне базы знаний), но всплеск доли пустых почти всегда означает поломку: переиндексировали не в ту коллекцию, сменились embeddings, или порог score_threshold задрали слишком высоко. HTTP-статусы при этом все зелёные.

Оборачиваем реальный ретривал и метим каждый запрос как пустой или нет.

import os, requests, redis
R = redis.from_url(os.environ["REDIS_URL"])
KEY = "rag-retrieval" # "hit" / "miss"
WIN = 300
MIN = 40
THRESHOLD = 0.75 # порог похожести
def retrieve(query_vec, top_k=8):
hits = vector_search(query_vec, top_k=top_k) # ваш клиент
good = [h for h in hits if h.score >= THRESHOLD]
R.lpush(KEY, b"hit" if good else b"miss")
R.ltrim(KEY, 0, WIN - 1)
_maybe_alert()
return good
def _maybe_alert():
win = R.lrange(KEY, 0, WIN - 1)
if len(win) < MIN:
return
miss = sum(1 for x in win if x == b"miss")
rate = miss / len(win)
# обычный фон пустых выдач — 5–15%; тревога при заметном превышении
if rate > 0.35 and R.set("alert:retrieval-empty", "1", nx=True, ex=900):
notify("🫥 RAG: пустой ретривал",
f"Запросов без контекста: {miss}/{len(win)} ({int(rate*100)}%).\n"
f"Порог похожести: {THRESHOLD}.\n\n"
"Проверьте: та ли коллекция, совпадает ли модель embeddings, "
"не задран ли cutoff.",
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. Синтетическая канарейка на известных запросах»

Не ждём, пока просядет прод. Раз в 10 минут cloud-функция прогоняет запросы, для которых мы точно знаем, что контекст в базе есть:

CANARIES = [
"как сбросить пароль",
"какие способы оплаты вы поддерживаете",
"политика возврата средств",
]
def handler(event, context):
empty = []
for q in CANARIES:
vec = embed(q)
good = [h for h in vector_search(vec, top_k=8) if h.score >= THRESHOLD]
if not good:
empty.append(q)
if empty:
notify("🫥 Канарейка ретривала пуста",
"Нет контекста для заведомо покрытых запросов:\n"
+ "\n".join(f"• {q}" for q in empty),
priority=9)
return {"statusCode": 200}

Если базовые запросы перестали находить контекст — почти наверняка индекс пуст или несовместим, и это надо чинить немедленно.

  • доля пустых выдач и размер окна;
  • текущий score_threshold;
  • имя коллекции и модель embeddings (сверьте с drift-чеком);
  • пара примеров запросов, оставшихся без контекста.