← Все статьи
Новости12 мин чтения

llamaindex: RAG по своим документам с проверкой качества ответов

Как проверить RAG на llamaindex до расширения корпуса: разделить ошибки поиска и ответа, собрать контрольные вопросы и свести всё в таблицу «вопрос-фрагмент-ответ-оценка».

Обложка статьи: llamaindex: RAG по своим документам с проверкой качества ответов

Если система выдаёт ответ по внутренним документам, но не показывает, какой именно фрагмент она нашла, ты не можешь понять, где она ошиблась: в поиске или в формулировке. Это две разные поломки, и лечатся они по-разному. Пока путь от вопроса к фрагменту не виден, любая оценка качества сводится к «звучит убедительно», а убедительность - плохой контролёр для базы знаний, на которой команда принимает решения.

Отсюда рабочий тезис, который стоит принять до всякого кода: если ответ не опирается на нужный фрагмент, RAG проверку не прошёл, каким бы гладким этот ответ ни был. Вся проверка ниже собрана вокруг одной таблицы «вопрос - найденный фрагмент - ответ - оценка», и строится она на малом, заранее зафиксированном корпусе - не на всей базе сразу.

Подключите модели для проверки контента с оплатой в рублях на provod.ai

Почему без найденного фрагмента нельзя понять, где ошибка?

По умолчанию считается, что хорошо сформулированный ответ доказывает работу RAG. Удобное заблуждение: генеративная модель пишет связно и поверх нерелевантного контекста. Документация LlamaIndex прямо разделяет качество RAG на две независимо проверяемые оси: оценка retrieval («релевантны ли найденные источники запросу?») и оценка ответа («соответствует ли ответ найденному контексту и самому запросу?»). Система может пройти одну ось и провалить другую (S2).

Из этого разделения следует практическое: пока ты не зафиксировал, какой фрагмент был подан модели, ты смешиваешь два риска в один симптом. Ответ неверен - но это ретривер принёс не тот кусок или генератор проигнорировал правильный? Без найденного фрагмента этот вопрос неразрешим, и ты чинишь наугад.

Это разделение стоит держать в голове и когда выбираешь модельный маршрут: первая ось меряется вообще без модели, а на вторую приходятся и генератор, и LLM-судьи, о которых ниже. provod.ai (российский аналог OpenRouter) даёт единый OpenAI-совместимый API, который встаёт под коннекторы LlamaIndex сменой ключа и base_url, - но retrieval он за тебя не оценивает.

Как разложить качество RAG на две проверки?

Первую ось - «нашёл ли ретривер нужное» - в llamaindex закрывает RetrieverEvaluator. Он оценивает ретривер изолированно, до всякой генерации, стандартными IR-метриками: hit rate, MRR, precision, recall, average precision и NDCG. Механика простая: для каждого запроса сравниваются ID узлов, которые ретривер реально вернул, с заранее заданным набором ожидаемых релевантных ID (S3).

Вторую ось - «опирается ли ответ на контекст» - закрывает FaithfulnessEvaluator. Он проверяет, обоснован ли сгенерированный ответ найденными контекстами (проверка на галлюцинацию), и работает только со строкой ответа и пассажами контекста - эталонный правильный ответ ему не нужен. Его EvaluationResult возвращает явные passing (bool), score, feedback и сами contexts, так что пройдено/не пройдено по каждому ответу привязывается к конкретному тексту источника (S5).

Рядом стоит RelevancyEvaluator. Ему нужен исходный запрос вместе с ответом, эталон не требуется, и он проверяет, что и найденный контекст, и ответ релевантны и согласованы именно с этим запросом. Важная деталь: его можно запускать по отдельным исходным узлам, а не только по полному ответу, - так изолируешь, какой единственный найденный чанк поддерживает ответ, а какой нет (S1).

Эти три оценщика - сами LLM-судьи, а не детерминированные проверки. Их passing и score отражают суждение модели о заземлённости ответа. Поэтому они сужают поле для ручной сверки, но не отменяют её.

Матрица примитивов оценки LlamaIndex: какие требуют эталонного ответа, а какие нет

