Аудит процесу отримання даних за технологією RAG щодо застарілих фрагментів, відхилень та шумного контексту.
Аудит Python для отримання даних за технологією RAG: ідеальні запити, критерії кореляції за косинусом, перевірки відхилень за методом Колмогорова-Смірнова та ефективність використання контексту, а також як читати звіт.
Джерела: I Built a RAG System That Audits Itself. Here’s How (With Code).
Тихий тип збою RAG
Уявіть собі бота підтримки, який повідомляє клієнту, що термін повернення товару становить 30 днів, хоча насправді це 14 днів. Документ з правилами було оновлено у січні, а індекс вбудовування останній раз змінювався у жовтні. Ніхто не постраждав, і судових позовів не було, але проблема полягає у тому, що ніхто не може сказати, як довго поширюється неправильна відповідь. Тиждень? Три місяці? Без можливості перевірки неможливо це визначити.
Саме такий збій вартий уваги, і це не той драматичний випадок, коли модель створює хибні уявлення чи довго розповідає без змісту. Це тихий збій. Індикатори працюють нормально, а невелика кількість користувачів помиляється протягом місяців без жодних попереджень.
Більшість команд, які розробляють RAG, вимірюють не те, що потрібно. Вони перевіряють, чи звучить кінцева відповідь добре, але ігнорують питання про те, чи був якісним матеріал, з якого вона сформована. Це відбувається переважно тому, що ніхто не надав їм простого інструменту для цього. Нижче наведено такий інструмент: скрипт на Python, який автоматично перевіряє шар отримання даних, створений за допомогою бібліотек з відкритим кодом та JSON-файлу з тестовими запитами; на налаштування потрібно приблизно 30 хвилин, і не потрібна жодна платформа для моніторингу чи знання статистики.
Зосередьтесь на отриманні даних, а не на моделі
Коли система RAG поводиться некоректно, першою реакцією є звинувачення моделі: неправильний запит, неправильна температура, неправильна версія. Якщо змінити запит або перейти на новішу модель, ситуація покращується на кілька тижнів, а потім проблема знову з’являється, тому що справжній причинник знаходиться в іншому місці. У продакшн-системах ланцюжок виконання зазвичай виглядає так:
Документ змінюється, але індекс вбудовування не оновлюється. Завантажується застарілий фрагмент, ШІ впевнено відповідає на основі устарілого контексту, відповідь є неправильною, і ніхто не може пояснити чому.
Модель не бреше. Вона робить саме те, що їй було сказано — створювати відповідь на основі наданих фрагментів. Якщо надати їй фрагменти віком у шість місяців, відповідь також буде старою на шість місяців, проте буде подана плавно та з бездоганною граматикою.
Проблема полягає у звичайному методі оцінки, який є кінцево-кінцевим: надходить запит, а потім оцінюється отримана відповідь. RAGAS, який належить до найбільш поширених фреймворків оцінки, був створений на основі повного процесу обробки та оцінює точність контексту, вірність та релевантність відповіді (Es та ін., 2023, EACL 2024). Це надійний фреймворк. Проте він оцінює систему в цілому, а не окремо шар пошуку, тож висока кінцева оцінка може приховувати повільне погіршення ефективності пошуку, коли модель іноді має удачу та компенсує це.
Під цією проблемою криється ще одна. У 2024 році Лю та співавтори повідомили, що моделі мови показують значно гірші результати, коли відповідна інформація знаходиться посеред довгого контексту, навіть у моделях, навчених спеціально для довгих вхідних даних. Тож отримання п’яти фрагментів, з яких три є лише частково релевантними, активно шкодить системі, а кількість токенів, витрачених на них, погіршує якість відповіді. Жодна з цих проблем не проявляється, якщо не перевірити безпосередньо шар отримання даних, що і є планом.
Три способи збою, які не помічають стандартні метрики
Отруєння фрагментами
Назва „отруєння чанками“ не походить з жодної наукової статті. Вона описує чанки, які здаються релевантними через збіг формулювань із запитом, тому вони посідають високі позиції у векторному пошуку, хоча не містять відповіді. Однією з поширених причин є розділення тексту так, що ключові слова залишаються в одному чанку, а справжній контент — у іншому. Чанк, багатий на ключові слова, отримує високі бали за коефіцієнтом косинусової схожості, тоді як корисний чанк не потрапляє до топ-результатів, і модель складає правдоподібну, але неправильну відповідь.
Застарілий контент
Припустимо, FAQ оновила свою політику повернення грошей у січні, а індекс було останній раз оновлено у жовтні. Старий чанк з політикою все ще посідає високі позиції для запитів про повернення грошей та продовжує вважатися „релевантним“ для моделі. У результаті користувачі отримують суперечливу інформацію від служби підтримки, хоча показники нічого не вказують.
Зміна ембеддингів
Цей процес є повільним та кумулятивним, і саме він є найнебезпечнішим із трьох. Модель імбеддингу фіксує корпус у тому вигляді, в якому він був на момент індексації. Кожна зміна документа, кожен новий термін, який потрапляє до домену, та кожна зміна у способі формулювання запитів користувачами трохи знижують точність цього „знімка“. Середнє косинусове схожість може знизитися з 0,82 до 0,74 протягом шести місяців. Нічого не ламається, жодних сповіщень не виникає, а занепад якості стає помітним лише тоді, коли відповіді значно погіршуються, а до цього моменту система вже відстає на кілька місяців.
Шум у контексті також має свої наслідки. Якщо у вікні є п’ять фрагментів, з яких три є нерелевантними, це призводить до марної витрати ресурсів та погіршення якості, адже контент у середині довгого контексту отримує менше уваги, ніж контент на краях, як це було задокументовано Лю та іншими.
Що робить аудит
Три перевірки відповідають цим трьом способам виникнення проблем.
- Релевантність та кількість результатів. Для кожного запиту в наборі тестів скрипт отримує топ-K фрагментів та обчислює косинусову схожість між ембеддингом запиту та ембеддингом кожного фрагмента. Він також перевіряє, чи повернулися фрагменти, які, як відомо, містять відповідь. Якщо цього не сталося, запит позначається як невдалий.
- Відхилення від базового рівня. Кожен запуск зберігає свою розподілу значень схожості, а подальші запуски порівнюють цей новий розподіл із збереженим базовим за допомогою тесту Колмогорова-Смірнова. Якщо вони суттєво відрізняються, спрацьовує сповіщення про відхилення. Не потрібно аналізувати математику: значення p нижче 0,05 просто свідчить про зміни, які вимагають уваги.
Аудит створює структурований звіт у форматі JSON. Достатньо щотижневої задачі cron, з увагою до кількості позначених елементів та аналізу відхилень. Більшість систем RAG ніколи не мали такого рівня моніторингу.
Золотий набір запитів
Перш ніж писати будь-який код, потрібен золотий набір запитів: файл JSON із репрезентативними запитами, кожен з яких поєднаний із ідентифікаторами частин, що мають з’явитися у його результатах.
[
{
"query": "What is the refund policy for digital products?",
"expected_chunk_ids": ["faq_doc_chunk_12", "faq_doc_chunk_13"],
"notes": "Customer FAQ, policy updated 2024-Q1"
},
{
"query": "How do I reset my API key?",
"expected_chunk_ids": ["api_docs_chunk_07"],
"notes": "API documentation, stable"
}
]
Розмір має значення. Спочатку здавалося, що 20 запитів буде достатньо, але це не так, адже тест на зміщення вимагає більшої кількості даних, а 20 значень створюють занадто розріджену розподілу, якій неможливо довіряти. Вважайте 50 мінімальною кількістю, а 100 або більше — рівнем, при якому сигнали зміщення стають значущими.
Почніть з вибору найчастіших запитів користувачів з журналів додатку. Виконуйте пошук для кожного з них вручну, перевіряйте результати та фіксуйте, які ID фрагментів знаходяться у топових позиціях. Ця робота нудна та займає кілька годин, проте саме вона не може бути автоматизована, і від неї залежить усе інше. Деякі звички допомагають:
- Розподіляйте запити по різних категоріях тем замість того, щоб групувати їх навколо одного документа.
- Додайте кілька запитів, які не повинні повертати жодних результатів, адже негативні приклади допомагають виявити хибно позитивні результати пошуку.
Структура проекту та основний скрипт
Репозиторій організований наступним чином:
rag-retrieval-audit/
├── audit/
│ ├── __init__.py # Package exports
│ ├── rag_audit.py # Main audit logic — all three checks
│ └── config.py # All thresholds and model settings
├── examples/
│ ├── golden_queries.json # Sample golden set (10 queries)
│ └── run_audit.py # End-to-end demo — runs without an existing collection
├── tests/
│ └── test_audit.py # Unit tests for all scoring functions
├── requirements.txt # sentence-transformers, chromadb, scipy, numpy
└── README.md # Setup, usage, how to read the report
Ось структура файлу rag_audit.py, який містить усі три перевірки. Це огляд для орієнтації, а не повна реалізація.
# rag_audit.py — structure overview
# Full implementation: https://github.com/satyam671/rag-retrieval-audit
# ── CONFIGURATION (tune to your pipeline)
EMBEDDING_MODEL = "all-MiniLM-L6-v2" # must match your index
TOP_K = 5
RELEVANCE_THRESHOLD = 0.70
DRIFT_P_THRESHOLD = 0.05
EFFICIENCY_THRESHOLD = 0.40
# ── CHECK 1: Are the right chunks coming back?
def score_relevance(query_embedding, chunk_embeddings, expected_ids, retrieved_ids):
"""Cosine similarity per chunk + expected chunk hit/miss against golden set."""
...
# ── CHECK 2: Has retrieval quality shifted over time?
def detect_drift(current_sims, baseline_path=None):
"""Two-sample KS test comparing current similarity distribution to baseline."""
...
# ── CHECK 3: How much of the context window is signal?
def score_efficiency(retrieved_docs, answer):
"""Token overlap between retrieved chunks and the LLM answer."""
...
# ── RUNNER
def run_audit(golden_set_path, collection_name, chroma_persist_dir,
baseline_path=None, answers_path=None, output_path="rag_audit_report.json"):
"""Runs all three checks, writes a structured JSON report, saves the baseline."""
...
У документації до скрипту пояснюється, як клонувати його, налаштувати на колекцію ChromaDB та запустити, включаючи інструкції щодо заміни на інший сховище векторів.
Розгляд кожної перевірки
Оцінка релевантності фрагментів
Перевірка релевантності виконує дві дії, які люди часто пропускають під час її створення самостійно.
По-перше, вона обчислює косинусову схожість безпосередньо з ембеддингів, не використовуючи значення відстаней, які повертає ChromaDB. Стандартною метрикою ChromaDB є L2, а не косинусова схожість, і ці показники не є взаємозамінними. Їхнє поєднання дає оцінки, які здаються правдоподібними, але насправді вимірюють щось інше.
По-друге, це підтверджує, що очікувані ID блоків золотого набору з’явилися у списку топ-K, і саме це виявляє проблему отруєння блоків. Середнє значення схожості 0,75 здається прийнятним, поки ви не дізнаєтесь, що блок, призначений для відповіді на запит, мав оцінку 0,62 та посів шосте місце, на одне місце після порогового значення. Модель так і не отримала правильної відповіді, хоча показники схожості все ще здавалися прийнятними.
Практична різниця між використанням механізму контролю та його відсутністю чітко видно у коді.
# WITHOUT the relevance gate — standard approach most teams use
def build_context_naive(query, collection, top_k=5):
results = collection.query(
query_embeddings=[embed(query)],
n_results=top_k,
include=["documents"]
)
# Pass everything back, no quality check
return "\n\n".join(results["documents"][0])
# WITH the relevance gate - what the audit tells you to build
def build_context_gated(query, collection, model, top_k=5, threshold=0.70):
q_emb = model.encode([query], normalize_embeddings=True)[0]
results = collection.query(
query_embeddings=[q_emb.tolist()],
n_results=top_k,
include=["documents", "embeddings", "ids"]
)
passed_chunks = []
for i, chunk_emb in enumerate(results["embeddings"][0]):
sim = cosine_sim(q_emb, np.array(chunk_emb))
if sim >= threshold:
passed_chunks.append(results["documents"][0][i])
# Empty context is better than wrong context
return "\n\n".join(passed_chunks)
У наївній версії найкращі за рейтингом фрагменти використовуються незалежно від їхньої якості. У версії з фільтрацією будь-які фрагменти, які не заслуговують на використання, залишаються осторонь. Відповідь «У мене недостатньо інформації, щоб відповісти на це» у порожньому контексті краща, ніж впевнена, але хибна відповідь, заснована на шумі.
Виявлення змін у ембеддингах
Перевірка на зміни ґрунтується на тесті Колмогорова-Смірнова. Він порівнює форму поточного розподілу схожості з збереженою базовою моделлю та виявляє значні відхилення. Чому саме розподіли, а не середні значення? Тому що одне число приховує занадто багато інформації. Середнє значення може залишатися стабільним на рівні 0,78, тоді як розподіл під ним стає плоским та розширюється, що означає, що деякі запити погіршуються, а інші компенсують це. Тест KS виявляє зміни в розподілах, які залишаються непоміченими середніми значеннями.
У зразковій діаграмі показано зміни у вбудовуванні інформації в промислову систему RAG через шість місяців після оновлення корпусу даних без повторної індексації: статистика KS становить 0,21, а значення p — 0,003.
Є один недолік: для надійних результатів тесту потрібна достатня кількість даних. При 15 запитах значення p коливаються, що створює хибну впевненість, тоді як 50 або більше запитів дають стабільні результати. Це ще одна причина, чому розмір „золотого набору“ має більше значення, ніж здається.
Ефективність вікна контексту
Це найскладніша з трьох перевірок, оскільки існують реальні обмеження. Перекриття на рівні слів між отриманими фрагментами та відповіддю моделі є лише орієнтовним показником. Коли модель сильно перефразовує, як це роблять моделі класу GPT-4, таке перекриття недооцінює кількість контексту, яка насправді була використана.
Проте цей метод виявляє систематичні проблеми. Якщо показники ефективності постійно залишаються нижче 40%, щось у попередніх етапах працездатності не так: фрагменти занадто великі, значення top-K занадто високе, або процес пошуку знаходить текст, пов’язаний з темою, але не відповідний запиту. Ця перевірка не є точною, але вказує правильний напрямок, і цього достатньо, щоб розпочати розслідування.
Читання звіту
Результатом є файл JSON, і наступні поля заслуговують уваги:
flag_rate— це частка запитів, які спричинили проблеми. Значення вище 20% означає, що процес пошуку потребує уваги; різкий зріст з 8% за тиждень до 22% наступного тижня свідчить про зміни.drift_analysis.drift_detected— це булеве значення, на яке варто звернути увагу. Якщо воно дорівнюєtrue, потрібно перевірити значенняmean_delta, щоб зрозуміти напрямок змін. Негативне значення delta означає зниження схожості, що є сигналом до переіндексації.drift_analysis.ks_statisticзростає зі збільшенням розміру зсуву розподілу. Значення нижче 0,10, ймовірно, є шумом. Значення вище 0,20 вказують на справжню проблему, незалежно від значення p.query_results[i].relevance.expected_chunk_hit— це істинний показник для кожного запиту. Значенняfalseозначає, що фрагмент, який мав би відповісти на запит, не потрапив до списку топ-K. Якщо це трапляється у більш ніж 15% елементів золотого набору, необхідно перестворити індексацію.
Приклад запускається за допомогою однієї команди, яка показана тут.
python -m examples.run_audit
Довідник узагальнює процес аудиту в одну картину.
Виконуйте його щотижня та зберігайте всі базові показники. Тренд між виконаннями говорить більше, ніж будь-яке окреме виконання.
Урок про розмір вибірки
Виконання перевірки на відхилення на наборі з 15 запитів дало значення p, які не мали сенсу. Тест технічно працював, але ненадійно, і було легко втратити два дні, підозрюючи проблеми зі статистичною реалізацією, перш ніж зрозуміти, що даних просто замало.
При менш ніж 50 запитах показники відхилення не можна вважати надійними, тому спочатку створіть набір, а вже потім увімкніть перевірку відхилень. Корпус, який охоплює кілька окремих тематичних доменів, вимагає окремого набору для кожного домену, який перевіряється окремо, адже розподіл у змішаному корпусі може приховати зниження в одному домені, яке було б помітним, якби його розглядати окремо.
Обмеження цього підходу
Скрипт перевіряє, чи були отримані правильні фрагменти. Він не оцінює, чи належним чином розміщені межі фрагментів, чи підходить розмір фрагмента для типів запитів, чи слід замінити фіксований розмір на семантичне розділення.
Ці питання мають велике значення. Бенчмарки NVIDIA на п’яти наборах даних, які узагальнено у аналізі FloTorch, показали, що оптимальний розмір фрагмента змінюється залежно від типу запиту: запити на факти мають оптимальний розмір від 256 до 512 токенів, тоді як аналітичні запити потребують 1024 токенів або більше. Будь-який фіксований розмір фрагмента є компромісом, а ідеальний набір, який поєднує обидва типи запитів, призводить до середнього рівня невідповідності. Пряме тестування стратегії розділення на фрагменти та порівняння семантичного та фіксованого розділення за допомогою реальних даних — це окреме завдання.
Посилання
- Стаття RAGAS про автоматизовану оцінку генерації з підсиленням через пошук (arXiv 2309.15217, 2023; EACL 2024).
- Дослідження «втрачене посередині» щодо поведінки мовних моделей у довгому контексті, опубліковане у 12-му томі TACL у 2024 році.
- Прикладні дослідження NVIDIA щодо стратегій часткового оброблення документів різних типів, 2024 рік.