В RAG ноль результатов: отличаем пустой индекс от фильтра и порога
Ассистент сообщает, что в документах нет ответа. Команда загружала нужный регламент вчера, а прямой поиск по его названию работает. Первое желание — снизить порог сходства или переиндексировать всё. Оба действия могут потратить время, поскольку одинаковый пустой ответ скрывает несколько разных причин: приложение смотрит в другую коллекцию, права отсекают документ, тематический фильтр неверен или слишком строг последний порог.
Для диагностики полезно увидеть путь одного запроса. Не требуется выводить пользователю содержимое чужих документов. Достаточно записать технические количества в доступном только операторам журнале, сохранив изоляцию клиентов и ограничения на раскрытие статистики. В пользовательском ответе остаётся обычное сообщение о недостатке доступных источников.
Считайте там, где действительно меняется набор
Предположим, сотрудник отдела поддержки спрашивает: «Как согласовать возврат после окончания гарантии?» Рабочая коллекция содержит 120 фрагментов, но приложение могло подключиться к тестовой коллекции из двух записей. Поэтому начинайте с имени коллекции, версии индекса и числа активных записей. Эти сведения помогают проверить загрузку, ещё не разбирая смысл вопроса.
Затем считайте кандидатов, доступных текущему пользователю, результаты тематического отбора и записи после порога. В реальной векторной базе фильтры могут выполняться вместе с поиском, а приблизительный поиск не предоставляет полный промежуточный список. Тогда такие счётчики получают отдельными разрешёнными диагностическими запросами, а не объявляют размер возвращённого top-k размером индекса. У каждого числа должен быть понятен объект измерения.
Локальный пример ниже моделирует явный конвейер из четырёх этапов. Он нужен для проверки диагностического события, поэтому оценки сходства заранее заданы в синтетическом наборе. Пример не запускает эмбеддинги. При переносе в сервис такие записи приходят от поискового компонента с теми же идентификаторами и числовой оценкой.
rows = [
{"id": "return-1", "tenant": "shop", "topic": "returns", "score": 0.68},
{"id": "repair-2", "tenant": "shop", "topic": "repair", "score": 0.83},
{"id": "return-3", "tenant": "other", "topic": "returns", "score": 0.91},
]
def diagnose(rows, tenant, topic, minimum):
counts = {"index": len(rows)}
allowed = [r for r in rows if r["tenant"] == tenant]
counts["access"] = len(allowed)
relevant = [r for r in allowed if r["topic"] == topic]
counts["topic"] = len(relevant)
accepted = [r for r in relevant if r["score"] >= minimum]
counts["threshold"] = len(accepted)
return counts, [r["id"] for r in accepted]
first, empty = diagnose(rows, "shop", "returns", 0.75)
second, found = diagnose(rows, "shop", "returns", 0.65)
assert empty == [] and found == ["return-1"]
assert diagnose([], "shop", "returns", 0)[0]["index"] == 0
assert diagnose(rows, "unknown", "returns", 0)[0]["access"] == 0
print(first)
print(second, found)
Первый запуск даёт index=3, access=2, topic=1, threshold=0. Значит, индекс заполнен, а доступ и тема пропустили один фрагмент. Запрос потерял результат на последнем шаге: оценка 0.68 не достигла установленного 0.75. Во втором запуске после изменения порога до 0.65 остаётся один идентификатор. Эти числа не доказывают, что фрагмент отвечает на вопрос: перед изменением рабочего порога нужно прочитать источник.
Какое исправление следует из каждого результата
Если index равен нулю, проверьте выбранную коллекцию, состояние публикации новой загрузки и число активных документов. Полезный следующий шаг — найти известный идентификатор документа через административный интерфейс этой же среды. Повторная загрузка не поможет, если приложение продолжит обращаться к другой коллекции или старому alias.
Если результат исчез на access, сравните ожидаемую роль пользователя с правами документа и типом значения в метаданных. Ошибка в строке tenant_id или старый список групп после смены роли объясняют сбой лучше, чем настройка промпта. Ослаблять фильтр доступа ради получения ответа нельзя. Исправление должно восстановить разрешённую область поиска.
Если записи пропали на topic, проверьте сохранённое название категории и правило извлечения категории из вопроса. Например, пользователь спрашивает о возврате, а маршрутизатор выбрал ремонт. Сохраните исходный вопрос и выбранное значение отдельно. Это позволяет увидеть ошибку классификации, даже когда поиск внутри неправильно выбранной темы работает корректно.
Порог меняют только после проверки релевантности оставшихся кандидатов. Оценки зависят от модели и способа поиска; универсального значения 0.75 для всех коллекций нет. На нескольких реальных вопросах сравните нужный источник с посторонними. Если при снижении порога появляются в основном случайные документы, проблема может находиться в подготовке текста или поисковом запросе.
Передайте генератору объяснимый результат
После диагностики верните вопрос в обычный путь: «Объясни порядок согласования возврата после гарантии по приложенным положениям; если порядок не описан, укажи, какой информации не хватает». Сохраните выбранные идентификаторы рядом с ответом. При следующем пустом результате команда увидит, изменился ли индекс, фильтр или распределение оценок. Счётчики стоит проверять также на заведомо пустом индексе и пользователе без разрешённых документов.
В платной программе «LangChain 1.0.x: с нуля до продакшн LLM-приложений» есть RAG с гибридным поиском, реранжированием и измерением качества, задержки и стоимости. Здесь следующий шаг — подключить диагностическое событие к настоящему retrieval и проверить его на вопросах с известными источниками. Откройте программу курса, чтобы пройти путь от загруженного PDF до проверяемого ответа.
Продолжить на практике
- LangChain 1.0.x: c нуля до продакшн LLM-приложений ↗
Практический курс по LangChain. Соберёте продакшн LLM-бота: RAG по PDF с цитатами, веб-поиск и трекинг заказов, многошаговый агент для анализа CSV и финальное веб-приложение (UI + API). Плюс профессиональные блоки: PromptOps и версионирование промптов, метрики качества RAG, гибридный поиск с реранком, observability/стоимость, надёжность (ретраи, таймауты), guardrails и отдельный модуль по LangGraph для устойчивых workflow. P.S. Полностью обновили материалы на актуальную версию LangChain
