RAG-ретривал возвращает пустоту
Ретривал вернул ноль чанков выше порога похожести — и модель отвечает «в
предоставленном контексте нет информации» либо, что хуже, галлюцинирует.
Единичная пустая выдача нормальна (запрос вне базы знаний), но всплеск
доли пустых почти всегда означает поломку: переиндексировали не в ту
коллекцию, сменились embeddings, или порог score_threshold задрали слишком
высоко. HTTP-статусы при этом все зелёные.
1. Считаем долю пустых выдач в окне
Заголовок раздела «1. Считаем долю пустых выдач в окне»Оборачиваем реальный ретривал и метим каждый запрос как пустой или нет.
import os, requests, redis
R = redis.from_url(os.environ["REDIS_URL"])KEY = "rag-retrieval" # "hit" / "miss"WIN = 300MIN = 40THRESHOLD = 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-чеком);
- пара примеров запросов, оставшихся без контекста.
Связанные рецепты
Заголовок раздела «Связанные рецепты»- Vector DB / RAG-инфраструктура — если легла сама БД.
- Смена версии embeddings — частая причина «всё стало не находиться».
- Дрейф чанкинга / конфига — несовместимость build- и query-конфигов.