Почему стандартные метрики не работают

Для классических NLP-задач есть проверенные метрики: BLEU и ROUGE считают n-gram overlap между ответом и эталоном, accuracy — долю правильных ответов. В RAG всё сложнее: один и тот же факт можно выразить десятком разных формулировок, и ROUGE посчитает правильный ответ плохим — просто потому что использовали синонимы.

Но главная проблема глубже. RAG-система может «ломаться» в трёх независимых местах, и одна метрика не покажет, где именно:

1
Retrieval
Плохой поиск — нужные чанки не нашлись. LLM вынужден отвечать по нерелевантному контексту или признаваться в незнании.
2
Контекст
Шумный контекст — нашлось много чанков, но большинство нерелевантны. LLM «отвлекается» на лишнее и генерирует ответы хуже, чем если бы чанков было меньше.
3
Генерация
Галлюцинации — контекст хороший, но LLM добавляет факты из своих весов, не опираясь на него. Ответ звучит уверенно, но не соответствует источнику.

RAGAS (RAG Assessment) — фреймворк, который вводит четыре ортогональные метрики. Каждая измеряет одну «точку отказа» и вместе они дают полную картину состояния системы.

Архитектура RAGAS: четыре метрики

Каждая метрика RAGAS принимает на вход одну или несколько из четырёх сущностей, которые производит RAG-система, и возвращает число от 0 до 1.

100%
колёсико — масштаб  ·  зажать и тянуть — перемещение
ВХОДНЫЕ ДАННЫЕ RAG СИСТЕМЫ Q Question вопрос пользователя A Answer ответ, сгенерированный LLM C Contexts retrieved chunks [c₁, c₂, …, cₙ] GT Ground Truth эталонный ответ (нужен для 2 из 4 метрик) опционально RAGAS ОЦЕНКА Faithfulness Q A C 0.85 Answer Relevancy Q A 0.79 Context Recall Q C GT 0.91 Context Precision Q C GT 0.73 GT = Ground Truth — нужен только Context Recall и Context Precision

Вот что измеряет каждая метрика:

Faithfulness
Фактическая точность
Все ли факты в ответе подтверждены контекстом? Измеряет галлюцинации.
Входные данные: QAC
0.85
Answer Relevancy
Релевантность ответа
Отвечает ли ответ именно на заданный вопрос? GT не нужен.
Входные данные: QA
0.79
Context Recall
Полнота контекста
Нашёл ли retriever все нужные куски? Сравнивает с эталонным ответом.
Входные данные: QCGT
0.91
Context Precision
Точность контекста
Насколько чист контекст — нет ли лишнего шума среди чанков?
Входные данные: QCGT
0.73
Reference-free vs reference-based. Faithfulness и Answer Relevancy не требуют ground truth — это важно на старте, когда у вас нет размеченных данных. Context Recall и Context Precision требуют GT, зато дают точный сигнал о качестве retrieval.
bash
pip install ragas datasets langchain-openai

Faithfulness: нет ли галлюцинаций?

Представьте: пользователь спрашивает «Когда вышел GPT-4?», retriever находит правильный документ, но LLM отвечает «GPT-4 вышел в октябре 2022». Это галлюцинация — ответ звучит уверенно, но противоречит источнику.

Faithfulness проверяет: каждый факт в ответе — только ли то, что есть в retrieved контексте? Метрика не проверяет правильность самого контекста — только соответствие ответа ему.

Как работает

1
Извлечение клеймов
LLM разбивает ответ на атомарные утверждения.
Ответ: «RAG вышел в 2020 году, его придумала Meta. Метод улучшает точность на 30%.»
Клеймы: «RAG появился в 2020» · «Meta создала RAG» · «точность улучшается на 30%»
2
Верификация
Каждый клейм проверяется по контексту через NLI-промпт.
«RAG появился в 2020»найдено в ctx[0]: «Lewis et al., 2020»
«Meta создала RAG»найдено в ctx[0]: «команда Facebook AI»
«точность улучшается на 30%»в контексте такого числа нет
3
Результат
faithfulness = верифицированных / всего = 2 / 3 = 0.67
Один клейм не подтверждён контекстом → есть галлюцинация
faithfulness =
|верифицированных клеймов| |всех клеймов в ответе|
∈ [0, 1]

Реализация

python — кастомная реализация
import json
from openai import OpenAI

client = OpenAI()

