mcpField
L
llm-evaluation

llm-evaluation

github.com/sickn33テスト・品質48.8k215 回インストール今日更新
リポジトリ

llm-evaluation について

llm-evaluationは、自動メトリクス、人手評価、LLMによる判定、実験を扱うLLMアプリ評価スキルです。モデルやプロンプトの比較、回帰の追跡、検証とデータ安全への配慮を踏まえた結果の解釈に役立ちます。

主な機能

  1. 01テキスト・分類・検索の自動メトリクス
  2. 02人手評価の観点と評価者間一致
  3. 03LLM判定のスコアリング手法
  4. 04A/Bテストと回帰テストの指針
  5. 05ベンチマークと評価の実践

活用シーン

  1. 01モデルやプロンプト案の比較
  2. 02デプロイ前の性能回帰チェック
  3. 03予期しないモデル動作の調査
  4. 04評価ベースラインの継続的な追跡

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/sickn33/agentic-awesome-skills --skill llm-evaluation

インストール後、Claude Code で/llm-evaluationを入力して読み込まれたか確認します。

同梱ファイルreferences/benchmarking.md

詳細

LLM 評価

自動評価指標から人手評価、A/B テストまで、LLM アプリケーションの包括的な評価戦略を習得します。

このスキルを使用しない場合

  • タスクが LLM 評価に関係しない場合
  • この範囲外の別のドメインやツールが必要な場合

手順

  • 目標、制約、必要な入力を明確にします。
  • 関連するベストプラクティスを適用し、結果を検証します。
  • 実行可能な手順と検証方法を提示します。
  • 以下の例を使用します。このスキルには、別個の実装手順書は含まれていません。

このスキルを使用する場合

  • LLM アプリケーションのパフォーマンスを体系的に測定する
  • 異なるモデルやプロンプトを比較する
  • デプロイ前にパフォーマンスの低下を検出する
  • プロンプト変更による改善を検証する
  • 本番システムへの信頼性を高める
  • ベースラインを確立し、経時的な進捗を追跡する
  • 予期しないモデルの動作をデバッグする

主な評価の種類

1. 自動評価指標

計算したスコアを用いて、高速かつ反復可能でスケーラブルな評価を行います。

テキスト生成:

  • BLEU:n-gram の重複(翻訳)
  • ROUGE:再現率重視(要約)
  • METEOR:意味的類似度
  • BERTScore:埋め込みベースの類似度
  • Perplexity:言語モデルの確信度

分類:

  • Accuracy:正解率
  • Precision/Recall/F1:クラスごとのパフォーマンス
  • Confusion Matrix:エラーパターン
  • AUC-ROC:ランキング品質

検索(RAG):

  • MRR:平均逆順位
  • NDCG:正規化割引累積利得
  • Precision@K:上位 K 件に含まれる関連項目の割合
  • Recall@K:上位 K 件での網羅率
2. 人手評価

自動化が難しい品質面を手動で評価します。

評価の観点:

  • Accuracy:事実の正確さ
  • Coherence:論理的な流れ
  • Relevance:質問に答えているか
  • Fluency:自然な言語表現の品質
  • Safety:有害な内容がないこと
  • Helpfulness:ユーザーにとって役立つこと
3. LLM-as-Judge

より強力な LLM を使用して、より弱いモデルの出力を評価します。

アプローチ:

  • Pointwise:個々の回答を採点する
  • Pairwise:2 つの回答を比較する
  • Reference-based:正解データと比較する
  • Reference-free:正解データなしで判定する

クイックスタート

from llm_eval import EvaluationSuite, Metric

# Define evaluation suite
suite = EvaluationSuite([
    Metric.accuracy(),
    Metric.bleu(),
    Metric.bertscore(),
    Metric.custom(name="groundedness", fn=check_groundedness)
])

# Prepare test cases
test_cases = [
    {
        "input": "What is the capital of France?",
        "expected": "Paris",
        "context": "France is a country in Europe. Paris is its capital."
    },
    # ... more test cases
]

# Run evaluation
results = suite.evaluate(
    model=your_model,
    test_cases=test_cases
)

