Головна / Статті

Виправлення проблем тихих збоїв 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, моделі, які повинні вирішувати, чи є у них достатньо доказів, та процеси, які виграють від повторної спроби з іншим запитом.

Погані кандидати – це прості відповіді на FAQ, автодоповнення з низькою затримкою, пошук у стабільній документації та все те, де кожен додатковий виклик моделі коштує занадто дорого. Проблема має виправдовувати використання агентного пошуку. Якщо фільтр метаданих може її вирішити, використовуйте цей фільтр.

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

Перш ніж звинувачувати модель, перегляньте це:

  1. Чи мають документи версії?
  2. Чи дотримуються правила дозволів перед отриманням даних?
  3. Чи виключаються за замовчуванням застарілі документи?
  4. Чи ґрунтуються фрагменти на змісті, а не лише на кількості символів?
  5. Чи покриваються точні терміни за допомогою пошуку за ключовими словами?
  6. Чи обробляються таблиці та зображення окремо?
  7. Чи враховуються у оцінках випадки відсутності відповіді?
  8. Чи фіксуються ID отриманих документів?
  9. Чи перевіряються посилані джерела?
  10. Чи оцінюється якість відповіді окремо від якості пошуку?

Більшість проблем у RAG не є загадковими. Вони просто залишаються непоміченими.

Де ламається ланцюг доказів

RAG надає системам LLM відчуття стабільності, але ця стабільність не виникає автоматично. Модель може використовувати лише ті докази, які отримує. Механізм пошуку може знаходити лише те, що було правильно індексовано. Індекс може надавати лише ті метадані, які були збережені. Оцінювач може виявити лише ті помилки, які хтось потрудився описати. Ця робота менш приваблива, ніж демонстрація, але саме в ній будується довіра.

Тож коли система RAG працює некоректно без видимих проблем, пошук причини у запиті — неправильний підхід. Краще запитати: де саме зламалася ланцюгова структура доказів?