DEV Community

Cover image for llamaindex: RAG по своим документам с проверкой качества ответов
Promptra Team for Promptra

Posted on

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

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

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

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

По умолчанию считается, что хорошо сформулированный ответ доказывает работу 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"],
)
Enter fullscreen mode Exit fullscreen mode

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

Чтобы каждый ответ разбирался механически, а не на глаз, включи цитирование. 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)
Enter fullscreen mode Exit fullscreen mode

Для набора заранее заданных критичных вопросов, у которых есть известный правильный ответ, подключается 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,
)
Enter fullscreen mode Exit fullscreen mode

Диагностический маршрут от вопроса через инлайн-цитату к 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 не измеряет и ручную сверку источника не отменяет.


provod.ai — централизуйте расходы на модели

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

В одном каталоге — актуальные модели для текста и медиа: 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.

Соберите AI-расходы в одном месте: форма регистрации · цены на модели · защита данных по 152-ФЗ · реквизиты для договора

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, пропиши base_url в коннекторе LlamaIndex и собери первый контрольный прогон на своём малом корпусе. Один рублёвый баланс закрывает и генератор, и судью; оплатить можно картой, через СБП или по счёту с закрывающими документами. Если базу знаний ведёт не один человек, командное пространство с общими ключами снимает возню с раздачей доступов на время прогона.

Источники

  • 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.

Top comments (0)