CLAIMS_PROMPT = """Извлеки все фактические утверждения из ответа.
Каждое утверждение — отдельный атомарный факт, без интерпретации.

Ответ: {answer}

Верни JSON-массив: ["утверждение 1", "утверждение 2", ...]"""

VERIFY_PROMPT = """Можно ли вывести следующее утверждение из контекста?
Отвечай строго по контексту, не используй внешние знания.

Контекст:
{context}

Утверждение: {claim}

Верни JSON: {{"verdict": "yes" | "no", "reason": "кратко"}}"""


def extract_claims(answer: str) -> list[str]:
    resp = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": CLAIMS_PROMPT.format(answer=answer)}],
        response_format={"type": "json_object"},
    )
    data = json.loads(resp.choices[0].message.content)
    # LLM может вернуть {"claims": [...]} или просто [...]
    return data if isinstance(data, list) else data.get("claims", [])


def verify_claim(claim: str, context: str) -> bool:
    resp = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": VERIFY_PROMPT.format(
            context=context, claim=claim
        )}],
        response_format={"type": "json_object"},
    )
    result = json.loads(resp.choices[0].message.content)
    return result.get("verdict") == "yes"


def faithfulness(answer: str, contexts: list[str]) -> float:
    context_text = "\n\n---\n\n".join(contexts)
    claims = extract_claims(answer)
    if not claims:
        return 1.0  # Нет клеймов — нечего проверять
    verified = sum(1 for c in claims if verify_claim(c, context_text))
    return verified / len(claims)


# Использование
score = faithfulness(
    answer="RAG появился в 2020 году. Метод улучшает точность на 30%.",
    contexts=["Lewis et al. (2020) из Facebook AI..."]
)
print(f"Faithfulness: {score:.2f}")  # 0.50
python — через библиотеку RAGAS
from ragas.metrics import faithfulness
from ragas import evaluate
from datasets import Dataset

data = Dataset.from_dict({
    "question":     ["Когда вышел RAG?"],
    "answer":       ["RAG появился в 2020 году. Метод улучшает точность на 30%."],
    "contexts":     [["Lewis et al. (2020) из Facebook AI..."]],
    "ground_truth": ["RAG появился в 2020 году в работе Lewis et al."],
})

result = evaluate(data, metrics=[faithfulness])
print(result)  # {'faithfulness': 0.50}
Faithfulness ≠ правильность ответа. Метрика не проверяет, верен ли сам контекст. Если в вашей базе лежит устаревший документ с ошибкой, и LLM точно воспроизведёт эту ошибку — faithfulness будет 1.0. Для проверки правильности нужен Answer Correctness (с GT).

Answer Relevancy: отвечает ли на вопрос?

Ответ может быть фактически точным, но уйти не туда. На вопрос «Как установить Docker?» LLM может дать длинное объяснение что такое Docker — технически верное, но нерелевантное. Answer Relevancy измеряет именно это: насколько ответ по существу.

Как работает: обратная генерация вопросов

Прямое сравнение вопроса и ответа не работает: они написаны в разных стилях. RAGAS использует хитрость: просит LLM придумать вопросы, на которые мог бы отвечать данный ответ, и затем измеряет их сходство с оригинальным вопросом.

1
Обратная генерация
LLM получает ответ и придумывает N вопросов к нему.
Ответ: «Docker — это платформа контейнеризации. Контейнер изолирует приложение вместе с зависимостями.»
Сгенерированные вопросы:
q1: «Что такое Docker?» · q2: «Зачем нужны контейнеры?» · q3: «Как Docker изолирует приложения?»
2
Embedding similarity
Каждый сгенерированный вопрос сравнивается с оригинальным через cosine similarity.
Оригинал: «Как установить Docker?»
cos(orig, q1) = 0.72 · cos(orig, q2) = 0.61 · cos(orig, q3) = 0.68
3
Результат
answer_relevancy = mean(0.72, 0.61, 0.68) = 0.67
Ответ уходит в объяснение «что такое», а не «как установить» → низкая релевантность
answer_relevancy =
1/N · Σ cos(embed(qᵢ), embed(question)) где qᵢ — вопросы, сгенерированные из ответа
∈ [0, 1]
python — кастомная реализация
import numpy as np
from sentence_transformers import SentenceTransformer

embed_model = SentenceTransformer("intfloat/multilingual-e5-large")

REVERSE_Q_PROMPT = """По этому ответу придумай {n} разных вопросов, на которые он отвечает.
Вопросы должны быть разнообразными, но все — релевантными ответу.

Ответ: {answer}

Верни JSON-массив строк: ["вопрос 1", "вопрос 2", ...]"""


