Скрытый баг RAG: когда система врёт уверенно и тихо
RAG находит документ, цитирует, отвечает уверенно — и неправильно. Один поиск не покрывает сложные вопросы. Разбираю тихий режим отказа и 4 уровня защиты.
Наш RAG-бот для внутренней документации. Менеджер спрашивает: «Какой лимит возврата для корпоративных клиентов на тарифе Enterprise с годовым контрактом?»
Ответ: «Лимит возврата составляет 30 дней с момента оплаты.» Ссылка на документ. Тон уверенный. Ни одного маркера сомнения.
Правильный ответ: 90 дней. Для Enterprise с годовым контрактом — отдельная политика в другом документе. Бот нашёл общую политику возвратов (30 дней для всех). Не нашёл исключение. Ответил тем, что нашёл. Уверенно.
Менеджер сообщил клиенту 30 дней. Клиент показал контракт с 90. Неловкий звонок. Потеря доверия. А бот по метрикам — работал отлично: ответил быстро, дал ссылку, тикет закрыт.
Что такое silent failure
Обычная галлюцинация LLM: модель выдумывает факт. Заметно — факта не существует, проверка покажет.
Silent failure RAG: модель не выдумывает ничего. Цитирует реальный документ. Ссылка рабочая. Информация настоящая. Просто — не вся. Не тот документ. Не полный контекст.
Пользователь доверяет: бот же дал ссылку на документ. Зачем сомневаться? Ошибка всплывает через дни — когда кто-то натыкается на последствия. Через клиента, юриста, руководство.
Галлюцинацию можно поймать автоматическим фактчекингом. Silent failure — нет: всё, что бот сказал, фактически верно. В рамках того, что нашёл. Он просто нашёл не всё.
Механика: один поиск — одна дырка
Классический RAG: Вопрос → Embedding → Поиск top-K чанков → LLM → Ответ. Один поисковый запрос. Один набор результатов. Для простых вопросов — идеально. «Какой email поддержки?» → чанк с контактами → точный ответ. Ломается на сложных — когда ответ не живёт в одном чанке.
Multi-document: ответ разбросан
«Лимит возврата для Enterprise-annual» — общая политика в одном файле, исключение для Enterprise в другом. Embedding вопроса семантически ближе к первому. Второй не попадает в top-5. Модель отвечает по неполным данным, не зная, что они неполные.
Сравнительный: нужен расчёт
«Какой тариф выгоднее для команды из 12 человек?» Нужны описания трёх тарифов, цены, скидки. Ни один чанк не содержит готовое сравнение. RAG находит один тариф и строит ответ на нём.
С отрицанием: поиск промахивается
«В каких случаях мы НЕ делаем возврат?» Embedding «возврат» ближе к документам про процедуру возврата (как сделать), а не к ограничениям (когда нельзя). Семантическая близость — не то же, что релевантность для ответа.
Агрегационный: данные размазаны
«Сколько интеграций мы поддерживаем?» Интеграции описаны в 8 чанках: CRM, платежи, аналитика, мессенджеры. RAG находит один чанк с 5 интеграциями. Отвечает «5». Реально — 23.
Временной: нужна версионность
«Что изменилось в SLA с января?» Нужны две версии документа. RAG находит текущую. Предыдущая не попадает в top-K — или её вообще нет в индексе.
Масштаб: аудит на 200 вопросах
Я проверил наш бот. 100 простых вопросов (ответ в одном чанке) и 100 сложных (multi-document, сравнительные, условные).
Простые: 94% корректных. Работает.
Сложные: 61% корректных. 24% — частично правильные (неполные). 15% — неправильные.
И все 39% ошибочных — уверенные ответы со ссылками на реальные документы. Ни одного «я не уверен». Ни одного disclaimer.
Если ваш RAG обслуживает только FAQ — проблемы нет. Но реальные пользователи задают сложные вопросы. И 30-40% тихих ошибок — это не edge case, это системная проблема.
Диагностика
Retrieval recall
Для 50 сложных вопросов определите «золотой набор» документов — какие нужны для полного ответа. Проверьте, сколько RAG реально находит.
def retrieval_recall(question, gold_doc_ids, top_k=5):
retrieved = rag.retrieve(question, top_k=top_k)
found = {d.id for d in retrieved} & set(gold_doc_ids)
return len(found) / len(gold_doc_ids)Recall < 0.7 на multi-document вопросах — silent failures гарантированы.
Completeness check
Стандартный faithfulness проверяет: «Ответ основан на найденных документах?» Для silent failure мало — ответ действительно основан на документах. Просто документы неполные. Нужен расширенный check:
PROMPT = """
Вопрос: {question}
Найденные документы: {chunks}
Ответ: {answer}
Оцени:
1. faithfulness (0-1): ответ основан на документах?
2. completeness (0-1): документы содержат ВСЮ информацию?
3. gaps: какой информации не хватает?
""" Faithfulness 0.95, completeness 0.4 — классический silent failure.
Аудит «уверенных» ответов
Выборка ответов без disclaimers. Ручная проверка. Если 15%+ неполные — системная проблема.
Четыре уровня защиты
Уровень 1: научить говорить «не знаю» (1 час)
Добавить в промпт LLM: «Если документы не содержат полной информации — скажи. Не достраивай. Укажи, чего не хватает.» Работает в ~40% случаев. Модель иногда замечает неполноту. Но часто — нет: из одного найденного документа ответ выглядит полным. Дёшево. Лучше, чем ничего. Не решение.
Уровень 2: multi-query retrieval (1 день)
Один вопрос → 3-4 поисковых запроса → объединение результатов:
async def multi_query_retrieve(question, top_k=5):
queries = await generate_sub_queries(question, n=4)
all_chunks = []
for q in queries:
all_chunks.extend(vector_db.search(q, top_k=top_k))
return rerank(deduplicate(all_chunks), question, top_k=top_k * 2)«Лимит возврата для Enterprise-annual» → четыре запроса: «политика возвратов» → общий документ; «Enterprise тариф условия» → документ Enterprise; «годовой контракт возврат исключения» → исключения; «корпоративные клиенты лимиты» → доп. условия.
Стоимость: +$0.003 (один вызов gpt-4o-mini) + три поиска по векторной БД (бесплатно). Recall на сложных вопросах вырос с 0.58 до 0.81. Silent failures — с 39% до 18%.
Уровень 3: agentic RAG (1 неделя)
Цикл: поиск → оценка полноты → целенаправленный доиск:
async def agentic_rag(question, max_steps=3):
context = []
for step in range(max_steps):
query = question if step == 0 else await gap_query(question, context, gaps)
context.extend(await retrieve(query))
check = await assess_completeness(question, context)
if check["score"] > 0.85:
break
gaps = check["missing"]
return await generate(question, context)Итерация 1: нашли общую политику (30 дней). Оценка: «Нет специфики Enterprise.» Итерация 2: целевой поиск → нашли исключение (90 дней). Полно. Ответ: «90 дней для Enterprise-annual (исключение из стандартных 30).» Стоимость: +$0.01-0.02 за запрос. Silent failures: с 39% до 8%.
Уровень 4: confidence gating (2 дня)
Каждый ответ — с оценкой уверенности. При низкой — не отвечать:
async def gated_answer(question, context, answer):
conf = await score_confidence(question, context, answer)
if conf > 0.85:
return {"answer": answer, "action": "show"}
elif conf > 0.6:
return {"answer": answer, "action": "show_with_disclaimer"}
else:
return {"action": "escalate_to_human"}Три режима: уверенный ответ, ответ с предупреждением, эскалация. Бот перестаёт уверенно врать.
Что я использую
Multi-query (уровень 2) + confidence gating (уровень 4) — на всех запросах. Agentic RAG (уровень 3) — для критичных доменов: юридические вопросы, финансы.
+$0.01-0.02 за запрос. При 1 000 запросов/день — $10-20/мес. Один инцидент от уверенного неправильного ответа — от $500 до потери клиента.
Чеклист
Задайте своему RAG по 10 вопросов каждого типа: multi-document (ответ в 2+ источниках), сравнительный (что лучше / выгоднее), с отрицанием (когда НЕ / нельзя), временной (что изменилось / было раньше), агрегационный (сколько всего / полный список).
Если 20%+ ответов неполные при уверенном тоне — silent failures. Стандартные метрики (latency, uptime, CSAT) этого не покажут: бот быстрый, доступный, пользователи довольны — потому что не знают, что ответ неправильный.
Узнают потом. От клиента, юриста, руководства. Лучше узнать от чеклиста.