Контрольный корпус и вопросы с известным фрагментом

Начинай с малого и разрешённого. Возьми корпус, который тебе легально можно использовать, и зафиксируй его до прогона: сравнивать результаты имеет смысл только на неподвижной базе. В модели данных LlamaIndex Document - это исходная единица, которую при индексировании режут на объекты Node (чанки), а метаданные документа (имя файла, идентификатор источника) автоматически распространяются на каждый производный Node (S6). Именно это позже позволит при ручной сверке проследить найденный чанк до его исходного документа.

Дальше нужны вопросы с известным ожидаемым фрагментом. Здесь помогает generate_question_context_pairs: он синтезирует датасет EmbeddingQAFinetuneDataset прямо из фиксированного корпуса узлов - строит ID запросов, сопоставленные со сгенерированными вопросами, и указывает конкретный релевантный чанк (или чанки), который каждый вопрос должен вернуть (S3). Получается переиспользуемая связка «вопрос -> ожидаемый фрагмент» над ограниченным разрешённым корпусом.

У этого удобства есть честная граница. generate_question_context_pairs и RetrieverEvaluator работают с ID узлов из того же проиндексированного корпуса, на котором построен датасет. То есть измеряется, способен ли тот же ретривер заново найти чанки, которые ему объявили релевантными, а не реальная релевантность в оценке независимого человека. Синтетический датасет - это заготовка, но критичные вопросы всё равно пишет человек, знающий предметную область.

from llama\_index.core.evaluation import ( RetrieverEvaluator, generate\_question\_context\_pairs, )

qa\_dataset = generate\_question\_context\_pairs( nodes, llm=llm, num\_questions\_per\_chunk=2 )

retriever\_evaluator = RetrieverEvaluator.from\_metric\_names( ["hit\_rate", "mrr"], retriever=retriever ) result = retriever\_evaluator.evaluate( query="Каков срок ответа на претензию по договору?", expected\_ids=["node\_id\_17"], )

Как связать вопрос, фрагмент и ответ в одной строке?

Чтобы каждый ответ разбирался механически, а не на глаз, включи цитирование. CitationQueryEngine подвешивает к ответу нумерованные инлайн-цитаты вида [1], [2], и каждый номер индексирует непосредственно в массив source_nodes объекта ответа. Любое утверждение в ответе прослеживается назад до точного найденного фрагмента, который его породил. Гранулярность цитируемых чанков настраивается через citation_chunk_size (по умолчанию 512 символов) (S4).

Это и есть техническая основа таблицы «вопрос - найденный фрагмент - ответ - оценка». Цитата даёт тебе фрагмент, source_nodes вместе с проброшенными метаданными - его источник, а два оценщика - предварительный вердикт по каждой оси. Дальше человек ставит финальную отметку.

from llama\_index.core.query\_engine import CitationQueryEngine from llama\_index.core.evaluation import ( FaithfulnessEvaluator, RelevancyEvaluator, )

query\_engine = CitationQueryEngine.from\_args(index, citation\_chunk\_size=512) resp = query\_engine.query("Каков срок ответа на претензию по договору?")

faith = FaithfulnessEvaluator() rel = RelevancyEvaluator()

print(faith.evaluate\_response(response=resp).passing)   # заземлён ли ответ print(rel.evaluate\_response(query="...", response=resp).passing)  # релевантен ли for node in resp.source\_nodes:                           # [1], [2] -> точный фрагмент print(node.metadata.get("file\_name"), node.node\_id)

Для набора заранее заданных критичных вопросов, у которых есть известный правильный ответ, подключается CorrectnessEvaluator - единственный оценщик в наборе LlamaIndex, которому нужен эталонный ответ рядом с запросом и ответом. Он выдаёт оценку от 1 до 5 с фидбеком, объясняющим фактическое расхождение с эталоном, чем и отличается от Faithfulness и Relevancy, которым эталон не нужен (S7). Именно этот механизм подходит под малый список критичных вопросов с проверенными ответами.