def cosine(a: np.ndarray, b: np.ndarray) -> float:
    return float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b) + 1e-9))


def answer_relevancy(question: str, answer: str, n: int = 3) -> float:
    # 1. Генерируем обратные вопросы
    resp = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": REVERSE_Q_PROMPT.format(
            answer=answer, n=n
        )}],
        response_format={"type": "json_object"},
    )
    data = json.loads(resp.choices[0].message.content)
    gen_questions = data if isinstance(data, list) else list(data.values())[0]

    # 2. Embeddings
    q_emb = embed_model.encode(question, normalize_embeddings=True)
    gen_embs = embed_model.encode(gen_questions, normalize_embeddings=True)

    # 3. Среднее cosine similarity
    sims = [cosine(q_emb, g) for g in gen_embs]
    return float(np.mean(sims))
Зачем несколько обратных вопросов? Одного вопроса недостаточно — LLM может случайно сгенерировать близкий вариант оригинала. Усреднение по N=3..5 вопросам снижает дисперсию и даёт более стабильную оценку. RAGAS по умолчанию использует N=3.

Context Recall: нашёл ли retriever нужное?

Если retriever не нашёл важный кусок — LLM не сможет использовать его в ответе, независимо от качества промпта. Context Recall измеряет, насколько полно retrieved контекст покрывает информацию из эталонного ответа.

Как работает: attribution по предложениям

Эталонный ответ (ground truth) разбивается на предложения. Для каждого предложения LLM определяет: можно ли его вывести из retrieved контекста? Recall — доля предложений, которые «покрыты» контекстом.

1
Разбивка GT
Эталонный ответ разделяется на предложения:
s1: «RAG улучшает качество ответов LLM.»
s2: «Метод использует vector search для поиска релевантных документов.»
s3: «Авторы: Lewis et al. из Facebook AI, 2020.»
2
Attribution
LLM проверяет каждое предложение по retrieved чанкам:
s1подтверждено в ctx[0]: «повышает точность генерации»
s2подтверждено в ctx[1]: «retrieval с помощью ANN»
s3в контексте нет упоминания авторов
3
Результат
context_recall = атрибутированных / всего = 2 / 3 = 0.67
Retriever не нашёл чанк с информацией об авторах → recall снизился
context_recall =
|предложений GT, покрытых контекстом| |всех предложений GT|
∈ [0, 1]
python — кастомная реализация
ATTRIBUTION_PROMPT = """Для каждого предложения из эталонного ответа определи,
можно ли его вывести из контекста. Используй только контекст.

Контекст:
{context}

Предложения эталонного ответа (JSON-массив):
{sentences}

Верни JSON-массив объектов:
[{{"sentence": "...", "attributed": true | false}}, ...]"""


def split_sentences(text: str) -> list[str]:
    """Простая разбивка по '. ' — для продакшена используй spacy/nltk."""
    return [s.strip() for s in text.replace(".\n", ". ").split(". ") if s.strip()]


def context_recall(contexts: list[str], ground_truth: str) -> float:
    sentences = split_sentences(ground_truth)
    if not sentences:
        return 1.0

    context_text = "\n\n---\n\n".join(contexts)
    resp = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": ATTRIBUTION_PROMPT.format(
            context=context_text,
            sentences=json.dumps(sentences, ensure_ascii=False),
        )}],
        response_format={"type": "json_object"},
    )
    data = json.loads(resp.choices[0].message.content)
    results = data if isinstance(data, list) else list(data.values())[0]

    attributed = sum(1 for r in results if r.get("attributed", False))
    return attributed / len(results)
Context Recall требует quality ground truth. Если в GT написано слишком коротко («RAG — поиск + генерация»), метрика будет завышена. Чем детальнее ground truth, тем точнее сигнал о качестве retrieval. Хорошее правило: GT должен содержать 3–5 содержательных предложений.

Context Precision: нет ли лишнего шума?

Retriever нашёл 5 чанков, но 3 из них — мусор. LLM теперь читает их все и «отвлекается» на нерелевантное. Эксперименты показывают: один нерелевантный чанк в топ-1 ухудшает ответ сильнее, чем нерелевантный чанк в топ-5. Context Precision учитывает это через взвешенную позиционную оценку.

Как работает: Average Precision

Каждый чанк оценивается: полезен ли он для ответа на вопрос (относительно GT)? Затем считается Average Precision — метрика из information retrieval, которая штрафует за нерелевантные чанки в топе.