print(f"Overall Accuracy: {results.metrics['accuracy']}")
print(f"BLEU Score: {results.metrics['bleu']}")

自動評価指標の実装

BLEU スコア
from nltk.translate.bleu_score import sentence_bleu, SmoothingFunction

def calculate_bleu(reference, hypothesis):
    """Calculate BLEU score between reference and hypothesis."""
    smoothie = SmoothingFunction().method4

    return sentence_bleu(
        [reference.split()],
        hypothesis.split(),
        smoothing_function=smoothie
    )

# Usage
bleu = calculate_bleu(
    reference="The cat sat on the mat",
    hypothesis="A cat is sitting on the mat"
)
ROUGE スコア
from rouge_score import rouge_scorer

def calculate_rouge(reference, hypothesis):
    """Calculate ROUGE scores."""
    scorer = rouge_scorer.RougeScorer(['rouge1', 'rouge2', 'rougeL'], use_stemmer=True)
    scores = scorer.score(reference, hypothesis)

    return {
        'rouge1': scores['rouge1'].fmeasure,
        'rouge2': scores['rouge2'].fmeasure,
        'rougeL': scores['rougeL'].fmeasure
    }
BERTScore
from bert_score import score

def calculate_bertscore(references, hypotheses):
    """Calculate BERTScore using pre-trained BERT."""
    P, R, F1 = score(
        hypotheses,
        references,
        lang='en',
        model_type='microsoft/deberta-xlarge-mnli'
    )

    return {
        'precision': P.mean().item(),
        'recall': R.mean().item(),
        'f1': F1.mean().item()
    }
カスタム評価指標
def calculate_groundedness(response, context):
    """Check if response is grounded in provided context."""
    # Use NLI model to check entailment
    from transformers import pipeline

    nli = pipeline("text-classification", model="microsoft/deberta-large-mnli")

    result = nli(f"{context} [SEP] {response}")[0]

    # Return confidence that response is entailed by context
    return result['score'] if result['label'] == 'ENTAILMENT' else 0.0

def calculate_toxicity(text):
    """Measure toxicity in generated text."""
    from detoxify import Detoxify

    results = Detoxify('original').predict(text)
    return max(results.values())  # Return highest toxicity score

def calculate_factuality(claim, knowledge_base):
    """Verify factual claims against knowledge base."""
    # Implementation depends on your knowledge base
    # Could use retrieval + NLI, or fact-checking API
    raise NotImplementedError("A verified fact-checking adapter is required")

LLM-as-Judge のパターン

単一出力の評価
def llm_judge_quality(response, question):
    """Use GPT-5 to judge response quality."""
    prompt = f"""Rate the following response on a scale of 1-10 for:
1. Accuracy (factually correct)
2. Helpfulness (answers the question)
3. Clarity (well-written and understandable)

Question: {question}
Response: {response}

Provide ratings in JSON format:
{{
  "accuracy": <1-10>,
  "helpfulness": <1-10>,
  "clarity": <1-10>,
  "reasoning": "<brief explanation>"
}}
"""

    result = openai.ChatCompletion.create(
        model="gpt-5",
        messages=[{"role": "user", "content": prompt}],
        temperature=0
    )

    return json.loads(result.choices[0].message.content)
ペアワイズ比較
def compare_responses(question, response_a, response_b):
    """Compare two responses using LLM judge."""
    prompt = f"""Compare these two responses to the question and determine which is better.

Question: {question}

Response A: {response_a}

Response B: {response_b}

Which response is better and why? Consider accuracy, helpfulness, and clarity.

Answer with JSON:
{{
  "winner": "A" or "B" or "tie",
  "reasoning": "<explanation>",
  "confidence": <1-10>
}}
"""

    result = openai.ChatCompletion.create(
        model="gpt-5",
        messages=[{"role": "user", "content": prompt}],
        temperature=0
    )

    return json.loads(result.choices[0].message.content)

人手評価のフレームワーク

