Главная / Статьи

Устранение скрытых ошибок RAG: извлечение информации, разбиение на части, фильтры и оценка результатов

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

Издатель: REACTAPP.TOP8 мин чтения1774 слов

Источники: RAG Is Failing Quietly: A Debugging Playbook for Python Teams

Неудачи, которые никто не видит

Самые раздражающие ошибки RAG — это не те, что сопровождаются шумом. С такими ошибками легко справиться: бот признаёт, что ничего не нашёл, база векторов не работает, задача по генерации эмбеддингов сорвалась или ответ приходит без цитат.

Тихие ошибки стоят дороже. Документы находятся, ответ читается гладко, цитаты выглядят достоверными, а панель управления сообщает о успешном поиске. Однако специалист в данной области прочитывает ответ и говорит, что он неверен. Именно тогда команда понимает, что всё это время отлаживала неправильный уровень.

RAG — это не отдельная функция. Это последовательность решений, которые накапливаются друг за другом. Кто-то определяет, как интерпретировать и переписывать вопрос, где разбивать документы, какой инструмент поиска использовать, какие метаданные сужают круг кандидатов, как переоценивать приоритеты в списке, что в итоге попадет в запрос, как будет вести себя модель при недостатке доказательств и соответствует ли написанный ответ тому, что было предоставлено. Сосредотачиваясь только на окончательном запросе, мы приходим к выводам уже после того, как ущерб нанесен.

Процесс, находящийся в процессе отладки

Реалистичная система больше похожа на следующую схему, чем на аккуратную диаграмму на слайде:

flowchart LR
    A[User question] --> B[Query rewrite]
    B --> C[Retriever]
    C --> D[Reranker]
    D --> E[Evidence pack]
    E --> F[Answer generator]
    F --> G[Verifier]
    G --> H[Final answer]
    C --> I[Trace log]
    D --> I
    E --> I
    F --> I
    G --> I

Любой этап может дать сбой. Вина возлагается на генератор, поскольку это самый заметный этап, но многие сбои возникают раньше. Механизм поиска может выбрать неверный период времени. Механизм ранжирования может выдвинуть на первое место незначимый фрагмент текста. Пакет доказательств может не включить единственный полезный абзац из-за ограниченного количества токенов. Эффективная отладка подразумевает видимость каждого этапа.

Сбой 1: похожий текст не является полезным доказательством

Векторный поиск находит семантическое сходство, которое не всегда соответствует задаче. Возьмем случай, когда пользователь спрашивает, могут ли корпоративные клиенты получить возврат средств после обновления политики во втором квартале. Механизм поиска может вернуть фрагмент, касающийся политики возврата средств, просто потому что в нем упоминаются корпоративные клиенты, возврат средств и политика. Однако этот фрагмент может быть устаревшим, неполным или не связанным с вторым кварталом.

Полезная отчетность показывает ID полученного документа, его версию, дату вступления в силу, оценку сходства, оценку переранжирования, примененные фильтры метаданных и то, цитировался ли этот документ в окончательном ответе. Если в журналах записано лишь что-то вроде оценки 0.83, ошибку невозможно диагностировать.

Сбой 2: разбиение на фрагменты нарушило смысл

Разбиение на фрагменты — это не просто деталь хранения данных; оно влияет на качество ответа. Политика возврата средств может быть структурирована по критериям приемлемости, исключениям, дополнениям для корпоративных клиентов, датам вступления в силу и региональным особенностям. Если инструмент разбиения отделяет дату вступления в силу от исключений, ответ может цитировать раздел с критериями приемлемости, упуская при этом условие, которое меняет итоговый результат.

Лучшие фрагменты содержат стабильный идентификатор документа, заголовок раздела, версию, дату, достаточное количество соседних текстов для сохранения смысла и указатель на исходный документ. Ничто из этого не оправдывает использование огромных фрагментов, которые лишь добавляют шум. Цель — фрагменты, сохраняющие момент, когда принимается решение.

Ошибка 3: отсутствие фильтров метаданных

Многие системы RAG начинаются как чистые системы семантического поиска. Это работает до тех пор, пока одна и та же тема не появляется в разных версиях, продуктах, тенантах, регионах или группах разрешений. Вопрос о текущей политике возврата средств в компании не должен заставлять систему искать каждый документ о возвратах, когда-либо созданный компанией. Сначала должен произойти фильтраций.