1
Разметка чанков
Для каждого retrieved чанка LLM определяет его полезность:
c1 (rank=1): ✓ полезенсодержит ключевой факт
c2 (rank=2): ✗ шумнерелевантно вопросу
c3 (rank=3): ✓ полезендополнительный факт
c4 (rank=4): ✗ шум
2
Average Precision
Precision@k считается только для позиций релевантных чанков:
P@1 = 1/1 = 1.00 (c1 полезен)
P@2 = 1/2 = 0.50 (c2 пропускаем)
P@3 = 2/3 = 0.67 (c3 полезен)
3
Результат
context_precision = (P@1 + P@3) / 2 = (1.00 + 0.67) / 2 = 0.83
Шумный c2 на второй позиции штрафует precision. Если бы c2 и c3 поменялись местами → 0.92
context_precision =
Σₖ (P@k · relevantₖ) |relevant chunks|
∈ [0, 1]
python — кастомная реализация
RELEVANCE_PROMPT = """Полезен ли этот фрагмент для ответа на вопрос,
принимая во внимание правильный ответ?

Вопрос: {question}
Правильный ответ: {ground_truth}
Фрагмент: {chunk}

Верни JSON: {{"useful": true | false, "reason": "кратко"}}"""


def is_chunk_useful(question: str, chunk: str, ground_truth: str) -> bool:
    resp = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": RELEVANCE_PROMPT.format(
            question=question, ground_truth=ground_truth, chunk=chunk
        )}],
        response_format={"type": "json_object"},
    )
    result = json.loads(resp.choices[0].message.content)
    return result.get("useful", False)


def context_precision(question: str, contexts: list[str], ground_truth: str) -> float:
    # Оцениваем каждый чанк
    useful = [is_chunk_useful(question, ctx, ground_truth) for ctx in contexts]

    if not any(useful):
        return 0.0

    # Average precision: считаем P@k только на позициях релевантных чанков
    relevant_count = 0
    precision_sum = 0.0
    for k, is_rel in enumerate(useful, start=1):
        if is_rel:
            relevant_count += 1
            precision_sum += relevant_count / k  # precision@k

    return precision_sum / relevant_count
Почему позиция важна. LLM читает чанки в порядке их подачи. Нерелевантный чанк на первом месте нанесёт больше вреда, чем в конце — он задаёт «тон» для генерации. Context Precision штрафует за шум в верхних позициях сильнее, чем за шум внизу.

Тестовый датасет: структура и генерация

RAGAS принимает на вход датасет в виде словаря с четырьмя полями. Два поля обязательны для всех метрик, два — только для context recall и precision.

Структура датасета

python — ручное создание датасета
from datasets import Dataset

data = {
    # Обязательно для всех метрик
    "question": [
        "Что такое RAG?",
        "Как работает vector search?",
        "Чем отличается dense от sparse retrieval?",
    ],
    "answer": [
        "RAG — это подход, который совмещает поиск по базе знаний и генерацию LLM.",
        "Vector search находит похожие документы через cosine similarity эмбеддингов.",
        "Dense retrieval использует нейронные эмбеддинги, sparse — TF-IDF и BM25.",
    ],
    # Обязательно для всех метрик
    "contexts": [
        [
            "Retrieval-Augmented Generation (RAG) — архитектура, предложенная Lewis et al.",
            "RAG повышает точность LLM за счёт поиска релевантных документов.",
        ],
        [
            "ANN (Approximate Nearest Neighbor) позволяет искать похожие векторы за O(log n).",
            "FAISS, Qdrant, Chroma реализуют vector search для эмбеддингов.",
        ],
        [
            "BM25 — статистический метод, не требующий обучения.",
            "Dense retrieval требует предобученной модели эмбеддингов.",
        ],
    ],
    # Нужен только для context_recall и context_precision
    "ground_truth": [
        "RAG — метод, сочетающий поиск документов и генерацию ответа языковой моделью.",
        "Vector search использует ANN для поиска ближайших векторов по cosine similarity.",
        "Dense retrieval — нейронные эмбеддинги; sparse — TF-IDF / BM25 без обучения.",
    ],
}

dataset = Dataset.from_dict(data)

Автоматическая генерация тестов

Собирать GT вручную для 100+ вопросов затратно. RAGAS предоставляет TestsetGenerator, который читает ваши документы и генерирует вопросно-ответные пары трёх типов сложности.