アノテーションのガイドライン
class AnnotationTask:
    """Structure for human annotation task."""

    def __init__(self, response, question, context=None):
        self.response = response
        self.question = question
        self.context = context

    def get_annotation_form(self):
        return {
            "question": self.question,
            "context": self.context,
            "response": self.response,
            "ratings": {
                "accuracy": {
                    "scale": "1-5",
                    "description": "Is the response factually correct?"
                },
                "relevance": {
                    "scale": "1-5",
                    "description": "Does it answer the question?"
                },
                "coherence": {
                    "scale": "1-5",
                    "description": "Is it logically consistent?"
                }
            },
            "issues": {
                "factual_error": False,
                "hallucination": False,
                "off_topic": False,
                "unsafe_content": False
            },
            "feedback": ""
        }
評価者間の一致度
from sklearn.metrics import cohen_kappa_score

def calculate_agreement(rater1_scores, rater2_scores):
    """Calculate inter-rater agreement."""
    kappa = cohen_kappa_score(rater1_scores, rater2_scores)

    # Ordered ranges: Boolean dictionary keys overwrite each other.
    interpretation = "Undefined"
    for upper, label in [(0, "Poor"), (0.2, "Slight"), (0.4, "Fair"),
                         (0.6, "Moderate"), (0.8, "Substantial"),
                         (float("inf"), "Almost Perfect")]:
        if kappa < upper:
            interpretation = label
            break

    return {
        "kappa": kappa,
        "interpretation": interpretation
    }

A/B テスト

統計的検定のフレームワーク
from scipy import stats
import numpy as np

class ABTest:
    def __init__(self, variant_a_name="A", variant_b_name="B"):
        self.variant_a = {"name": variant_a_name, "scores": []}
        self.variant_b = {"name": variant_b_name, "scores": []}

    def add_result(self, variant, score):
        """Add evaluation result for a variant."""
        if variant == "A":
            self.variant_a["scores"].append(score)
        else:
            self.variant_b["scores"].append(score)

    def analyze(self, alpha=0.05):
        """Perform statistical analysis."""
        a_scores = self.variant_a["scores"]
        b_scores = self.variant_b["scores"]

        if len(a_scores) < 2 or len(b_scores) < 2:
            raise ValueError("At least two observations per independent group are required")
        if not np.all(np.isfinite(a_scores)) or not np.all(np.isfinite(b_scores)):
            raise ValueError("Require finite observations")
        # Illustrative independent-group analysis; use a paired design for matched tasks.
        # T-test
        t_stat, p_value = stats.ttest_ind(a_scores, b_scores)

        # Effect size (Cohen's d)
        pooled_std = np.sqrt((np.std(a_scores)**2 + np.std(b_scores)**2) / 2)
        cohens_d = (np.mean(b_scores) - np.mean(a_scores)) / pooled_std if pooled_std > 0 else None

        return {
            "variant_a_mean": np.mean(a_scores),
            "variant_b_mean": np.mean(b_scores),
            "difference": np.mean(b_scores) - np.mean(a_scores),
            "relative_improvement": ((np.mean(b_scores) - np.mean(a_scores)) / np.mean(a_scores)
                                     if np.mean(a_scores) != 0 else None),
            "p_value": p_value,
            "statistically_significant": p_value < alpha,
            "cohens_d": cohens_d,
            "effect_size": self.interpret_cohens_d(cohens_d) if cohens_d is not None else "undefined",
            "winner": (("B" if np.mean(b_scores) > np.mean(a_scores) else "A")
                       if np.isfinite(p_value) and p_value < alpha and np.mean(b_scores) != np.mean(a_scores)
                       else "inconclusive")
        }

    @staticmethod
    def interpret_cohens_d(d):
        """Interpret Cohen's d effect size."""
        abs_d = abs(d)
        if abs_d < 0.2:
            return "negligible"
        elif abs_d < 0.5:
            return "small"
        elif abs_d < 0.8:
            return "medium"
        else:
            return "large"

回帰テスト

