LLM評価&テスト は何をしますか?
LLMアプリケーションの評価とテストに関する包括的なガイド。プロンプトテスト、出力検証、幻覚検出、ベンチマーク作成、A/Bテスト、品質指標をカバーします。
クイックリファレンス
このスキルを使用するタイミング:
- LLMアプリケーションの出力テスト
- プロンプトの品質と一貫性の検証
- 幻覚や事実誤りの検出
- 評価ベンチマークの作成
- プロンプトまたはモデルのA/Bテスト
- 継続的評価(CI/CD)の実装
- 検索品質の測定(RAG向け)
- 予期しないLLM動作のデバッグ
カバーする指標:
- 従来型: BLEU、ROUGE、BERTScore、パープレキシティ
- LLM-as-Judge: GPT-4評価、ルーブリックベースのスコアリング
- タスク固有: 完全一致、F1、精度、再現率
- 品質: 有害性、バイアス、首尾一貫性、関連性
パート1: 評価の基礎
LLM評価の難しさ
LLM評価が難しい理由:
- 主観的な品質 - 「良い」出力はユースケースによって異なる
- 単一の正解がない - 複数の正当な回答が存在する
- 文脈依存性 - 同じ出力でもシナリオによって良し悪しが変わる
- ラベル付けにコストがかかる - 人間による評価はスケールしない
- 敵対的な脆弱性 - 小さなプロンプト変更が大きな出力変化を引き起こす
解決策: 多層評価
レイヤー1: 自動指標(高速、スケーラブル)
↓
レイヤー2: LLM-as-Judge(柔軟、ニュアンス対応)
↓
レイヤー3: 人間によるレビュー(ゴールドスタンダード、高コスト)
評価データセットの構造
from dataclasses import dataclass
from typing import List, Optional
@dataclass
class EvalExample:
"""単一の評価サンプル。"""
input: str # ユーザー入力 / プロンプト
expected_output: Optional[str] # ゴールドスタンダード(存在する場合)
context: Optional[str] # 追加コンテキスト(RAG向け)
metadata: dict # カテゴリ、難易度など
@dataclass
class EvalResult:
"""1サンプル分の評価結果。"""
example_id: str
actual_output: str
scores: dict # {'metric_name': score}
passed: bool
failure_reason: Optional[str]
# データセットの例
eval_dataset = [
EvalExample(
input="フランスの首都は?",
expected_output="パリ",
context=None,
metadata={'category': 'factual', 'difficulty': 'easy'}
),
EvalExample(
input="量子もつれを説明してください",
expected_output=None, # 単一の回答はない
context=None,
metadata={'category': 'explanation', 'difficulty': 'hard'}
)
]
パート2: 従来型の指標
指標1: 完全一致(最もシンプル)
def exact_match(predicted: str, expected: str, case_sensitive: bool = False) -> float:
"""
二値指標: 一致すれば1.0、そうでなければ0.0。
使用方法: 分類、短い回答、構造化出力
制限: 生成タスクには厳しすぎる
"""
if not case_sensitive:
predicted = predicted.lower().strip()
expected = expected.lower().strip()
return 1.0 if predicted == expected else 0.0
# 例
score = exact_match("Paris", "paris") # 1.0
score = exact_match("The capital is Paris", "Paris") # 0.0
指標2: ROUGE(再現率志向)
from rouge_score import rouge_scorer
def compute_rouge(predicted: str, expected: str) -> dict:
"""
テキスト重複のROUGE指標。
ROUGE-1: ユニグラムの重複
ROUGE-2: バイグラムの重複
ROUGE-L: 最長共通部分列
使用方法: 要約、翻訳
制限: 意味を捉えられない
"""
scorer = rouge_scorer.RougeScorer(['rouge1', 'rouge2', 'rougeL'], use_stemmer=True)
scores = scorer.score(expected, predicted)
return {
'rouge1': scores['rouge1'].fmeasure,
'rouge2': scores['rouge2'].fmeasure,
'rougeL': scores['rougeL'].fmeasure
}
# 例
scores = compute_rouge(
predicted="Paris is the capital of France",
expected="The capital of France is Paris"
)
# {'rouge1': 0.82, 'rouge2': 0.67, 'rougeL': 0.82}
指標3: BERTScore(意味的類似性)
from bert_score import score as bert_score
def compute_bertscore(predicted: List[str], expected: List[str]) -> dict:
"""
BERT埋め込みを使用した意味的類似性。
ROUGEより優れている点:
- パラフレーズ
- 意味的等価性
- 生成品質
戻り値: Precision、Recall、F1
"""
P, R, F1 = bert_score(predicted, expected, lang="en", verbose=False)
return {
'precision': P.mean().item(),
'recall': R.mean().item(),
'f1': F1.mean().item()
}
# 例
scores = compute_bertscore(
predicted=["The capital of France is Paris"],
expected=["Paris is France's capital city"]
)
# {'precision': 0.94, 'recall': 0.91, 'f1': 0.92}
指標4: パープレキシティ(モデルの信頼度)
import torch
from transformers import GPT2LMHeadModel, GPT2Tokenizer
def compute_perplexity(text: str, model_name: str = "gpt2") -> float:
"""
パープレキシティ: モデルがこのテキストにどれだけ「驚く」か?
低い = より可能性が高い/流暢
使用方法: 流暢性、自然さ
制限: 正確性を測定しない
"""
model = GPT2LMHeadModel.from_pretrained(model_name)
tokenizer = GPT2Tokenizer.from_pretrained(model_name)
inputs = tokenizer(text, return_tensors="pt")
with torch.no_grad():
outputs = model(**inputs, labels=inputs["input_ids"])
loss = outputs.loss
perplexity = torch.exp(loss).item()
return perplexity
# 例
ppl = compute_perplexity("Paris is the capital of France") # 低い(流暢)
ppl2 = compute_perplexity("Capital France the is Paris of") # 高い(不自然)
パート3: LLM-as-Judge評価
パターン1: ルーブリックベースのスコアリング
from openai import OpenAI
client = OpenAI()
EVALUATION_PROMPT = """
あなたは専門の評価者です。各基準について、アシスタントの応答を1~5のスケールで採点してください。
**基準:**
1. **正確性**: 情報は事実に基づいていますか?
2. **完全性**: 質問に完全に答えていますか?
3. **明確さ**: 理解しやすいですか?
4. **簡潔さ**: 適切に簡潔ですか?
**評価する応答:**
{response}
**期待される回答(参考):**
{expected}
JSON形式でスコアを提供してください:
{{
"accuracy": <1-5>,
"completeness": <1-5>,
"clarity": <1-5>,
"conciseness": <1-5>,
"reasoning": "簡単な説明"
}}
"""
def llm_judge_score(response: str, expected: str) -> dict:
"""
GPT-4を審査員としてルーブリックスコアリングに使用。
利点: 柔軟、ニュアンス対応、スケール良好
欠点: コストがかかる、バイアスの可能性、低速
"""
prompt = EVALUATION_PROMPT.format(response=response, expected=expected)
completion = client.chat.completions.create(
model="gpt-4",
messages=[{"role": "user", "content": prompt}],
response_format={"type": "json_object"}
)
import json
scores = json.loads(completion.choices[0].message.content)
return scores
# 例
scores = llm_judge_score(
response="パリはフランスの首都で、国の中北部に位置しています。",
expected="パリ"
)
# {'accuracy': 5, 'completeness': 5, 'clarity': 5, 'conciseness': 3, 'reasoning': '...'}
パターン2: 二値の合格/不合格評価
PASS_FAIL_PROMPT = """
アシスタントの応答が許容可能かどうかを評価してください。
**質問:** {question}
**応答:** {response}
**基準:** {criteria}
"PASS" または "FAIL" のみを返し、その後に一文で理由を記述してください。
"""
def binary_eval(question: str, response: str, criteria: str) -> tuple[bool, str]:
"""
シンプルな合格/不合格評価。
使用方法: 単体テスト、回帰テスト、CI/CD
"""
prompt = PASS_FAIL_PROMPT.format(
question=question,
response=response,
criteria=criteria
)
completion = client.chat.completions.create(
model="gpt-4",
messages=[{"role": "user", "content": prompt}],
temperature=0.0 # 決定論的
)
result = completion.choices[0].message.content
passed = result.startswith("PASS")
reason = result.split(":", 1)[1].strip() if ":" in result else result
return passed, reason
# 例
passed, reason = binary_eval(
question="フランスの首都は?",
response="首都はパリです",
criteria="応答にパリが含まれている必要があります"
)
# (True, "応答はパリを首都として正しく特定しています")
パターン3: ペアワイズ比較(A/Bテスト)
PAIRWISE_PROMPT = """
同じ質問に対する2つの応答を比較してください。どちらが優れていますか?
**質問:** {question}
**応答A:**
{response_a}
**応答B:**
{response_b}
**基準:** {criteria}
"A"、"B"、または "TIE" のみを返し、その後に一文で説明を記述してください。
"""
def pairwise_comparison(
question: str,
response_a: str,
response_b: str,
criteria: str = "全体的な品質、正確性、有用性"
) -> tuple[str, str]:
"""
2つの応答をA/Bテストします。
使用方法: プロンプトエンジニアリング、モデル比較
"""
prompt = PAIRWISE_PROMPT.format(
question=question,
response_a=response_a,
response_b=response_b,
criteria=criteria
)
completion = client.chat.completions.create(
model="gpt-4",
messages=[{"role": "user", "content": prompt}],
temperature=0.0
)
result = completion.choices[0].message.content
winner = result.split()[0] # "A"、"B"、または "TIE"
reason = result.split(":", 1)[1].strip() if ":" in result else result
return winner, reason
# 例
winner, reason = pairwise_comparison(
question="量子コンピューティングを説明してください",
response_a="量子コンピュータはビットの代わりに量子ビットを使用します...",
response_b="量子コンピューティングは複雑です。量子力学を使用します。"
)
# ("A", "応答Aはより詳細で説明が充実しています")
パート4: 幻覚検出
方法1: グラウンディングチェック
def check_grounding(response: str, context: str) -> dict:
"""
応答が提供されたコンテキストに基づいていることを確認。
RAGシステムでは重要。
"""
GROUNDING_PROMPT = """
コンテキスト: {context}
応答: {response}
応答はコンテキストによって完全にサポートされていますか?次のいずれかで回答してください:
- "GROUNDED": すべての主張がサポートされている
- "PARTIALLY_GROUNDED": 一部の主張がサポートされていない
- "NOT_GROUNDED": サポートされていない主張が含まれている
サポートされていない主張をリストアップしてください。
"""
prompt = GROUNDING_PROMPT.format(context=context, response=response)
completion = client.chat.completions.create(
model="gpt-4",
messages=[{"role": "user", "content": prompt}]
)
result = completion.choices[0].message.content
status = result.split("\n")[0]
unsupported = [line for line in result.split("\n")[1:] if line.strip()]
return {
'grounding_status': status,
'unsupported_claims': unsupported,
'is_hallucination': status != "GROUNDED"
}
方法2: 事実性チェック(外部検証)
def check_factuality(claim: str, use_search: bool = True) -> dict:
"""
外部ソースを使用して事実に基づく主張を検証。
オプション:
1. Web検索 + 検証
2. 知識ベース検索
3. 信頼できるソースとの相互参照
"""
if use_search:
# Web検索を使用して検証
from tavily import TavilyClient
tavily = TavilyClient(api_key="your-key")
# 証拠を検索
results = tavily.search(claim, max_results=3)
# LLMに検索結果に基づいて検証させる
VERIFY_PROMPT = """
主張: {claim}
検索結果:
{results}
この主張はこれらのソースによってサポートされていますか?TRUE、FALSE、またはUNCERTAINで回答してください。
説明:
"""
prompt = VERIFY_PROMPT.format(
claim=claim,
results="\n\n".join([r['content'] for r in results])
)
completion = client.chat.completions.create(
model="gpt-4",
messages=[{"role": "user", "content": prompt}]
)
result = completion.choices[0].message.content
is_factual = result.startswith("TRUE")
return {
'claim': claim,
'factual': is_factual,
'evidence': results,
'explanation': result
}
方法3: 自己整合性チェック
def self_consistency_check(question: str, num_samples: int = 5) -> dict:
"""
複数の応答を生成し、一貫性をチェック。
モデルが確信している場合、応答は一貫しているはず。
一貫性の欠如は幻覚リスクを示唆。
"""
responses = []
for _ in range(num_samples):
completion = client.chat.completions.create(
model="gpt-4",
messages=[{"role": "user", "content": question}],
temperature=0.7 # ある程度のランダム性
)
responses.append(completion.choices[0].message.content)
# ペアワイズ類似度を計算
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
vectorizer = TfidfVectorizer()
vectors = vectorizer.fit_transform(responses)
similarities = cosine_similarity(vectors)
# 平均ペアワイズ類似度
avg_similarity = similarities.sum() / (len(responses) * (len(responses) - 1))
return {
'responses': responses,
'avg_similarity': avg_similarity,
'is_consistent': avg_similarity > 0.7, # しきい値
'confidence': 'high' if avg_similarity > 0.85 else 'medium' if avg_similarity > 0.7 else 'low'
}
パート5: RAG固有の評価
検索品質指標
def evaluate_retrieval(query: str, retrieved_docs: List[dict], relevant_doc_ids: List[str]) -> dict:
"""
IR指標を使用して検索品質を評価。
Precision: 取得されたドキュメントのうち、関連するものの割合は?
Recall: 関連するドキュメントのうち、取得されたものの割合は?
MRR: 平均逆順位
NDCG: 正規化割引累積利得
"""
retrieved_ids = [doc['id']
...