Обрати внимание на счёт: каждая строка таблицы оплачивается дважды - генерацией и судейством. На прогоне из сотни вопросов это уже заметный расход, и судью полезно держать на другой модели, чем генератор, чтобы он не подтверждал собственные формулировки. Здесь помогает провод (provod.ai): разные модели живут под одним OpenAI-совместимым API и одним рублёвым балансом, по ценам провайдеров без наценки сверху. Развести генератор и судью получается прямо в этом коде - без второй интеграции и второго счёта.

from llama\_index.llms.openai\_like import OpenAILike

llm = OpenAILike( model="anthropic/claude-...", api\_base="https://api.provod.ai/v1", api\_key="ПРОВОД\_КЛЮЧ", is\_chat\_model=True, )
Диагностический маршрут от вопроса через инлайн-цитату к source_nodes и метаданным документа

Что вскрывает таблица оценки

Одна таблица - и есть весь измеряемый результат проверки. Каждая строка держит вопрос, найденный фрагмент (текст плюс источник из метаданных), сгенерированный ответ и итоговую оценку по двум осям. Оценщики LlamaIndex дают предварительные passing, а последний столбец - результат ручной сверки, потому что LLM-судья судит о заземлённости, но не доказывает её.

Ниже - учебная иллюстрация того, как таблица вскрывает разные поломки. Числа и тексты здесь показывают форму записи, а не измерение на реальном корпусе.

Вопрос (пользовательский ввод)Найденный фрагмент (источник)ОтветОценка
Каков срок ответа на претензию?п.7.2, dogovor_2025.pdf«10 рабочих дней»retrieval ok, ответ подтверждён
Какой лимит возврата по акции?akcia.pdf, но про доставку«Возврат 14 дней»фрагмент не тот, ответ звучит гладко - retrieval провал
срок гарантии на партиюgarant.pdf, п.3«12 месяцев» вместо «24»фрагмент верный, ответ разошёлся - response провал
Кто подписант допсоглашения?нет найденного фрагментаразвёрнутый ответ «из общих знаний»источника нет - отклонить

Вторая строка - ровно тот случай, ради которого таблица существует: красивый ответ поверх нерелевантного чанка. Без колонки «найденный фрагмент» ты бы засчитал его как норму. Третья строка - обратная поломка: ретривер принёс правильный п.3, но генератор исказил число, и это ловит FaithfulnessEvaluator по несоответствию контексту. Четвёртая - жёсткий стоп: нет найденного фрагмента, значит для базы знаний ответа нет, каким бы уверенным он ни выглядел.

Именно так оценка разделяет ошибки retrieval и ответа вместо того, чтобы валить их в одно расплывчатое «плохо отвечает». Карта точек качества получается предметной: видно, какие вопросы проваливают поиск, а какие - генерацию.

Контрольная таблица вопрос-фрагмент-ответ-оценка с подсветкой строки скрытого провала retrieval

Когда можно расширять корпус, а когда нельзя?

Моя позиция здесь жёсткая: корпус не растёт, пока набор критичных вопросов не пройден. Документация LlamaIndex такого не требует - это выбор владельца базы знаний, которому нужен источник под каждым ответом. Цена выбора честная - оценка требует ручной работы, зато риск ошибочных ответов становится видимым до того, как база вырастет и ошибки станет дороже искать.

Решение принимается по трём критериям отказа. Первый: для вопроса нет найденного фрагмента - ответ отклоняется независимо от формулировки. Второй: ответ не подтверждается фрагментом - FaithfulnessEvaluator показывает passing=false, и это тоже стоп. Третий: критичный вопрос не проходит ручную сверку, даже если оба LLM-судьи поставили «пройдено». Пока хоть один критичный вопрос спотыкается о любой из трёх, корпус не расширяется.

СитуацияСигналРешение
Фрагмент не найденпустой source_nodesотклонить ответ, не расширять
Ответ не заземлёнFaithfulness.passing=falseчинить генерацию/промпт, не расширять
Ретривер мимонизкий hit rate / MRRчинить индекс и чанкинг, не расширять
Все критичные вопросы прошлиручная сверка + оценщики согласнырасширять корпус контролируемо