python — RAGAS TestsetGenerator
from ragas.testset.generator import TestsetGenerator
from ragas.testset.evolutions import simple, reasoning, multi_context
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_community.document_loaders import DirectoryLoader

# Загружаем документы
loader = DirectoryLoader("./docs/", glob="**/*.md")
documents = loader.load()

# Создаём генератор
generator = TestsetGenerator.with_openai(
    generator_llm=ChatOpenAI(model="gpt-4o-mini"),  # генерирует вопросы
    critic_llm=ChatOpenAI(model="gpt-4o"),           # оценивает качество
    embeddings=OpenAIEmbeddings(),
)

# Генерируем тестовый датасет
testset = generator.generate_with_langchain_docs(
    documents,
    test_size=100,
    distributions={
        simple: 0.5,         # Простые фактологические вопросы
        reasoning: 0.25,     # Требующие рассуждений
        multi_context: 0.25, # Ответ в нескольких чанках одновременно
    },
    with_debugging_logs=True,
)

df = testset.to_pandas()
print(df[["question", "ground_truth", "evolution_type"]].head())
#                     question         ground_truth  evolution_type
# 0  Что такое chunking?  Chunking — разбивка...       simple
# 1  Почему важен overlap?  Перекрытие позволяет...   reasoning
Практика: начните с 20–30 вопросов. Запустите генератор на небольшом наборе документов (10–15 страниц), просмотрите результат вручную, отфильтруйте плохие вопросы. После валидации можно масштабировать до 100–500 вопросов.

Полный pipeline оценки

Все четыре метрики запускаются одним вызовом evaluate(). Под капотом RAGAS делает параллельные LLM-вызовы — каждый сэмпл датасета обрабатывается независимо.

python — запуск оценки
from ragas import evaluate
from ragas.metrics import (
    faithfulness,
    answer_relevancy,
    context_recall,
    context_precision,
)
from ragas.run_config import RunConfig
from datasets import Dataset

# Датасет с ответами вашей RAG-системы
dataset = Dataset.from_dict({
    "question":     [...],
    "answer":       [...],   # ответы вашего RAG
    "contexts":     [...],   # чанки, которые retriever вернул для каждого вопроса
    "ground_truth": [...],
})

# RunConfig управляет параллелизмом и retry-политикой
run_config = RunConfig(
    max_workers=16,     # параллельных LLM-вызовов
    max_wait=120,       # таймаут в секундах на один запрос
    max_retries=3,      # повторных попыток при rate limit
)

result = evaluate(
    dataset=dataset,
    metrics=[faithfulness, answer_relevancy, context_recall, context_precision],
    run_config=run_config,
    raise_exceptions=False,  # продолжать при ошибках отдельных сэмплов
)

print(result)
# {'faithfulness': 0.85, 'answer_relevancy': 0.79,
#  'context_recall': 0.91, 'context_precision': 0.73}

# Детальный анализ по каждому вопросу
df = result.to_pandas()
print(df[["question", "faithfulness", "answer_relevancy"]].sort_values("faithfulness"))

Интеграция с вашей RAG-системой

python — сбор данных для оценки
from dataclasses import dataclass
from typing import Any
import asyncio


@dataclass
class RAGSample:
    question: str
    answer: str
    contexts: list[str]
    ground_truth: str | None = None


async def run_rag(question: str, rag_pipeline) -> tuple[str, list[str]]:
    """Запускает RAG и возвращает (ответ, список контекстов)."""
    contexts = await rag_pipeline.retrieve(question)
    answer = await rag_pipeline.generate(question, contexts)
    return answer, [c.page_content for c in contexts]


async def collect_eval_data(
    questions: list[str],
    ground_truths: list[str],
    rag_pipeline,
) -> list[RAGSample]:
    tasks = [run_rag(q, rag_pipeline) for q in questions]
    results = await asyncio.gather(*tasks)

    return [
        RAGSample(
            question=q,
            answer=answer,
            contexts=contexts,
            ground_truth=gt,
        )
        for q, gt, (answer, contexts) in zip(questions, ground_truths, results)
    ]


def build_dataset(samples: list[RAGSample]) -> Dataset:
    return Dataset.from_dict({
        "question":     [s.question for s in samples],
        "answer":       [s.answer for s in samples],
        "contexts":     [s.contexts for s in samples],
        "ground_truth": [s.ground_truth or "" for s in samples],
    })


