Аудит процесса поиска в 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 et al., 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 значений дают слишком редкое распределение, которому нельзя доверять. Считайте, что минимальное количество — 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, а не косинусное сходство, и эти показатели не являются взаимозаменяемыми. Их смешивание даёт оценки, которые кажутся разумными, но на самом деле измеряют что-то другое.
Во-вторых, это подтверждает, что ожидаемые идентификаторы блоков золотого набора появились в списке топ-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, в то время как распределение под ним становится плоским и расширяется, что означает, что некоторые запросы становятся гораздо хуже, в то время как другие компенсируют это. Тест КС выявляет изменения в распределении, которые ускользают от средних значений.
На приведённом графике показано изменение качества встраивания информации в производственную систему 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 запросов для обнаружения смещений получались п-значения, не имевшие смысла. Тест технически выполнялся, но ненадёжно, и было легко потратить два дня на расследование проблем с статистической реализацией, прежде чем понять, что данных просто недостаточно.
При количестве запросов менее 50 показатели смещения нельзя считать надёжными, поэтому сначала создайте набор, а затем включите проверку смещений. Для корпуса, охватывающего несколько отдельных тематических областей, требуется по одному золотому набору на каждую область, который аудируется отдельно, поскольку распределение в смешанном корпусе может скрыть ухудшение в одной области, которое было бы заметно при рассмотрении её отдельно.
Ограничения этого подхода
Скрипт проверяет, что загружаются правильные фрагменты. Он не оценивает, правильно ли расположены границы фрагментов, подходит ли их размер для типов запросов или следует ли использовать семантическое разделение вместо разделения фиксированного размера.
Эти вопросы имеют большое значение. Тесты NVIDIA на пяти наборах данных, представленные в анализе FloTorch, показали, что оптимальный размер фрагмента варьируется в зависимости от типа запроса: для фактологических запросов он составляет от 256 до 512 токенов, тогда как для аналитических требуется 1024 и более токенов. Любой фиксированный размер фрагмента является компромиссом, а идеальный набор параметров, сочетающий оба типа запросов, в среднем приводит к некоторому несоответствию. Прямое тестирование стратегии разделения на фрагменты и сравнение семантического и фиксированного подходов с использованием реальных данных — это отдельная задача.
Список литературы
- Статья RAGAS об автоматизированной оценке генерации с усилением за счёт поиска (arXiv 2309.15217, 2023; EACL 2024).
- Исследование поведения языковых моделей в длинных контекстах, описанное как «затерянное посередине», опубликованное в томе 12 журнала TACL в 2024 году.
- Прикладные исследования NVIDIA по стратегиям разбиения на части для различных типов документов, проведенные в 2024 году.
Связанная литература
- Практические заметки: Ограничения векторного поиска в RAG — косинусное сходство и связанные аспекты — Подробное руководство по использованию практических заметок: Ограничения векторного поиска в RAG — косинусное сходство и связанные аспекты, включающее шаблоны контрактов, проверок и готового кода для команд, внедряющих эту практику.
- Переранкинг в RAG: узкий диапазон, где кросс-кодеры действительно помогают — почему кросс-кодер улучшает работу RAG только в пределах нижней границы воспроизведения данных и верхней границы K, что такое фантомные результаты, и как измерить обе границы для собственных запросов за один день.
- PageIndex: RAG без векторных баз данных, основанный на логических рассуждениях с использованием деревянного индекса — PageIndex заменяет векторные базы данных деревянным индексом, управляемым большой языковой моделью. Узнайте, как это работает, примеры кода на Python, результаты тестирования и когда его использовать.
- Отладка процесса поиска в RAG: Recall@K, BM25, гибридное объединение результатов и переурочение — небольшой эксперимент с пайплайном RAG, в котором правильный фрагмент текста оказался на четвертом месте в рейтинге; показано, как разбиение текста на фрагменты, алгоритм BM25, объединение рейтингов и переурочение влияют на показатели Recall@3 и MRR.
- Создание систем RAG, работающих в реальных производственных условиях: от ввода данных до возможности наблюдения за их работой — от разбиения текста на фрагменты и гибридных методов поиска до переурочения результатов, контроля доступа, наборов для оценки и отслеживания: элементы, отличающие демо-версию системы RAG от полноценной производственной системы.
- Чат-бот для ремонта автопарка как пример использования технологии RAG на простом языке с помощью Chroma и Ollama — как генерация с усилением за счёт поиска может обрабатывать первичную диагностику отчётов о ремонте автомобилей, отправленных через WhatsApp, с использованием Chroma, векторных представлений MiniLM и модели Llama 3.2.
- Создание команды агентов ИИ для совместных исследований с помощью LangGraph, DeepEval и GitLab CI — как агенты-планировщики, исследователи и авторы обмениваются информацией в LangGraph, как DeepEval оценивает степень достоверности, и как Langfuse и GitLab CI обеспечивают честность всей работы.