title: "Оценка AI-агентов: RAGAS, DeepEval, LLM-as-Judge" slug: evaluation-agents-automated-qa-2026-ru date: 2026-02-24 lang: ru
Оценка AI-агентов: RAGAS, DeepEval, LLM-as-Judge
Ключевые факты
- ragas 0.4.3 (последняя версия): фреймворк для оценки RAG с контекстно-ориентированными метриками
- deepeval 3.8.8 (последняя версия): комплексная оценка LLM с пользовательскими критериями через GEval
- langsmith 0.7.9 (последняя версия): платформа оценки и трассировки LangChain
- braintrust 0.7.0 (последняя версия): платформа оценки с версионированием датасетов и онлайн-скорингом
- Основные метрики RAGAS:
faithfulness(ответ основан на контексте),answer_relevancy(ответ отвечает на вопрос),context_precision(полученные чанки ранжированы по релевантности),context_recall(вся ground truth в полученном контексте) - Классы метрик DeepEval:
GEval(LLM-as-judge с пользовательскими критериями),HallucinationMetric(обнаруживает необоснованные утверждения),AnswerRelevancyMetric(семантическое сходство с вопросом),FaithfulnessMetric(утверждения поддерживаются контекстом),ContextualRecallMetric,ContextualPrecisionMetric - Смещения LLM-as-judge: смещение позиции (предпочтение первого/последнего варианта), смещение многословия (отдавание предпочтения более длинным ответам), самопредпочтение (модель оценивает собственные выходы выше), смещение последовательности (сложность с обнаружением тонких различий качества)
- Минимальный размер golden dataset: 100 примеров для базового тестирования, 300+ для обнаружения небольших улучшений с статистической мощностью
- Бенчмарк GAIA: General AI Assistants benchmark с 3 уровнями сложности (Level 1: одношаговый поиск в web, Level 2: многошаговое рассуждение, Level 3: сложные многоинструментальные задачи); измеряет реальные возможности ассистентов включая использование инструментов и многошаговое планирование
- SWE-bench Verified: измеряет способность разрешать реальные issue GitHub из open-source проектов; содержит вручную проверенные экземпляры из SWE-bench для снижения шума
- Порог faithfulness: обычно 0.8-0.9 для production RAG; более низкий порог рискует галлюцинациями
- Порог answer relevancy: обычно 0.7-0.8; оценки ниже 0.5 указывают, что ответ не отвечает на вопрос
- Температура judge модели: 0.0-0.3 для консистентности; более высокие значения увеличивают дисперсию между запусками
- Стоимость оценки: LLM-as-judge добавляет стоимость вывода на пример; каскадная оценка (дешёвый judge → дорогой judge для граничных случаев) существенно снижает затраты
- Статистическая значимость: тест Wilcoxon signed-rank предпочтителен t-тесту для не-нормальных распределений оценок; alpha=0.05 стандартная
- Порог обнаружения регрессии: падение оценки на 5% — типичное условие для CI/CD; более строгие пороги (2-3%) для критических систем
- Смягчение смещения позиции: оценить в обоих порядках (A затем B, B затем A) и агрегировать; несогласие указывает на смещение
- Калибровка judge: проверить против 50-100 примеров с человеческой разметкой; согласие выше 0.8 (Cohen's kappa) указывает на надёжного судьи
- Многооборотная оценка: метрики на уровне траектории (качество плана, восстановление после ошибок, финальный успех) отличаются от одноёборотных метрик
- Обработка недетерминизма: запустить каждую оценку 3-5 раз с разными seeds; сообщить медиану и дисперсию
Почему оценка агентов сложна
Оценка агентов принципиально отличается от оценки отдельных LLM вызовов:
Недетерминизм: Агенты используют sampling (temperature > 0) для рассуждения, выбор инструмента варьируется в зависимости от доступности, порядок выполнения различается между запусками. Один и тот же вход производит разные траектории, что затрудняет прямое сравнение.
Многошаговые траектории: Успех зависит от цепочки решений. Правильный финальный ответ через неэффективный путь может указывать на везение, а не компетентность. Промежуточные шаги имеют значение, а не только финальный результат.
Отсутствие ground truth: Для открытых задач вроде "исследуй тренды квантовых вычислений" нет единственно правильного ответа. Оценка требует оценки качества, полноты и рассуждения, а не точного совпадения.
Стоимость оценки: Агенты делают несколько LLM вызовов за задачу. Оценка 1000 примеров по 10 шагов каждый = 10,000 LLM вызовов, плюс LLM-as-judge вызовы для скорирования. Оценка может стоить дороже, чем вывод.
Возникающие режимы отказа: Агенты могут циклиться бесконечно, галлюцинировать выходы инструментов или молчаливо отказывать, выбирая неправильный инструмент. Традиционные NLP метрики (BLEU, ROUGE) полностью пропускают эти режимы отказа.
Чувствительность к контексту: Производительность агента варьируется в зависимости от домена, доступных инструментов и сложности задачи. Coding агент может преуспевать в Python, но отказывать в Rust. Датасеты оценки должны охватывать deployment распределение.
Фреймворк принятия решений
Стратегия оценки агента зависит от того, что вы измеряете и какие ресурсы доступны:
Unit Eval vs Trajectory Eval vs Human Eval:
| Тип оценки | Что измеряет | Когда использовать | Стоимость | Пример |
|---|---|---|---|---|
| Unit Eval | Качество одного шага | Отдельные вызовы инструмента, качество retrieval, отдельные LLM ответы | Низкая | Faithfulness RAGAS для RAG ответов |
| Trajectory Eval | Многошаговое поведение | Планирование агента, восстановление от ошибок, последовательность выбора инструмента | Средняя | Success rate на задачах GAIA benchmark |
| Human Eval | Субъективное качество | Качество финального выхода, удовлетворённость пользователя, обработка граничных случаев | Высокая | Оценки предпочтения человека по шкале 1-5 |
Автоматизированные vs человеческие метки:
| Источник метки | Плюсы | Минусы | Лучше всего для |
|---|---|---|---|
| Автоматизированный (LLM-as-judge) | Масштабируемый, консистентный, быстрая итерация | Смещён, требует калибровки, пропускает нюансы | Regression testing, CI/CD gates, крупномасштабное сравнение |
| Человеческие метки | Ground truth, захватывает нюансы, обнаруживает новые отказы | Дорого, медленно, разногласие между аннотаторами | Создание golden dataset, калибровка judge, финальная валидация |
| Гибридный (LLM + human review) | Human review только граничные случаи, отмеченные LLM | Сбалансированная стоимость/качество | Production monitoring, итеративное улучшение |
Дерево решений:
Вы тестируете качество RAG?
├─ YES → Используйте RAGAS (faithfulness, context metrics)
└─ NO → Вы тестируете траектории агента?
├─ YES → Используйте DeepEval GEval с пользовательскими критериями + trajectory metrics
└─ NO → Вы сравниваете версии моделей?
├─ YES → LLM-as-judge с testing статистической значимости
└─ NO → Human eval с чёткой rubric
Таблица справочника параметров
| Параметр | Значение | Заметки |
|---|---|---|
faithfulness_threshold |
0.8-0.9 | Ниже 0.8 указывает на риск галлюцинации |
answer_relevancy_threshold |
0.7-0.8 | Ниже 0.5 означает ответ off-topic |
context_precision_threshold |
0.6-0.8 | Измеряет качество ранжирования retrieval |
min_dataset_size |
100-300 | 100 для базового тестирования, 300+ для обнаружения маленьких эффектов |
judge_model |
gpt-4o, claude-sonnet-4 |
Используйте способную модель; избегайте судить более слабыми моделями |
judge_temperature |
0.0-0.3 | 0.0 для максимальной консистентности, 0.1-0.3 если judge кажется жёсткой |
regression_threshold_pct |
0.05 | 5% падение оценки запускает alert; отрегулируйте на основе толерантности |
statistical_alpha |
0.05 | Стандартный p-value порог для значимости |
statistical_power |
0.8 | Вероятность обнаружить истинное улучшение |
num_judge_samples |
3-5 | Оценить один пример N раз для измерения дисперсии |
human_sample_size |
50-100 | Для калибровки judge против человеческих меток |
cascading_eval_threshold |
0.3-0.7 | Оценки в этом диапазоне эскалируют к дорогому judge |
max_trajectory_length |
20-50 | Обрезать бесконечные циклы; зависит от задачи |
timeout_seconds |
60-300 | По задаче timeout; предотвращает зависания |
Частые ошибки
Ошибка 1: Оценка с более слабым Judge, чем система под тестом
Симптом: Judge даёт высокие оценки явно неправильным выходам; не различает уровни качества.
Влияние: Ложная уверенность в качестве системы; регрессии остаются необнаруженными.
❌ Неправильно:
# Использование GPT-3.5 для оценки GPT-4o выходов
from deepeval.metrics import GEval
from deepeval.test_case import LLMTestCase
metric = GEval(
name="answer_quality",
criteria="Is the answer accurate and complete?",
evaluation_model="gpt-3.5-turbo", # ❌ Более слабая, чем система под тестом
)
test_case = LLMTestCase(
input="Explain quantum entanglement",
actual_output=gpt4_response, # GPT-4o выход
)
metric.measure(test_case)
# Judge может пропустить тонкие ошибки в продвинутом объяснении физики
✅ Правильно:
# Используйте judge как минимум такой же мощности, как система под тестом
metric = GEval(
name="answer_quality",
criteria="Is the answer accurate and complete?",
evaluation_model="gpt-4o", # ✅ Тот же уровень способности
evaluation_params={"temperature": 0.0}, # Консистентное скорирование
)
test_case = LLMTestCase(
input="Explain quantum entanglement",
actual_output=gpt4_response,
)
metric.measure(test_case)
# Judge может оценить точность продвинутой физики
Ошибка 2: Отсутствие смягчения смещения позиции
Симптом: При сравнении двух выходов judge постоянно предпочитает, какой появляется первым.
Влияние: Смещённые A/B тесты; невозможно надёжно определить, какая модель лучше.
❌ Неправильно:
# Одноразовое сравнение без swap позиции
def compare_models(prompt, model_a_output, model_b_output):
judge_prompt = f"""
Which response is better?
Response A: {model_a_output}
Response B: {model_b_output}
Choose: A or B
"""
judgment = judge_llm.invoke(judge_prompt)
# ❌ Смещение позиции: judge может предпочесть Response A из-за primacy effect
return judgment
✅ Правильно:
# Оценка с swap позиции для обнаружения и смягчения смещения
async def compare_models_unbiased(prompt, model_a_output, model_b_output):
# Оценить оба порядка
result_ab = await judge_compare(model_a_output, model_b_output) # A первая
result_ba = await judge_compare(model_b_output, model_a_output) # B первая
# Извлечь победителей
a_wins_ab = result_ab.winner == "first"
a_wins_ba = result_ba.winner == "second"
# Проверка согласия
if a_wins_ab and a_wins_ba:
return {"winner": "A", "confidence": "high", "bias_detected": False}
elif not a_wins_ab and not a_wins_ba:
return {"winner": "B", "confidence": "high", "bias_detected": False}
else:
# Несогласие указывает на смещение позиции
return {"winner": "tie", "confidence": "low", "bias_detected": True}
Ошибка 3: Тестирование на примерах, видевшихся во время разработки
Симптом: Высокие оценки eval во время разработки, плохая performance в production.
Влияние: Overfitting к test set; неспособность обобщаться.
❌ Неправильно:
# Использование одних примеров для разработки и оценки
dev_examples = load_examples("dev_set.jsonl") # 200 примеров
# Tune prompts при проверке оценок на dev_examples
for prompt_variant in prompt_variants:
scores = [evaluate(ex, prompt_variant) for ex in dev_examples]
print(f"{prompt_variant}: {mean(scores)}")
# ❌ Оптимизация для этих специфических примеров
# Финальная eval на одном наборе
final_score = mean([evaluate(ex, best_prompt) for ex in dev_examples])
# ❌ Overfit оценка, не репрезентативна для реальной performance
✅ Правильно:
# Правильный train/validation/test split
all_examples = load_examples("all_examples.jsonl") # 500 примеров
# Split: 60% train, 20% validation, 20% test
train_set = all_examples[:300]
val_set = all_examples[300:400]
test_set = all_examples[400:] # Отложено до финальной eval
# Разработать на train, tune на validation
for prompt_variant in prompt_variants:
train_scores = [evaluate(ex, prompt_variant) for ex in train_set]
val_scores = [evaluate(ex, prompt_variant) for ex in val_set]
print(f"{prompt_variant} - Train: {mean(train_scores)}, Val: {mean(val_scores)}")
# Выбрать лучший на основе validation
best_prompt = max(prompt_variants, key=lambda p: mean([evaluate(ex, p) for ex in val_set]))
# Финальная eval на отложенном test set (только один раз!)
test_scores = [evaluate(ex, best_prompt) for ex in test_set]
final_score = mean(test_scores)
print(f"Final test score (unbiased): {final_score}")
Ошибка 4: Игнорирование дисперсии оценок в недетерминированных системах
Симптом: A/B тест показывает Model B лучше, но повторный запуск даёт противоположный результат.
Влияние: Ненадёжные решения; ложные alarm по регрессии; потраченное время на отладку.
❌ Неправильно:
# Одноразовая оценка с temperature > 0
model_a_scores = [evaluate_example(ex, model="a", temperature=0.7)
for ex in test_set]
model_b_scores = [evaluate_example(ex, model="b", temperature=0.7)
for ex in test_set]
mean_a = mean(model_a_scores) # е.g., 0.82
mean_b = mean(model_b_scores) # е.g., 0.85
if mean_b > mean_a:
print("Model B is better!") # ❌ Могло быть шумом, не реальным улучшением
✅ Правильно:
# Множественные запуски с анализом дисперсии
import numpy as np
from scipy import stats
def evaluate_with_variance(test_set, model, num_runs=5):
"""Запустить оценку много раз для захвата дисперсии."""
run_scores = []
for run in range(num_runs):
scores = [
evaluate_example(ex, model=model, temperature=0.7, seed=run)
for ex in test_set
]
run_scores.append(mean(scores))
return {
"mean": np.mean(run_scores),
"std": np.std(run_scores),
"median": np.median(run_scores),
"runs": run_scores,
}
model_a_result = evaluate_with_variance(test_set, "a")
model_b_result = evaluate_with_variance(test_set, "b")
# Статистический тест с учётом дисперсии
statistic, p_value = stats.ttest_ind(
model_a_result["runs"],
model_b_result["runs"],
)
print(f"Model A: {model_a_result['mean']:.3f} ± {model_a_result['std']:.3f}")
print(f"Model B: {model_b_result['mean']:.3f} ± {model_b_result['std']:.3f}")
print(f"Significant difference: {p_value < 0.05} (p={p_value:.4f})")
Ошибка 5: Использование метрик точного совпадения для выходов агента
Симптом: Агент производит правильный ответ в другом формате, получает оценку 0.
Влияние: Недооценивает компетентность агента; препятствует валидным альтернативным решениям.
❌ Неправильно:
# Точное совпадение строки для оценки агента
def evaluate_agent_answer(actual, expected):
return 1.0 if actual == expected else 0.0
# Пример
expected = "The capital of France is Paris."
actual = "Paris is the capital of France."
score = evaluate_agent_answer(actual, expected)
# ❌ score = 0.0, хотя ответ правильный
✅ Правильно:
# Семантическая оценка для выходов агента
from deepeval.metrics import AnswerRelevancyMetric
from deepeval.test_case import LLMTestCase
def evaluate_agent_answer(question, actual, expected):
# Используйте LLM-as-judge для семантической эквивалентности
test_case = LLMTestCase(
input=question,
actual_output=actual,
expected_output=expected,
)
metric = AnswerRelevancyMetric(threshold=0.8)
metric.measure(test_case)
return metric.score
# Пример
question = "What is the capital of France?"
expected = "The capital of France is Paris."
actual = "Paris is the capital of France."
score = evaluate_agent_answer(question, actual, expected)
# ✅ score ≈ 0.95, распознаёт семантическую эквивалентность
Интеграция RAGAS
RAGAS предоставляет метрики, специально разработанные для оценки RAG систем:
from ragas import evaluate
from ragas.metrics import (
faithfulness,
answer_relevancy,
context_precision,
context_recall,
)
from datasets import Dataset
# Подготовить датасет оценки
eval_data = {
"question": [
"What is the capital of France?",
"How does photosynthesis work?",
],
"answer": [
"The capital of France is Paris, known for the Eiffel Tower.",
"Photosynthesis converts light energy into chemical energy in plants.",
],
"contexts": [
["Paris is the capital and largest city of France."],
["Photosynthesis is the process by which plants use sunlight, water, and CO2 to produce oxygen and energy in the form of sugar."],
],
"ground_truth": [
"Paris",
"Plants convert light energy into chemical energy",
],
}
dataset = Dataset.from_dict(eval_data)
# Запустить RAGAS оценку
result = evaluate(
dataset=dataset,
metrics=[
faithfulness, # Ответ основан на контексте
answer_relevancy, # Ответ отвечает на вопрос
context_precision, # Полученные чанки ранжированы по релевантности
context_recall, # Ground truth охватывается контекстом
],
)
print(result)
# Выход:
# {
# 'faithfulness': 0.95, # 95% утверждений поддержано контекстом
# 'answer_relevancy': 0.88, # Ответ релевантен вопросу
# 'context_precision': 0.92, # Полученный контекст хорошо ранжирован
# 'context_recall': 0.85, # 85% ground truth в контексте
# }
Пользовательские метрики RAGAS
from ragas.metrics import Metric
from ragas.metrics.base import MetricWithLLM
class CustomCitationMetric(MetricWithLLM):
"""Пользовательская метрика: проверить, что ответ включает ссылки на контекст."""
name = "citation_quality"
async def _ascore(self, row):
answer = row["answer"]
contexts = row["contexts"]
# Проверить, ссылается ли ответ на контекст (е.g., [1], [2])
has_citations = bool(__import__("re").search(r'\[\d+\]', answer))
if not has_citations:
return 0.0
# Используйте LLM для проверки, что ссылки точны
prompt = f"""
Check if the citations in the answer correctly reference the provided contexts.
Answer: {answer}
Contexts:
{chr(10).join(f"[{i+1}] {ctx}" for i, ctx in enumerate(contexts))}
Return only a score from 0.0 to 1.0:
- 1.0: All citations are accurate
- 0.5: Some citations are wrong
- 0.0: Citations are misleading or wrong
"""
response = await self.llm.agenerate([prompt])
score_text = response.generations[0][0].text.strip()
try:
score = float(score_text)
return max(0.0, min(1.0, score))
except ValueError:
return 0.5 # Default to mid-range if parsing fails
# Используйте пользовательскую метрику
citation_metric = CustomCitationMetric()
result = evaluate(
dataset=dataset,
metrics=[faithfulness, answer_relevancy, citation_metric],
)
Интеграция DeepEval
DeepEval предоставляет гибкую оценку с пользовательскими критериями и CI/CD интеграцией:
from deepeval import assert_test
from deepeval.metrics import GEval, HallucinationMetric, AnswerRelevancyMetric
from deepeval.test_case import LLMTestCase, LLMTestCaseParams
# Определить test case
test_case = LLMTestCase(
input="What are the main causes of climate change?",
actual_output="The main causes of climate change include greenhouse gas emissions from burning fossil fuels, deforestation, and industrial processes. CO2 and methane trap heat in the atmosphere.",
expected_output="Greenhouse gas emissions, primarily from fossil fuels, are the main driver of climate change.",
context=["Climate change is primarily caused by human activities that emit greenhouse gases.", "Burning fossil fuels releases CO2, the main greenhouse gas."],
)
# Метрика Faithfulness (обнаружение галлюцинации)
faithfulness_metric = HallucinationMetric(threshold=0.8)
# Релевантность ответа
relevancy_metric = AnswerRelevancyMetric(threshold=0.7)
# Пользовательские критерии с GEval
correctness_metric = GEval(
name="correctness",
criteria="Determine if the actual output is factually correct compared to the expected output.",
evaluation_steps=[
"Compare key facts in actual output to expected output",
"Check for any factual errors or omissions",
"Assign score based on accuracy: 1.0=perfect, 0.5=partial, 0.0=wrong",
],
evaluation_params={"temperature": 0.0},
)
# Оценить
assert_test(test_case, [faithfulness_metric, relevancy_metric, correctness_metric])
Пользовательские критерии GEval для агентов
# Пользовательские критерии для оценки траектории агента
trajectory_quality = GEval(
name="trajectory_quality",
criteria="Evaluate the agent's decision-making process and tool usage.",
evaluation_steps=[
"Check if the agent chose appropriate tools for each step",
"Verify the agent recovered from errors effectively",
"Assess if the trajectory was efficient (no unnecessary steps)",
"Rate overall trajectory quality from 0.0 to 1.0",
],
evaluation_params={"temperature": 0.1, "model": "gpt-4o"},
)
# Agent test case
agent_test_case = LLMTestCase(
input="Find the current stock price of Apple and calculate 10% gain",
actual_output="AAPL is currently $182.50. A 10% gain would be $200.75.",
context=["Agent trajectory: 1) Called stock_price('AAPL') → $182.50, 2) Called calculator(182.50 * 1.10) → $200.75"],
retrieval_context=["Stock price API documentation", "Calculator tool documentation"],
)
trajectory_quality.measure(agent_test_case)
print(f"Trajectory Quality Score: {trajectory_quality.score}")
print(f"Reasoning: {trajectory_quality.reason}")
Обнаружение регрессии с DeepEval
from deepeval.dataset import EvaluationDataset
from deepeval.test_case import LLMTestCase
from deepeval import evaluate
# Загрузить базовые результаты
baseline_dataset = EvaluationDataset(test_cases=[...])
baseline_results = evaluate(baseline_dataset, metrics=[faithfulness_metric])
# Оценить новую версию
new_test_cases = [
LLMTestCase(input=ex.input, actual_output=new_model(ex.input), context=ex.context)
for ex in baseline_dataset
]
new_dataset = EvaluationDataset(test_cases=new_test_cases)
new_results = evaluate(new_dataset, metrics=[faithfulness_metric])
# Сравнить
baseline_score = baseline_results["faithfulness"]
new_score = new_results["faithfulness"]
regression_detected = (baseline_score - new_score) > 0.05 # 5% порог
if regression_detected:
print(f"⚠️ Regression detected: {baseline_score:.3f} → {new_score:.3f}")
else:
print(f"✅ No regression: {baseline_score:.3f} → {new_score:.3f}")
CI/CD Gate с DeepEval
# test_agent_quality.py
import pytest
from deepeval import assert_test
from deepeval.metrics import GEval
from deepeval.test_case import LLMTestCase
@pytest.mark.parametrize(
"test_case",
[
LLMTestCase(
input="What is 2+2?",
actual_output=agent_run("What is 2+2?"),
expected_output="4",
),
LLMTestCase(
input="List 3 mammals",
actual_output=agent_run("List 3 mammals"),
expected_output="Dogs, cats, whales",
),
]
)
def test_agent_quality(test_case):
metric = GEval(
name="answer_quality",
criteria="Is the answer correct and complete?",
evaluation_params={"temperature": 0.0},
)
assert_test(test_case, [metric])
# Запустить с: pytest test_agent_quality.py
# Отказывает CI если любой тест отказывает
Паттерны LLM-as-Judge
Шаблон структурированного Judge Prompt
JUDGE_TEMPLATE = """You are an expert evaluator assessing AI system outputs.
## Task
Evaluate the response below according to the provided rubric.
## Input
{input}
## Response to Evaluate
{output}
## Reference Answer (if available)
{reference}
## Rubric
{rubric}
## Instructions
1. Read the response carefully
2. Compare against the rubric criteria
3. Assign a score from 0.0 to 1.0
4. Explain your reasoning in 2-3 sentences
Return JSON:
{{
"score": 0.0-1.0,
"reasoning": "brief explanation",
"passes_threshold": true/false
}}
Be consistent: same quality should get same score across evaluations.
Be calibrated: reserve 0.9+ for exceptional quality, 0.0-0.3 for poor quality.
"""
def llm_as_judge(input_text, output_text, reference=None, rubric="", threshold=0.7):
prompt = JUDGE_TEMPLATE.format(
input=input_text,
output=output_text,
reference=reference or "N/A",
rubric=rubric,
)
response = judge_llm.invoke(prompt, temperature=0.0)
result = parse_json(response)
result["passes_threshold"] = result["score"] >= threshold
return result
Смягчение смещения с помощью калибровки
import numpy as np
from sklearn.metrics import cohen_kappa_score
def calibrate_judge_against_human_labels(judge_fn, test_set_with_human_labels):
"""
Проверить согласие judge с человеческими аннотациями.
Cohen's kappa > 0.8 указывает на сильное согласие.
"""
human_scores = []
judge_scores = []
for example in test_set_with_human_labels:
human_score = example["human_score"] # 0.0-1.0
judge_result = judge_fn(
input_text=example["input"],
output_text=example["output"],
)
judge_score = judge_result["score"]
human_scores.append(human_score)
judge_scores.append(judge_score)
# Конвертировать в категорийные для kappa (buckets: низкий, средний, высокий)
def bucketize(score):
if score < 0.4:
return 0 # низкий
elif score < 0.7:
return 1 # средний
else:
return 2 # высокий
human_categorical = [bucketize(s) for s in human_scores]
judge_categorical = [bucketize(s) for s in judge_scores]
kappa = cohen_kappa_score(human_categorical, judge_categorical)
correlation = np.corrcoef(human_scores, judge_scores)[0, 1]
print(f"Cohen's Kappa: {kappa:.3f} (>0.8 = сильное согласие)")
print(f"Correlation: {correlation:.3f}")
if kappa < 0.6:
print("⚠️ Низкое согласие с людьми. Отрегулируйте judge prompt или используйте другую модель.")
return {
"kappa": kappa,
"correlation": correlation,
"human_scores": human_scores,
"judge_scores": judge_scores,
}
Ensemble множественных судей
async def ensemble_judge(input_text, output_text, num_judges=3):
"""
Используйте множественных судей и агрегируйте оценки для снижения дисперсии.
"""
import asyncio
async def single_judge(judge_id):
result = llm_as_judge(
input_text,
output_text,
temperature=0.1, # Лёгкая temperature для разнообразия
)
return result["score"]
# Запустить судей параллельно
tasks = [single_judge(i) for i in range(num_judges)]
scores = await asyncio.gather(*tasks)
return {
"mean_score": np.mean(scores),
"median_score": np.median(scores),
"std_dev": np.std(scores),
"individual_scores": scores,
"high_agreement": np.std(scores) < 0.1, # Низкая дисперсия = согласие
}
Регрессионный testing pipeline
Управление Golden Dataset
import json
from pathlib import Path
from datetime import datetime
class GoldenDataset:
"""Управлять версионированными golden датасетами для регрессионного тестирования."""
def __init__(self, dataset_path="golden_datasets"):
self.dataset_path = Path(dataset_path)
self.dataset_path.mkdir(exist_ok=True)
def create_version(self, examples, version_name=None):
"""Создать новую версию golden датасета."""
if version_name is None:
version_name = datetime.now().strftime("%Y%m%d_%H%M%S")
version_file = self.dataset_path / f"{version_name}.jsonl"
with open(version_file, "w") as f:
for example in examples:
f.write(json.dumps(example) + "\n")
print(f"Created golden dataset version: {version_name}")
return version_name
def load_version(self, version_name):
"""Загрузить специфическую версию golden датасета."""
version_file = self.dataset_path / f"{version_name}.jsonl"
examples = []
with open(version_file) as f:
for line in f:
examples.append(json.loads(line))
return examples
def list_versions(self):
"""Список всех доступных версий датасета."""
versions = [f.stem for f in self.dataset_path.glob("*.jsonl")]
return sorted(versions)
# Использование
dataset_manager = GoldenDataset()
# Создать начальный golden датасет
examples = [
{"input": "What is 2+2?", "expected_output": "4", "category": "math"},
{"input": "Capital of France?", "expected_output": "Paris", "category": "geography"},
]
version = dataset_manager.create_version(examples, version_name="v1.0")
# Загрузить для оценки
test_cases = dataset_manager.load_version("v1.0")
Threshold Gates для CI/CD
class EvaluationGate:
"""CI/CD gate, который отказывает build если метрики ниже порога."""
def __init__(self, thresholds):
self.thresholds = thresholds # е.g., {"faithfulness": 0.8, "relevancy": 0.7}
def evaluate_with_gate(self, test_cases, metrics):
"""
Запустить оценку и определить pass/fail.
Возвращает gate_passed (bool) и детальные результаты.
"""
from deepeval import evaluate
from deepeval.dataset import EvaluationDataset
dataset = EvaluationDataset(test_cases=test_cases)
results = evaluate(dataset, metrics)
failures = []
for metric_name, threshold in self.thresholds.items():
score = results.get(metric_name, 0.0)
if score < threshold:
failures.append({
"metric": metric_name,
"score": score,
"threshold": threshold,
"gap": threshold - score,
})
gate_passed = len(failures) == 0
return {
"gate_passed": gate_passed,
"results": results,
"failures": failures,
"summary": f"{'✅ PASSED' if gate_passed else '❌ FAILED'} - {len(failures)} metrics below threshold",
}
# CI/CD использование
gate = EvaluationGate(thresholds={
"faithfulness": 0.85,
"answer_relevancy": 0.75,
})
test_cases = [...] # Загрузить из golden датасета
metrics = [HallucinationMetric(), AnswerRelevancyMetric()]
result = gate.evaluate_with_gate(test_cases, metrics)
print(result["summary"])
if not result["gate_passed"]:
print("\nFailures:")
for failure in result["failures"]:
print(f" - {failure['metric']}: {failure['score']:.3f} < {failure['threshold']:.3f}")
exit(1) # Fail CI build
Обнаружение регрессии статистическими методами
from scipy import stats
import numpy as np
def detect_regression_statistical(baseline_scores, current_scores, alpha=0.05):
"""
Использовать тест Wilcoxon signed-rank для обнаружения значительной регрессии.
Null hypothesis: текущая performance >= baseline
"""
baseline = np.array(baseline_scores)
current = np.array(current_scores)
if len(baseline) != len(current):
raise ValueError("Score arrays must have equal length for paired test")
# Тест Wilcoxon signed-rank (непараметрический, paired)
# Alternative='less': тестирование если current < baseline (регрессия)
statistic, p_value = stats.wilcoxon(current, baseline, alternative='less')
regression_detected = p_value < alpha
mean_baseline = np.mean(baseline)
mean_current = np.mean(current)
delta = mean_current - mean_baseline
return {
"regression_detected": regression_detected,
"p_value": p_value,
"alpha": alpha,
"mean_baseline": mean_baseline,
"mean_current": mean_current,
"delta": delta,
"delta_pct": (delta / mean_baseline * 100) if mean_baseline > 0 else 0,
"conclusion": (
f"⚠️ Significant regression detected (p={p_value:.4f})"
if regression_detected else
f"✅ No significant regression (p={p_value:.4f})"
),
}
# Пример использования
baseline = [0.85, 0.82, 0.88, 0.90, 0.87] # Оценки предыдущей версии
current = [0.80, 0.79, 0.84, 0.85, 0.82] # Оценки текущей версии
result = detect_regression_statistical(baseline, current)
print(result["conclusion"])
Performance & Benchmarks
Примечание: Приведённые ниже цифры являются иллюстративными оценками на основе типичных production-конфигураций, а не измерениями конкретной системы.
Стоимость оценки RAGAS: Оценка faithfulness и answer relevancy требует LLM вызовов для каждого примера. Для датасета из 100 примеров ожидайте стоимость LLM вывода, пропорциональную размеру контекста и сложности метрики. Faithfulness (на основе NLI) более дорогая, чем answer relevancy (семантическое сходство).
Latency DeepEval GEval: GEval с пользовательскими критериями включает LLM вызовы с детальными prompts. Время оценки на пример обычно варьируется от нескольких секунд до десятков секунд в зависимости от сложности prompt и скорости модели. Batch оценка и async выполнение могут улучшить throughput.
LLM-as-Judge vs Human Labels: Выравнивание LLM-as-judge с человеческими метками варьируется в зависимости от сложности задачи. Для объективных задач (factual QA) согласие может быть высокое. Для субъективных задач (качество письма) выравнивание снижается. Калибровка против человеческих меток на подмножестве (50-100 примеров) рекомендуется.
Влияние смещения позиции: Без смягчения смещение позиции может существенно повлиять на результаты сравнения. Оценка с swap-ом снижает смещение, но удваивает оценочные вызовы. Для высокостав решений стоимость оправдана.
Экономия каскадной оценки: Использование дешёвого judge для ясных случаев (оценки ниже 0.3 или выше 0.7) и эскалация граничных случаев к дорогому judge может существенно снизить затраты при сохранении качества. Экономия зависит от распределения оценок в датасете.
Golden Dataset Size vs Statistical Power: Обнаружение маленьких улучшений (е.g., 2-3% увеличение оценки) требует больших датасетов. Датасет из 100 примеров предоставляет адекватную мощность для обнаружения умеренных эффектов. Для меньших эффектов или выше уверенности датасеты из 300+ примеров рекомендуются.
Чувствительность обнаружения регрессии: 5% порог регрессии распространён для CI/CD gates. Более строгие пороги (2-3%) увеличивают false positives из-за шума оценки. Тестирование статистической значимости (тест Wilcoxon) помогает отличить реальные регрессии от случайной дисперсии.
Дисперсия множественных судей: Запуск той же оценки много раз (3-5 запусков) и агрегирование снижает шум от LLM-as-judge стохастичности. Дисперсия оценок обычно выше для субъективных критериев, чем объективных.
Performance GAIA Benchmark: Задачи GAIA Level 1 (одношаговый web search) имеют тенденцию к выше success rates, чем Level 3 задачи (сложная многоинструментальная координация). Различия в performance между уровнями указывают на пробелы в способности агента в планировании vs выполнении.
Оценки SWE-bench Verified: SWE-bench Verified содержит вручную проверенные экземпляры для снижения шума от неоднозначных или неправильных меток в полном SWE-bench. Опубликованные оценки сильно варьируются между системами, с top-performing агентами разрешающими значительную часть экземпляров, тогда как более простые системы борются с многофайловыми изменениями и сложным рассуждением.