# Запуск
async def evaluate_rag(rag_pipeline, test_questions, test_answers):
    samples = await collect_eval_data(test_questions, test_answers, rag_pipeline)
    dataset = build_dataset(samples)
    result = evaluate(dataset, metrics=[faithfulness, answer_relevancy,
                                         context_recall, context_precision])
    return result

Интерпретация результатов

Абсолютные значения RAGAS не имеют смысла без контекста — важен тренд и диагноз. Тем не менее, вот практические пороги для ориентира:

Faithfulness
0.85
хорошо ✓
Answer Relevancy
0.79
средне △
Context Recall
0.91
отлично ✓✓
Context Precision
0.73
средне △
Метрика < 0.5 — плохо 0.5–0.7 — средне 0.7–0.9 — хорошо > 0.9 — отлично
Faithfulness Много галлюцинаций Частичные отклонения от контекста Редкие галлюцинации Практически без галлюцинаций
Answer Relevancy Ответ не по теме Частично уходит в сторону Обычно по существу Точно отвечает на вопрос
Context Recall Retriever пропускает ключевое Часть информации теряется Хорошее покрытие Находит всё нужное
Context Precision Контекст полон шума Заметный шум в топе Преимущественно релевантен Только нужные чанки

Что делать с результатами

Context Recall < 0.7
Retriever не находит нужные чанки. Попробуй: гибридный поиск (dense + BM25), увеличить top-k, улучшить chunking, query transformation.
Context Precision < 0.7
Среди чанков много нерелевантных. Попробуй: reranking (cross-encoder), metadata filtering, уменьшить top-k, contextual compression.
Faithfulness < 0.7
LLM галлюцинирует поверх контекста. Попробуй: усилить system prompt («отвечай только по контексту»), снизить temperature, добавить self-check.
Answer Relevancy < 0.7
Ответы уходят не в ту сторону. Попробуй: query rewriting, улучшить system prompt (добавь инструкцию «отвечай кратко и по существу»), query decomposition для сложных вопросов.
Порядок оптимизации. Начинайте с Context Recall и Context Precision — они дают сигнал о retrieval, который является фундаментом всего пайплайна. Если retriever не находит нужное, улучшение промпта или генерации не поможет. Только после того как контекст хорош, работайте над Faithfulness и Answer Relevancy.

Шпаргалка

МетрикаВходные данныеВопросМетодНужен GT?
Faithfulness Q, A, C Нет ли галлюцинаций? Клеймы → NLI-верификация Нет
Answer Relevancy Q, A Ответ по существу? Обратные вопросы → cosine sim Нет
Context Recall Q, C, GT Нашёл ли retriever всё? Attribution предложений GT Да
Context Precision Q, C, GT Чист ли контекст? Average Precision@k Да

Быстрый старт (без GT):

python
# Оценка без ground truth — для первого запуска
from ragas import evaluate
from ragas.metrics import faithfulness, answer_relevancy
from datasets import Dataset

result = evaluate(
    Dataset.from_dict({
        "question": questions,
        "answer":   answers,
        "contexts": contexts,
    }),
    metrics=[faithfulness, answer_relevancy],
)
print(result)  # {'faithfulness': 0.85, 'answer_relevancy': 0.79}

Ключевые принципы:

  • Faithfulness и Answer Relevancy — reference-free, работают без GT
  • Context Recall и Precision — требуют GT, но дают точный сигнал о retrieval
  • Начинайте с 30–50 вопросов, масштабируйте после валидации
  • RAGAS использует LLM как судью — качество оценки зависит от качества evaluation LLM
  • Смотрите на тренд (до/после изменений), а не на абсолютные числа
  • Используйте result.to_pandas() для анализа проблемных вопросов поштучно

Практические задания

  1. Базовая оценка. Возьмите RAG-систему из предыдущих уроков, запустите 20 вопросов и оцените faithfulness и answer relevancy. Найдите 3 вопроса с наихудшим faithfulness — что общего в ответах? Это системная проблема или единичные случаи?
  2. Сравнение retrieval-стратегий. Запустите оценку для двух конфигураций: (a) simple top-5 retrieval, (b) hybrid search + reranking top-5. Сравните context_recall и context_precision. Какая стратегия лучше по каждой метрике?
  3. Генерация тестового датасета. Используйте TestsetGenerator на 10–15 страницах вашей документации. Сгенерируйте 30 вопросов (50% simple, 25% reasoning, 25% multi_context). Просмотрите вручную — какие вопросы оказались плохого качества и почему?