Исследования ИИ

Скрытый баг RAG: когда система врёт уверенно и тихо

RAG находит документ, цитирует, отвечает уверенно — и неправильно. Один поиск не покрывает сложные вопросы. Разбираю тихий режим отказа и 4 уровня защиты.

21 марта 2026 г.
9 мин чтения
мультиагентные системыИИ в финансах

Наш 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) этого не покажут: бот быстрый, доступный, пользователи довольны — потому что не знают, что ответ неправильный.

Узнают потом. От клиента, юриста, руководства. Лучше узнать от чеклиста.

Автор: Алик Завалишев

Эксперт по ИИ и автоматизации процессов

Больше статей