from dataclasses import dataclass
from datetime import date

@dataclass(frozen=True)
class SearchFilters:
    product: str | None
    customer_tier: str | None
    region: str | None
    as_of: date
    permission_group: str

def build_filters(user_context: dict) -> SearchFilters:
    return SearchFilters(
        product=user_context.get("product"),
        customer_tier=user_context.get("tier"),
        region=user_context.get("region"),
        as_of=date.today(),
        permission_group=user_context["permission_group"],
    )

Порядок имеет значение. Сначала применяйте фильтры разрешений и свежести, прежде чем просить модель о рассуждениях. Получение частного или устаревшего контекста в надежде, что подсказка всё исправит, — плохая стратегия.

Ошибка 4: набор для оценки «удачных» сценариев

Самый быстрый способ улучшения системы RAG — это небольшой, целенаправленно составленный набор для оценки. Не используйте тысячи примеров. Начните с примеров в тридцать, отражающих реальные ситуации сбоев системы:

  • вопросы, на которые нет ответа в корпусе
  • вопросы, требующие двух источников
  • вопросы, в которых старая и новая политика противоречат друг другу
  • вопросы с похожей формулировкой, но о разных продуктах
  • вопросы, зависящие от таблицы
  • вопросы, к ответам на которые правильно обратиться к человеку

Простая структура для поиска информации может выглядеть так:

from dataclasses import dataclass

@dataclass(frozen=True)
class RagCase:
    question: str
    required_doc_ids: set[str]
    forbidden_doc_ids: set[str]

def evaluate_retrieval(cases: list[RagCase], retrieve) -> dict:
    total = len(cases)
    hit = 0
    leaked_forbidden = 0

    for case in cases:
        results = retrieve(case.question)
        retrieved_ids = {item["doc_id"] for item in results}

        if case.required_doc_ids & retrieved_ids:
            hit += 1

        if case.forbidden_doc_ids & retrieved_ids:
            leaked_forbidden += 1

    return {
        "cases": total,
        "required_hit_rate": hit / total,
        "forbidden_leak_rate": leaked_forbidden / total,
    }

Это сознательно упрощено. Метод не гарантирует высокого качества ответов, но позволяет выявить множество сбоев при поиске информации ещё до того, как модель увидит контекст.

Сбой 5: оценка ответов без учёта доказательств

Ответ, сгенерированный с использованием технологии RAG, должен оцениваться на основе фактически предоставленных доказательств, а не всего корпуса данных. Это различие имеет значение с трёх точек зрения: если правильный ответ существует в базе знаний, но не был найден, от модели нельзя ожидать, что она его знает — это сбой при поиске. Если были найдены подходящие доказательства, но ответ их проигнорировал, это сбой при генерации. Если в ответе приводится источник, который не подтверждает утверждение, это сбой при обосновании ответа. Для всех трёх случаев необходимы соответствующие метки.

@dataclass(frozen=True)
class AnswerEval:
    question: str
    answer: str
    evidence_doc_ids: set[str]
    expected_claims: set[str]

def simple_claim_check(eval_case: AnswerEval) -> dict:
    answer_lower = eval_case.answer.lower()
    missing = [
        claim
        for claim in eval_case.expected_claims
        if claim.lower() not in answer_lower
    ]

    return {
        "passed": len(missing) == 0,
        "missing_claims": missing,
        "evidence_count": len(eval_case.evidence_doc_ids),
    }

В процессе производства могут применяться человеческий аудит или оценка с помощью нейросети для более тонких утверждений, но даже краткая проверка утверждений лучше, чем расплывчатое одобрение.

Лучшая трассируемость

Каждый ответ должен оставлять за собой структурированную трассу:

import time
import uuid
from dataclasses import dataclass, field

@dataclass
class RagTrace:
    run_id: str = field(default_factory=lambda: str(uuid.uuid4()))
    started_at: float = field(default_factory=time.time)
    query: str = ""
    rewritten_query: str | None = None
    filters: dict = field(default_factory=dict)
    retrieved: list[dict] = field(default_factory=list)
    evidence_doc_ids: list[str] = field(default_factory=list)
    prompt_tokens: int = 0
    completion_tokens: int = 0
    verifier_result: str | None = None
    latency_ms: int | None = None

