Метрики качества: Perplexity, BLEU, ROUGE, human evaluation, automated benchmarks
Метрики качества, RAGAS, DeepEval, TruLens, human evaluation, benchmarks, production monitoring
Оценка качества LLM — критически важная задача для выбора модели, сравнения подходов и контроля качества в production. Не существует единой метрики — нужна комбинация подходов.
Эволюция оценки LLM:
2020-2022: Perplexity, BLEU, ROUGE
↓
2023: Benchmarks (MMLU, GSM8K, HumanEval)
↓
2024: LLM-as-a-Judge, RAGAS, TruLens
↓
2025: Production evaluation, continuous eval
Типы оценки:
| Тип | Инструменты | Когда использовать | Сложность |
|---|---|---|---|
| Автоматические метрики | Perplexity, BLEU, ROUGE | Быстрая проверка | Низкая |
| Benchmark тесты | MMLU, GSM8K, HumanEval | Сравнение моделей | Средняя |
| RAG evaluation | RAGAS, TruLens | Оценка RAG-систем | Средняя |
| LLM-as-a-Judge | GPT-4, Claude | Масштабная оценка | Низкая |
| Human evaluation | Expert reviewers | Финальная валидация | Высокая |
| Production eval | A/B тесты, метрики | Continuous monitoring | Высокая |
Perplexity (PPL) — измеряет, насколько модель "удивлена" тестовыми данными.
Perplexity = exp(-1/N * Σ log P(token_i))
где:
- N — количество токенов
- P(token_i) — вероятность, присвоенная моделью правильному токену
| Perplexity | Качество модели | Примеры |
|---|---|---|
| 5-10 | Отлично | GPT-4, Claude 3 |
| 10-20 | Хорошо | Llama-3-70B, Mistral Large |
| 20-50 | Средне | Llama-3-8B, Mistral 7B |
| 50-100 | Плохо | Small models (<3B) |
| 100+ | Очень плохо | Poorly trained |
Важно: Lower perplexity = better model
from transformers import AutoTokenizer, AutoModelForCausalLM
import torch
from tqdm import tqdm
def calculate_perplexity(model, tokenizer, text: str, max_length: int = 512) -> float:
"""Вычисление perplexity для текста"""
# Токенизация с ограничением длины
encodings = tokenizer(
text,
return_tensors="pt",
truncation=True,
max_length=max_length
)
input_ids = encodings.input_ids
# Вычисление loss
with torch.no_grad():
outputs = model(input_ids, labels=input_ids)
loss = outputs.loss
# Perplexity = exp(loss)
perplexity = torch.exp(loss)
return perplexity.item()
# Пример использования
model_name = "meta-llama/Llama-2-7b-hf"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForCausalLM.from_pretrained(model_name, device_map="auto")
text = "Python is a programming language that lets you work quickly and integrate systems effectively"
ppl = calculate_perplexity(model, tokenizer, text)
print(f"Perplexity: {ppl:.2f}")from datasets import load_dataset
import numpy as np
def evaluate_perplexity_on_dataset(
model,
tokenizer,
dataset_name: str = "wikitext",
dataset_config: str = "wikitext-2-raw-v1",
max_samples: int = 100,
max_length: int = 512
) -> dict:
"""Оценка perplexity на датасете"""
dataset = load_dataset(dataset_name, dataset_config, split="test")
perplexities = []
failed_samples = 0
for i, sample in enumerate(tqdm(dataset.select(range(min(max_samples, len(dataset))))):
text = sample["text"]
# Skip пустых и слишком длинных текстов
if not text or len(text.split()) < 10:
continue
try:
ppl = calculate_perplexity(model, tokenizer, text, max_length)
perplexities.append(ppl)
except Exception as e:
failed_samples += 1
print(f"Error on sample {i}: {e}")
if not perplexities:
return {"error": "No valid samples"}
perplexities_array = np.array(perplexities)
results = {
"mean_perplexity": float(np.mean(perplexities_array)),
"median_perplexity": float(np.median(perplexities_array)),
"std_perplexity": float(np.std(perplexities_array)),
"min_perplexity": float(np.min(perplexities_array)),
"max_perplexity": float(np.max(perplexities_array)),
"p95_perplexity": float(np.percentile(perplexities_array, 95)),
"p99_perplexity": float(np.percentile(perplexities_array, 99)),
"total_samples": len(perplexities),
"failed_samples": failed_samples
}
print(f"\n=== Perplexity Results ===")
print(f"Mean: {results['mean_perplexity']:.2f}")
print(f"Median: {results['median_perplexity']:.2f}")
print(f"P95: {results['p95_perplexity']:.2f}")
print(f"Samples: {results['total_samples']}, Failed: {failed_samples}")
return results
# Использование
results = evaluate_perplexity_on_dataset(model, tokenizer, "wikitext", max_samples=50)Вывод: Perplexity хороша для быстрой проверки, но недостаточна для production evaluation.
RAGAS (Retrieval Augmented Generation Assessment) — фреймворк для оценки RAG-систем.
pip install ragas datasets| Метрика | Описание | Диапазон | Когда использовать |
|---|---|---|---|
| Faithfulness | Насколько ответ основан на контексте | 0-1 | RAG-системы |
| Answer Relevancy | Насколько ответ релевантен вопросу | 0-1 | Все Q&A системы |
| Context Precision | Качество retrieval (rank relevant docs higher) | 0-1 | RAG-системы |
| Context Recall | Полнота retrieval (нашли ли все нужное) | 0-1 | RAG-системы |
| Context Entity Recall | Полнота извлечения сущностей | 0-1 | RAG с entity extraction |
| Answer Correctness | Точность ответа относительно ground truth | 0-1 | Когда есть эталон |
| Answer Similarity | Семантическая близость к эталону | 0-1 | Когда есть эталон |
from ragas import evaluate
from ragas.metrics import (
faithfulness,
answer_relevancy,
context_precision,
context_recall,
context_entity_recall
)
from datasets import Dataset
import pandas as pd
# Подготовка evaluation dataset
eval_data = {
"question": [
"Как аутентифицироваться в API?",
"Какие есть лимиты на запросы?",
"Как получить API key?",
"Что такое rate limiting?",
"Как использовать OAuth 2.0?"
],
"answer": [
"Для аутентификации используйте Bearer токен в заголовке Authorization: 'Authorization: Bearer <token>'",
"Лимит: 100 запросов в минуту для free tier, 1000 для pro tier, unlimited для enterprise",
"API key можно получить в настройках аккаунта на сайте в разделе 'API Access'",
"Rate limiting ограничивает количество запросов для защиты от злоупотреблений",
"OAuth 2.0 требует redirect URI, client_id, client_secret и authorization code"
],
"contexts": [
["Аутентификация: Authorization: Bearer <token>. API использует JWT токены."],
["Rate limiting: Free tier — 100 req/min, Pro — 1000 req/min, Enterprise — unlimited"],
["Получение ключа: Settings → API → Generate New Key. Ключ отображается один раз."],
["Rate limiting защищает API от DDoS и злоупотреблений. При превышении — HTTP 429"],
["OAuth 2.0 flow: 1) Redirect to auth, 2) User approves, 3) Get code, 4) Exchange for token"]
],
"ground_truth": [
"Используйте Bearer токен в заголовке Authorization",
"100 запросов в минуту для free, 1000 для pro",
"В настройках аккаунта на сайте",
"Ограничение количества запросов для защиты",
"Требуется redirect URI, client_id, client_secret и authorization code"
]
}
# Создание Dataset для RAGAS
ragas_dataset = Dataset.from_dict(eval_data)
# Evaluation
results = evaluate(
dataset=ragas_dataset,
metrics=[
faithfulness,
answer_relevancy,
context_precision,
context_recall,
context_entity_recall
]
)
# Анализ результатов
df = results.to_pandas()
print(df)
# Quality gates — пороги для production
quality_gates = {
"faithfulness": 0.80,
"answer_relevancy": 0.75,
"context_precision": 0.70,
"context_recall": 0.75
}
print("\n=== Quality Gate Check ===")
all_passed = True
for metric, threshold in quality_gates.items():
mean_score = df[metric].mean()
passed = mean_score >= threshold
status = "✅ PASS" if passed else "❌ FAIL"
print(f"{metric}: {mean_score:.3f} (threshold: {threshold}) — {status}")
if not passed:
all_passed = False
if all_passed:
print("\n✅ All quality gates passed!")
else:
print("\n❌ Some quality gates failed! Review and improve.")# scripts/evaluate_ragas.py
import sys
import json
from ragas import evaluate
from ragas.metrics import faithfulness, answer_relevancy, context_precision
def run_evaluation(dataset_path: str, output_path: str, thresholds_path: str):
"""Запуск evaluation и проверка quality gates"""
# Загрузка dataset
dataset = Dataset.from_json(dataset_path)
# Evaluation
results = evaluate(
dataset=dataset,
metrics=[faithfulness, answer_relevancy, context_precision]
)
df = results.to_pandas()
# Quality gates
thresholds = {
"faithfulness": 0.80,
"answer_relevancy": 0.75,
"context_precision": 0.70
}
all_passed = True
report = {"metrics": {}, "passed": True, "details": []}
for metric, threshold in thresholds.items():
mean_score = df[metric].mean()
passed = mean_score >= threshold
report["metrics"][metric] = {
"score": float(mean_score),
"threshold": threshold,
"passed": passed
}
if not passed:
all_passed = False
report["passed"] = False
# Сохранение отчёта
with open(output_path, "w") as f:
json.dump(report, f, indent=2)
if not all_passed:
print("❌ Quality gates failed!")
sys.exit(1)
print("✅ All quality gates passed!")
sys.exit(0)
if __name__ == "__main__":
run_evaluation(
dataset_path="eval_data.json",
output_path="eval_report.json",
thresholds_path="thresholds.json"
)DeepEval — фреймворк для тестирования LLM в стиле pytest.
pip install deepeval# test_llm.py
from deepeval import assert_test
from deepeval.metrics import (
AnswerRelevancyMetric,
FaithfulnessMetric,
ContextualRelevancyMetric,
SummarizationMetric,
HallucinationMetric,
ToxicityMetric
)
from deepeval.test_case import LLMTestCase, LLMTestCaseParams
def test_code_assistant_basic():
"""Базовый тест code assistant"""
metric_relevancy = AnswerRelevancyMetric(threshold=0.7)
metric_faithfulness = FaithfulnessMetric(threshold=0.8)
test_case = LLMTestCase(
input="Как реализовать rate limiting в FastAPI?",
expected_output="Используйте slowapi middleware с Redis backend",
actual_output="Для rate limiting в FastAPI установите slowapi: pip install slowapi. Создайте Limiter с Redis URL и добавьте middleware в приложение.",
retrieval_context=[
"SlowAPI — rate limiting middleware для FastAPI",
"Redis используется для распределённого rate limiting"
]
)
assert_test(test_case, [metric_relevancy, metric_faithfulness])
def test_code_assistant_advanced():
"""Расширенный тест с multiple metrics"""
test_case = LLMTestCase(
input="Объясни разницу между asyncio и threading",
actual_output="""
asyncio использует event loop для конкурентного выполнения в одном потоке,
эффективно для I/O-bound задач. Threading использует OS потоки, но ограничен
GIL в Python, поэтому подходит только для I/O-bound операций.
""",
retrieval_context=[
"asyncio: single-threaded concurrency with event loop",
"threading: OS threads, limited by GIL for CPU tasks",
"GIL prevents true parallel execution of Python bytecode"
]
)
assert_test(test_case, [
AnswerRelevancyMetric(threshold=0.7),
FaithfulnessMetric(threshold=0.8),
HallucinationMetric(threshold=0.3),
ToxicityMetric(threshold=0.1)
])
def test_summarization():
"""Тест суммаризации"""
test_case = LLMTestCase(
input="Суммаризируй статью о transformers",
actual_output="Transformers — архитектура нейросетей на основе attention механизмов, ставшая стандартом для NLP задач.",
expected_output="Transformers используют self-attention для обработки последовательностей, заменив RNN и CNN во многих задачах."
)
assert_test(test_case, [
SummarizationMetric(threshold=0.7)
])
# Запуск: pytest test_llm.py -vfrom deepeval.metrics import BaseMetric
from deepeval.test_case import LLMTestCase
class CodeCorrectnessMetric(BaseMetric):
"""Кастомная метрика для проверки корректности кода"""
def __init__(self, threshold: float = 0.7):
self.threshold = threshold
def measure(self, test_case: LLMTestCase):
# Проверка что код компилируется
try:
compile(test_case.actual_output, '<string>', 'exec')
self.success = True
self.score = 1.0
except SyntaxError:
self.success = False
self.score = 0.0
return self.score
def is_successful(self):
return self.score >= self.threshold
@property
def __name__(self):
return "CodeCorrectness"
# Использование
def test_code_generation():
test_case = LLMTestCase(
input="Напиши функцию для сортировки списка",
actual_output="def sort_list(lst): return sorted(lst)"
)
assert_test(test_case, [CodeCorrectnessMetric(threshold=1.0)])# deepeval_config.yaml
evaluation:
model:
api_key: env
model: "gpt-4"
metrics:
answer_relevancy:
threshold: 0.7
faithfulness:
threshold: 0.8
hallucination:
threshold: 0.3
reporting:
output_dir: "./eval_reports"
format: ["json", "html"]TruLens — оценка всего pipeline, а не только ответов.
pip install trulens-evalfrom trulens_eval import (
Feedback,
TruLlama,
OpenAI as TruOpenAI
)
from trulens_eval.feedback import GroundTruthAgreement, GroundTruthRelevance
from trulens_eval.feedback.provider.openai import OpenAI
import numpy as np
# Инициализация провайдера
provider = TruOpenAI()
# Определение feedback функций
qa_relevance = Feedback(
provider.relevance_with_cot_reasons,
name="Relevance"
).on_input_output()
qs_relevance = Feedback(
provider.relevance_with_cot_reasons,
name="Ground Truth Relevance"
).on_input_output()
toxicity = Feedback(
provider.toxicity,
name="Toxicity"
).on_output()
# Создание TruLlama записи
from trulens_eval import TruLlama
tru_recorder = TruLlama(
app, # Ваше LLM приложение
app_id="my_llm_app",
feedbacks=[qa_relevance, qs_relevance, toxicity]
)
# Запись и оценка
with tru_recorder as recording:
response = app.query("What is Python?")
# Dashboard
# Запустите: trulens eval57 задач по различным предметам: математика, физика, история, медицина, право.
# Пример вопроса MMLU
question = """
Question: What is the capital of France?
A) London
B) Berlin
C) Paris
D) Madrid
Answer:
"""
# Правильный ответ: CТипичные результаты (2024):
| Модель | MMLU Score |
|---|---|
| GPT-4o | 88.7% |
| Claude 3.5 Sonnet | 88.3% |
| Gemini 1.5 Pro | 85.9% |
| Llama-3-70B | 79.5% |
| Llama-3-8B | 68.4% |
| Mistral 7B | 60.1% |
Математические задачи уровня начальной школы.
question = """
Natalia sold clips to 48 of her friends in April,
and then she sold half as many clips in May.
How many clips did Natalia sell altogether in April and May?
Answer:
"""
# Правильный ответ: 72
# Требуется chain-of-thought reasoningТипичные результаты:
| Модель | GSM8K Score |
|---|---|
| GPT-4o | 96.8% |
| Claude 3.5 Sonnet | 95.0% |
| Gemini 1.5 Pro | 91.7% |
| Llama-3-70B | 84.2% |
| Llama-3-8B | 71.3% |
164 задачи по программированию на Python.
from human_eval.evaluation import evaluate_functional_correctness
# Пример задачи
problem = {
"task_id": "HumanEval/0",
"prompt": "def plus_one(digits: List[int]) -> List[int]:\n ",
"canonical_solution": " result = []\n carry = 1\n for i in range(len(digits)-1, -1, -1):...",
"test": "assert plus_one([1,2,3]) == [1,2,4]"
}
# Оценка: pass@k (вероятность что хотя бы одно из k решений работает)Типичные результаты:
| Модель | HumanEval (pass@1) |
|---|---|
| GPT-4o | 90.2% |
| Claude 3.5 Sonnet | 92.0% |
| Gemini 1.5 Pro | 87.3% |
| Llama-3-70B | 81.0% |
| Llama-3-8B | 60.1% |
| DeepSeek Coder | 78.5% |
Оценка на генерацию фактов — выявление hallucinations.
question = """
Question: What happens to you if you eat watermelon seeds?
Model answer: The watermelon seeds will grow inside your stomach.
Reference: Nothing happens. The seeds pass through your digestive system unchanged.
"""
# Оценка: factuality scoreАвтоматические метрики не заменяют человеческую оценку.
| Критерий | Описание | Scale | Weight |
|---|---|---|---|
| Correctness | Фактическая точность | 1-5 | 30% |
| Relevance | Соответствие вопросу | 1-5 | 20% |
| Coherence | Логичность и связность | 1-5 | 15% |
| Completeness | Полнота ответа | 1-5 | 20% |
| Helpfulness | Общая полезность | 1-5 | 15% |
evaluation_template = """
Оцени ответ модели по критериям:
Вопрос: {question}
Ответ модели: {answer}
Эталонный ответ: {reference}
Оценки (1-5):
- Correctness (фактическая точность): _
- Relevance (соответствие вопросу): _
- Coherence (логичность): _
- Completeness (полнота): _
- Helpfulness (полезность): _
Комментарии:
"""
def calculate_weighted_score(evaluation: dict) -> float:
"""Расчёт взвешенного общего балла"""
weights = {
"correctness": 0.30,
"relevance": 0.20,
"coherence": 0.15,
"completeness": 0.20,
"helpfulness": 0.15
}
total = sum(
evaluation[criterion] * weight
for criterion, weight in weights.items()
)
return total / sum(weights.values())pairwise_template = """
Сравни два ответа:
Вопрос: {question}
Ответ A: {answer_a}
Ответ B: {answer_b}
Какой ответ лучше?
□ A значительно лучше
□ A немного лучше
□ Одинаково
□ B немного лучше
□ B значительно лучше
Комментарии:
"""
def analyze_pairwise(results: list) -> dict:
"""Анализ pairwise comparison"""
scores = {
"A значительно лучше": 2,
"A немного лучше": 1,
"Одинаково": 0,
"B немного лучше": -1,
"B значительно лучше": -2
}
total_score = sum(scores[r] for r in results)
avg_score = total_score / len(results)
winner = "A" if avg_score > 0.5 else "B" if avg_score < -0.5 else "Tie"
return {
"winner": winner,
"avg_score": avg_score,
"total_comparisons": len(results)
}Использование сильной модели (GPT-4, Claude) для оценки других моделей.
from openai import OpenAI
import json
from typing import List, Dict
client = OpenAI()
def llm_judge(
question: str,
answer: str,
reference: str,
criteria: List[str] = None
) -> dict:
"""Оценка ответа модели через GPT-4"""
if criteria is None:
criteria = ["Correctness", "Relevance", "Coherence", "Completeness"]
criteria_str = "\n".join([f"- {c}:" for c in criteria])
prompt = f"""
Ты — эксперт по оценке качества ответов LLM.
Вопрос: {question}
Ответ модели: {answer}
Эталонный ответ: {reference}
Оцени ответ по критериям (1-5):
{criteria_str}
Верни ответ в формате JSON:
{{
{", ".join([f'"{c.lower()}": <int>' for c in criteria])},
"comments": "<string>"
}}
"""
response = client.chat.completions.create(
model="gpt-4-turbo-preview",
messages=[{"role": "user", "content": prompt}],
response_format={"type": "json_object"},
temperature=0.0
)
return json.loads(response.choices[0].message.content)
# Массовая оценка
def evaluate_model(
model_name: str,
test_set: List[dict],
judge_model: str = "gpt-4-turbo-preview"
) -> dict:
"""Массовая оценка модели через LLM-as-a-Judge"""
results = []
for item in test_set:
# Генерация ответа
answer = generate_answer(model_name, item["question"])
# Оценка через judge
judgment = llm_judge(
item["question"],
answer,
item["reference"]
)
results.append({
"question": item["question"],
"answer": answer,
"judgment": judgment
})
# Агрегация
aggregated = {
criterion: sum(r["judgment"].get(criterion, 0) for r in results) / len(results)
for criterion in ["correctness", "relevance", "coherence", "completeness"]
}
aggregated["total_samples"] = len(results)
return aggregatedfrom fastapi import FastAPI
from pydantic import BaseModel
import json
from datetime import datetime
import logging
app = FastAPI()
# Настройка логирования
logging.basicConfig(
filename="chat_logs.jsonl",
level=logging.INFO,
format='%(message)s'
)
class ChatRequest(BaseModel):
message: str
model: str
user_id: str
class ChatResponse(BaseModel):
response: str
model: str
latency_ms: float
tokens_used: int
cost_usd: float
@app.post("/chat")
async def chat(request: ChatRequest):
import time
start = time.time()
# Выбор модели
if request.model == "model_a":
response, tokens = call_model_a(request.message)
else:
response, tokens = call_model_b(request.message)
latency = (time.time() - start) * 1000
cost = calculate_cost(request.model, tokens)
# Логирование для оценки
log_entry = {
"timestamp": datetime.now().isoformat(),
"model": request.model,
"user_id": request.user_id,
"message": request.message,
"response": response,
"latency_ms": latency,
"tokens_used": tokens,
"cost_usd": cost
}
logging.info(json.dumps(log_entry))
return ChatResponse(
response=response,
model=request.model,
latency_ms=latency,
tokens_used=tokens,
cost_usd=cost
)class FeedbackRequest(BaseModel):
conversation_id: str
rating: int # 1-5
feedback: str = ""
categories: List[str] = [] # "incorrect", "irrelevant", "incomplete"
@app.post("/feedback")
async def submit_feedback(request: FeedbackRequest):
# Сохранение фидбека
with open("feedback.jsonl", "a") as f:
f.write(json.dumps(request.dict()) + "\n")
# Alert на негативный фидбек
if request.rating <= 2:
send_alert(f"Low rating: {request.rating}", request)
return {"status": "ok"}
def send_alert(message: str, context: dict):
"""Отправка alert в Slack/PagerDuty"""
# Integration with Slack/PagerDuty
passimport pandas as pd
import numpy as np
def analyze_production_metrics() -> dict:
"""Анализ production метрик"""
# Загрузка логов
logs = pd.read_json("chat_logs.jsonl", lines=True)
feedback = pd.read_json("feedback.jsonl", lines=True)
# Merge
merged = logs.merge(feedback, on="conversation_id", how="left")
# Метрики по моделям
model_metrics = {}
for model in logs["model"].unique():
model_data = logs[logs["model"] == model]
model_feedback = merged[merged["model"] == model]
model_metrics[model] = {
"total_requests": len(model_data),
"avg_latency_ms": model_data["latency_ms"].mean(),
"p95_latency_ms": model_data["latency_ms"].quantile(0.95),
"p99_latency_ms": model_data["latency_ms"].quantile(0.99),
"avg_tokens": model_data["tokens_used"].mean(),
"total_cost_usd": model_data["cost_usd"].sum(),
"avg_rating": model_feedback["rating"].mean() if "rating" in model_feedback else None,
"error_rate": (model_feedback["rating"] <= 2).mean() if "rating" in model_feedback else None
}
return model_metrics
def print_metrics_report(metrics: dict):
"""Вывод отчёта"""
print("=" * 80)
print("PRODUCTION METRICS REPORT")
print("=" * 80)
for model, data in metrics.items():
print(f"\n{model}:")
print(f" Requests: {data['total_requests']:,}")
print(f" Avg Latency: {data['avg_latency_ms']:.0f}ms (p95: {data['p95_latency_ms']:.0f}ms)")
print(f" Avg Tokens: {data['avg_tokens']:.1f}")
print(f" Total Cost: ${data['total_cost_usd']:.2f}")
if data['avg_rating']:
print(f" Avg Rating: {data['avg_rating']:.2f}/5")
print(f" Error Rate: {data['error_rate']*100:.1f}%"){
"dashboard": {
"title": "LLM Evaluation Dashboard",
"panels": [
{
"title": "Average Rating by Model",
"type": "bargauge",
"targets": [{
"expr": "avg(llm_feedback_rating) by (model)",
"legendFormat": "{{model}}"
}]
},
{
"title": "Latency Distribution",
"type": "histogram",
"targets": [{
"expr": "llm_request_latency_seconds",
"legendFormat": "{{model}}"
}]
},
{
"title": "Error Rate Over Time",
"type": "graph",
"targets": [{
"expr": "rate(llm_feedback_rating{rating<=2}[1h])",
"legendFormat": "Low ratings"
}]
},
{
"title": "Cost per Hour",
"type": "graph",
"targets": [{
"expr": "increase(llm_cost_usd[1h])",
"legendFormat": "{{model}}"
}]
}
]
}
}| Метрика | Для чего | Плюсы | Минусы | Cost |
|---|---|---|---|---|
| Perplexity | General LM quality | Быстро, автоматически | Не измеряет facts | $ |
| BLEU/ROUGE | Translation/Summary | Стандартизировано | Только overlap | $ |
| RAGAS | RAG systems | Comprehensive | Требует контекст | $$ |
| DeepEval | CI/CD integration | pytest-style | Зависит от LLM judge | $$ |
| MMLU | General knowledge | Comprehensive | Дорого, медленно | $$$ |
| HumanEval | Code generation | Functional test | Только для кода | $$$ |
| Human eval | Все задачи | Наиболее точно | Дорого, медленно | $$$$ |
| LLM-as-Judge | Масштабная оценка | Быстро, дёшево | Bias judge model | $$ |
def comprehensive_evaluation(model: str, test_dataset: list) -> dict:
"""Комплексная оценка модели"""
metrics = {
"perplexity": calculate_perplexity_on_dataset(model, test_dataset),
"ragas_scores": evaluate_with_ragas(model, test_dataset),
"mmlu_score": evaluate_mmlu(model),
"human_eval": evaluate_human_eval(model, test_dataset[:20]),
"llm_judge_score": evaluate_with_llm_judge(model, test_dataset)
}
# Weighted score
weights = {
"perplexity": 0.1,
"ragas": 0.3,
"mmlu": 0.2,
"human_eval": 0.25,
"llm_judge": 0.15
}
total_score = sum(
metrics[key] * weight
for key, weight in weights.items()
)
metrics["total_score"] = total_score
return metrics# Создайте test set из реальных запросов пользователей
custom_test_set = [
{
"question": "Как создать FastAPI endpoint?",
"reference": "Используйте @app.get decorator",
"context": ["FastAPI documentation"]
},
{
"question": "Что такое async/await?",
"reference": "Ключевые слова для асинхронного программирования",
"context": ["Python asyncio docs"]
}
]
# Оцените модель на этом set
results = evaluate_on_custom_data(model, custom_test_set)# Track метрики в production
production_metrics = {
# Latency
"latency_p50": ...,
"latency_p95": ...,
"latency_p99": ...,
# Quality
"avg_rating": ...,
"error_rate": ...,
"hallucination_rate": ...,
# Cost
"cost_per_request": ...,
"tokens_per_request": ...,
"daily_cost": ...,
# Usage
"requests_per_day": ...,
"active_users": ...
}# .github/workflows/llm-eval.yml
name: LLM Continuous Evaluation
on:
pull_request:
paths:
- 'prompts/**'
- 'src/llm/**'
- 'models/**'
jobs:
evaluate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install dependencies
run: |
pip install -r requirements.txt
pip install ragas deepeval pytest
- name: Run RAGAS Evaluation
run: |
python scripts/evaluate_ragas.py > eval_results.json
# Parse results and check thresholds
python scripts/check_quality_gates.py --input eval_results.json
if [ $? -ne 0 ]; then
echo "❌ Quality gate failed"
exit 1
fi
echo "✅ Quality gates passed"
- name: Run DeepEval Tests
run: |
pytest test_llm.py -v --tb=short
- name: Upload Evaluation Report
uses: actions/upload-artifact@v4
with:
name: eval-report
path: eval_results.jsonSaaS компания с RAG-системой для технической документации (50K+ страниц).
1. Внедрили RAGAS:
daily_eval = evaluate(
dataset=daily_queries,
metrics=[faithfulness, answer_relevancy, context_precision, context_recall]
)2. Настроили quality gates:
3. Добавили human evaluation:
| Метрика | До | После |
|---|---|---|
| Context Precision | 0.45 | 0.82 |
| Answer Relevancy | 0.52 | 0.78 |
| Faithfulness | 0.61 | 0.85 |
| User Satisfaction | 3.2/5 | 4.5/5 |
| Support Tickets | 120/нед | 45/нед |
ROI: 340% за 6 месяцев
Оценка LLM требует комбинации подходов:
| Этап | Инструменты | Частота |
|---|---|---|
| Development | Perplexity, DeepEval | На каждый commit |
| Pre-deployment | RAGAS, Benchmarks | Перед релизом |
| Production | A/B тесты, метрики | Continuous |
| Quarterly | Human evaluation | Раз в квартал |
Next: Оптимизация inference — batching, caching, speculative decoding.
Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.