Два других пути я отклонил. Расширять RAG по впечатлению от ответов - ловушка, где гладкость подменяет проверяемость. Проверять только критичные вопросы на малом корпусе - разумный минимум, и с него стоит начинать, но полноценная карта появляется, когда к критичным вопросам добавлена сверка обеих осей.

Маршрут принятия решения: четыре шлюза отказа перед контролируемым расширением RAG

Чего это не решает

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

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

LLM-судьи не дают гарантий. passing от Faithfulness или Relevancy - это модельное суждение о заземлённости, а не проверка на истинность; на нём нельзя останавливать сверку критичного вопроса. И ещё техническая осторожность: сайт документации LlamaIndex переехал с docs.llamaindex.ai на developers.llamaindex.ai, часть старых ссылок отдаёт 301, а сигнатуры конструкторов и пороги по умолчанию могут меняться между релизами - фиксируй версию пакета вместе с корпусом.

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

FAQ

Можно ли обойтись без эталонных ответов?

Для двух осей - да. FaithfulnessEvaluator и RelevancyEvaluator работают без эталона: первому хватает ответа и контекстов, второму - запроса и ответа (S1, S5). Эталон обязателен только для CorrectnessEvaluator с его оценкой 1-5, и его держат для малого набора критичных вопросов с известным правильным ответом (S7).

Как понять, ретривер виноват или генератор?

Разнеси оси. RetrieverEvaluator меряет поиск изолированно метриками hit rate, MRR, precision, recall, average precision и NDCG до генерации (S3). Если поиск попал, а ответ всё равно неверен - смотри Faithfulness на заземлённость ответа в контексте.

Зачем нужны инлайн-цитаты, если есть оценщики?

Цитаты дают механическую прослеживаемость. В CitationQueryEngine номер [1] индексирует прямо в source_nodes, а размер цитируемого чанка задаётся citation_chunk_size (по умолчанию 512 символов), так что каждое утверждение привязано к конкретному фрагменту (S4). Оценщики выносят вердикт, цитаты показывают, по какому именно тексту.

С чего начать, если корпус большой?

С малого разрешённого среза и зафиксированного набора критичных вопросов. Расширяй только после того, как эти вопросы проходят все три критерия отказа. Так ошибки видны дешевле - до роста базы, а не после.

provod.ai как модельный маршрут для генерации и судейства в проверяемом RAG на LlamaIndex

provod.ai — обновляйте AI-стек, не переписывая продукт

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

В одном каталоге — актуальные модели для текста и медиа: GPT от OpenAI, Claude от Anthropic, Gemini от Google, Grok от xAI, DeepSeek, Qwen, GLM, Kimi и MiniMax; для изображений — Nano Banana 2 Pro и GPT Image; для видео — последние версии Seedance, Kling, Veo и Google Omni. Также доступны модели для reasoning, поиска, документов, эмбеддингов, музыки и аудио.

За новизну нет дополнительной маржи платформы: официальный тариф новой модели применяется 1:1, без наценки provod.ai.

Подключайте новые модели быстрее: форма регистрации · цены на модели · защита данных по 152-ФЗ · API и интеграции

Источники

  • LlamaIndex, Evaluating (обзор двух осей), developers.llamaindex.ai, доступ 2026-07-18 - F1.
  • LlamaIndex, Evaluation Usage Pattern (RelevancyEvaluator), developers.llamaindex.ai, 2026-07-18 - F3.
  • LlamaIndex, Retriever Eval (RetrieverEvaluator, generate_question_context_pairs), developers.llamaindex.ai, 2026-07-18 - F5, F6.
  • LlamaIndex, Citation Query Engine, developers.llamaindex.ai, 2026-07-18 - F7.
  • LlamaIndex, FaithfulnessEvaluator (API reference), developers.llamaindex.ai, 2026-07-18 - F2.
  • LlamaIndex, Documents and Nodes, developers.llamaindex.ai, 2026-07-18 - F8.
  • LlamaIndex, Correctness Eval, developers.llamaindex.ai, 2026-07-18 - F4.