def finish_trace(trace: RagTrace) -> RagTrace:
    trace.latency_ms = int((time.time() - trace.started_at) * 1000)
    return trace

Благодаря ему на практические вопросы находятся ответы. Была ли найдена правильная коллекция? Удалены ли фильтрами нужные документы? Обнаружен ли ожидаемый источник? Содержит ли набор доказательств слишком много контекста? Заметил ли проверщик отсутствующие цитаты? Происходит ли задержка из-за поиска, генерации или проверки? Без ответов на эти вопросы пока нет системы RAG — есть лишь промпт с встроенным полем поиска.

Гибридный поиск часто оказывается неэффективным решением

Команды слишком часто слишком быстро переходят от простого векторного поиска к агентному RAG. Сначала стоит попробовать гибридный способ поиска. Семантический поиск проявляет себя наилучшим образом, когда пользователь и документ используют разные слова. Поиск по ключевым словам эффективен там, где важна точность буквальных строк, таких как идентификатор, код ошибки, имя поля или формулировка в юридическом тексте. В реальных системах зачастую требуются оба подхода.

def hybrid_rank(vector_results: list[dict], keyword_results: list[dict]) -> list[dict]:
    scores: dict[str, float] = {}
    items: dict[str, dict] = {}

    for rank, item in enumerate(vector_results, start=1):
        doc_id = item["doc_id"]
        scores[doc_id] = scores.get(doc_id, 0.0) + 1.0 / (rank + 10)
        items[doc_id] = item

    for rank, item in enumerate(keyword_results, start=1):
        doc_id = item["doc_id"]
        scores[doc_id] = scores.get(doc_id, 0.0) + 1.0 / (rank + 10)
        items[doc_id] = item

    return sorted(
        items.values(),
        key=lambda item: scores[item["doc_id"]],
        reverse=True,
    )

Этот способ слияния на основе взаимного рангирования не является сложным, но при этом полезен. Он поощряет документы, присутствующие в обоих списках результатов, при этом оставляя возможность любому из поисковых механизмов выявить что-то важное.

Когда агентный поиск оправдан

Агентный RAG подходит для задач, требующих детального исследования. Хорошими примерами являются случаи, когда при первом просмотре часто не хватает доказательств, вопросы, связанные с несколькими документами, рабочие процессы, где необходимо выбирать между SQL, векторным поиском и вызовами API, модели, которым нужно определить наличие достаточных доказательств, а также сценарии, при которых полезно попробовать снова с другим запросом.

Плохими кандидатами на использование агентного поиска являются простые ответы на часто задаваемые вопросы, автодополнение с низкой задержкой, поиск в стабильной документации и любые ситуации, где каждый дополнительный вызов модели слишком дорог. Проблема должна оправдывать применение агентного поиска; если её можно решить с помощью фильтра метаданных, используйте этот фильтр.

Чек-лист для внедрения в производство

Прежде чем винить модель, проверьте следующее:

  1. Имеются ли версии документов?
  2. Применяются ли права доступа перед получением данных?
  3. Исключаются ли устаревшие документы по умолчанию?
  4. Определяются ли фрагменты на основе смысла, а не только количества символов?
  5. Покрываются ли точные термины поиском по ключевым словам?
  6. Обрабатываются ли таблицы и изображения отдельно?
  7. Учитываются ли в оценках случаи отсутствия ответа?
  8. Фиксируются ли идентификаторы полученных документов?
  9. Проверяются ли цитируемые источники?
  10. Оценивается ли качество ответа отдельно от качества поиска?

Большинство сбоев в работе RAG не являются загадочными. Они просто остаются незамеченными.

Где нарушается цепочка доказательств

RAG придает системам LLM ощущение надежности, но такая надежность не возникает автоматически. Модель может использовать только те доказательства, которые ей предоставлены. Механизм поиска может находить только то, что было правильно индексировано. Индекс может содержать только те метаданные, которые были сохранены. Оценщик может выявить только те сбои, которые кто-то потрудился описать. Эта работа менее заметна, чем демонстрация, но именно в ней формируется доверие.

Поэтому, когда система RAG работает некорректно незаметно, начинать следует не с промпта. Лучший вопрос звучит так: в каком месте оборвалась цепочка доказательств?