回帰の検出
class RegressionDetector:
    def __init__(self, baseline_results, threshold=0.05, directions=None):
        import math
        if not baseline_results or not math.isfinite(threshold) or threshold < 0:
            raise ValueError("Require baseline metrics and a finite nonnegative threshold")
        self.baseline = baseline_results
        self.threshold = threshold
        self.directions = directions or {}

    def check_for_regression(self, new_results):
        """Detect if new results show regression."""
        import math
        regressions = []
        missing_metrics = []

        for metric in self.baseline.keys():
            baseline_score = self.baseline[metric]
            new_score = new_results.get(metric)

            if new_score is None:
                missing_metrics.append(metric)
                continue
            if not math.isfinite(baseline_score) or not math.isfinite(new_score):
                raise ValueError("Metrics must be finite")

            # Calculate relative change
            if baseline_score <= 0:
                raise ValueError("Relative regression requires a positive nonzero baseline; use an absolute threshold instead.")
            relative_change = (new_score - baseline_score) / baseline_score

            # Default is higher-is-better; explicitly mark latency/cost as lower.
            direction = self.directions.get(metric, "higher")
            if direction not in {"higher", "lower"}:
                raise ValueError("Unknown metric direction")
            is_regression = (relative_change < -self.threshold if direction == "higher"
                             else relative_change > self.threshold)
            if is_regression:
                regressions.append({
                    "metric": metric,
                    "baseline": baseline_score,
                    "current": new_score,
                    "change": relative_change
                })

        return {
            "has_regression": True if regressions else (None if missing_metrics else False),
            "analysis_complete": not missing_metrics,
            "missing_metrics": missing_metrics,
            "regressions": regressions
        }

ベンチマーク

制約付きランナー、アダプターの前提条件、不完全なデータの扱いについては、ローカルベンチマークの例を参照してください。

ローカルリソースと実行上の境界

ローカルベンチマークのリファレンスには、説明用のランナーが含まれています。インストール済みの評価フレームワークやデータセットは含まれていません。以下のコードは、プロジェクトの実際の依存関係に合わせて調整し、使用前に検証する必要があるパターンを示しています。llm_eval インターフェースは擬似コードであり、インストール済みのパッケージが提供されていることを保証するものではありません。古いプロバイダー呼び出しをそのままコピーするのではなく、既存の SDK バージョンを確認してください。

サニタイズ済みで代表性のあるデータと、明示的な評価予算を使用してください。モデルのダウンロード、ホスト型推論、有料の判定呼び出し、テレメトリーのエクスポートには、データの利用と費用に関する承認が必要です。すでにキャッシュされているモデルを優先してください。プロンプトが、評価対象の出力に埋め込まれた指示に対するセキュリティ境界になると考えてはいけません。判定スコアを人手でラベル付けしたサンプルに照らして調整し、エラー、バイアス、不一致を確認してください。重複度のスコアだけでは、正確性、安全性、本番環境への準備が整っていることは証明できません。

ベストプラクティス

  1. 複数の指標:包括的に把握するため、多様な指標を使用します
  2. 代表的なデータ:現実世界の多様な例でテストします
  3. ベースライン:常にベースラインのパフォーマンスと比較します
  4. 統計的な厳密性:比較には適切な統計検定を使用します
  5. 継続的な評価:CI/CD パイプラインに組み込みます
  6. 人による検証:自動化された指標と人の判断を組み合わせます
  7. エラー分析:弱点を理解するため、失敗を調査します
  8. バージョン管理:評価結果を時系列で追跡します

よくある落とし穴

  • 単一指標への固執:ほかの指標を犠牲にして、1つの指標だけを最適化すること
  • サンプル数の不足:例が少なすぎる状態で結論を出すこと
  • データ汚染:トレーニングデータでテストすること
  • ばらつきの無視:統計的不確実性を考慮しないこと
  • 指標の不一致:ビジネス目標と合致しない指標を使用すること

制限事項

  • 上記で説明した範囲にタスクが明確に合致する場合にのみ、このスキルを使用してください。
  • 出力を、環境固有の検証、テスト、または専門家によるレビューの代わりとして扱わないでください。
  • 必要な入力、権限、安全上の境界、または成功基準が不足している場合は、作業を中止して確認を求めてください。

コメント