Claude Media
Claudeアプリの成功基準を測る4つのeval実装

Claudeアプリの成功基準を測る4つのeval実装

Claudeを使ったアプリの成功基準を、exact match・コサイン類似度・ROUGE-L・LLM Likertスケールの4手法で定量評価する実装をPythonで解説します。

Claudeアプリの成功基準を4つの型で定量化する

「感情分類の精度が良い」「要約が的確」「サポート対応の口調が丁寧」といった目標は、そのままでは測れません。公式ドキュメントは、成功基準を測定可能な形に落とし込む具体的なeval実装を、タスクの性質ごとに使い分けるべきだと示しています。本記事では、①分類タスクのexact match ②言い換え耐性が要るタスクのコサイン類似度 ③要約タスクのROUGE-L ④トーンのようなニュアンスを測るLLM Likertスケール、の4手法を実装コード付きで見ていきます。どれも「1000件のツイートで感情分類の精度を測る」のようなタスク単位の評価に使う、再利用可能な採点関数です。

成功基準はSMART原則で先に言語化する

eval実装に入る前に、成功基準そのものを測定可能な文にしておく必要があります。「モデルはうまく感情分類できるべき」は測れません。「保持データセット1万件のツイートに対しF1スコア0.85以上、現行ベースラインから5%改善」のように、具体的(Specific)・測定可能(Measurable)・達成可能(Achievable)・関連性がある(Relevant)・期限がある(Time-bound)の観点で書き直します。この言語化が終わって初めて、以下の4手法のどれを使うかが決まります。

1. exact match — 分類タスクの精度を測る

向くタスク: 感情分析のように、正解が「positive」「negative」「neutral」「mixed」のようなカテゴリカルな値に定まるタスク。

測るもの: モデルの出力が、空白・大文字小文字を正規化したうえで正解ラベルと完全一致するかどうか。単純で曖昧さが無いため、カテゴリカルな正解があるタスクに最適です。

tweets = [
    {"text": "This movie was a total waste of time. 👎", "sentiment": "negative"},
    {"text": "The new album is 🔥! Been on repeat all day.", "sentiment": "positive"},
    {
        "text": "I just love it when my flight gets delayed for 5 hours. #bestdayever",
        "sentiment": "negative",
    },  # エッジケース: 皮肉
    {
        "text": "The movie's plot was terrible, but the acting was phenomenal.",
        "sentiment": "mixed",
    },  # エッジケース: 感情が混在
    # ... 残り996件
]
 
client = anthropic.Anthropic()
 
 
def get_completion(prompt: str):
    message = client.messages.create(
        model="claude-opus-5",
        max_tokens=50,
        messages=[{"role": "user", "content": prompt}],
    )
    return next(block.text for block in message.content if block.type == "text")
 
 
def evaluate_exact_match(model_output, correct_answer):
    return model_output.strip().lower() == correct_answer.lower()
 
 
outputs = [
    get_completion(
        f"Classify this as 'positive', 'negative', 'neutral', or 'mixed': {tweet['text']}"
    )
    for tweet in tweets
]
accuracy = sum(
    evaluate_exact_match(output, tweet["sentiment"])
    for output, tweet in zip(outputs, tweets)
) / len(tweets)
print(f"Sentiment Analysis Accuracy: {accuracy * 100}%")

テストケースには皮肉や感情の混在のようなエッジケースを必ず混ぜます。曖昧なケースを避けたテストセットは、実運用での精度を過大評価します。

2. コサイン類似度 — 言い換えへの一貫性を測る

向くタスク: FAQボットのように、同じ質問を別の言葉で聞いても意味的に同じ答えが返ってくるべきタスク。

測るもの: モデル出力をSentence-BERT(SBERT)で文埋め込みに変換し、ベクトル間の角度の余弦(コサイン)を計算します。値が1に近いほど類似度が高いことを示します。言い換えで表現が変わっても意味的な一貫性を測れる点が、文字列一致にはない利点です。

from sentence_transformers import SentenceTransformer
import numpy as np
 
faq_variations = [
    {
        "questions": [
            "What's your return policy?",
            "How can I return an item?",
            "Wut's yur retrn polcy?",
        ],
        "answer": "Our return policy allows...",
    },  # エッジケース: タイプミス
    {
        "questions": [
            "I bought something last week, and it's not really what I expected, "
            "so I was wondering if maybe I could possibly return it?",
            "I read online that your policy is 30 days but that seems out of date "
            "because the website was updated six months ago, so what's your current policy?",
        ],
        "answer": "Our return policy allows...",
    },  # エッジケース: 長く回りくどい質問
    # ... 残り47件のFAQ
]
 
client = anthropic.Anthropic()
 
 
def get_completion(prompt: str):
    message = client.messages.create(
        model="claude-opus-5",
        max_tokens=2048,
        messages=[{"role": "user", "content": prompt}],
    )
    return next(block.text for block in message.content if block.type == "text")
 
 
def evaluate_cosine_similarity(outputs):
    model = SentenceTransformer("all-MiniLM-L6-v2")
    embeddings = model.encode(outputs)
 
    norms = np.linalg.norm(embeddings, axis=1)
    cosine_similarities = np.dot(embeddings, embeddings.T) / np.outer(norms, norms)
    return np.mean(cosine_similarities)
 
 
for faq in faq_variations:
    outputs = [get_completion(question) for question in faq["questions"]]
    similarity_score = evaluate_cosine_similarity(outputs)
    print(f"FAQ Consistency Score: {similarity_score * 100}%")

TypeScriptでも同じ手法が使えます。Pythonのsentence-transformersに相当するのが@huggingface/transformersfeature-extractionパイプラインです。C#・Go・Java・PHP・Rubyには文埋め込みのネイティブライブラリが無いため、公式ドキュメントもPythonかTypeScriptでの実装を勧めています。

3. ROUGE-L — 要約の質を測る

向くタスク: 記事の要約のように、参照要約に対してどれだけ要点を捉えられているかを測りたいタスク。

測るもの: ROUGE-L(Recall-Oriented Understudy for Gisting Evaluation - Longest Common Subsequence)は、生成された要約と参照要約の間の最長共通部分列の長さを測定します。スコアが高いほど、要約が参照要約の要点を一貫した順序で捉えていることを示します。

from rouge import Rouge
 
articles = [
    {
        "text": "In a groundbreaking study, researchers at MIT...",
        "summary": "MIT scientists discover a new antibiotic...",
    },
    {
        "text": "Jane Doe, a local hero, made headlines last week for saving... "
        "In city hall news, the budget... Meteorologists predict...",
        "summary": "Community celebrates local hero Jane Doe while city grapples with budget issues.",
    },  # エッジケース: 複数トピックが混在
    # ... 残り197件の記事
]
 
client = anthropic.Anthropic()
 
 
def get_completion(prompt: str):
    message = client.messages.create(
        model="claude-opus-5",
        max_tokens=1024,
        messages=[{"role": "user", "content": prompt}],
    )
    return next(block.text for block in message.content if block.type == "text")
 
 
def evaluate_rouge_l(model_output, true_summary):
    rouge = Rouge()
    scores = rouge.get_scores(model_output, true_summary)
    return scores[0]["rouge-l"]["f"]  # ROUGE-L F1スコア
 
 
outputs = [
    get_completion(f"Summarize this article in 1-2 sentences:\n\n{article['text']}")
    for article in articles
]
relevance_scores = [
    evaluate_rouge_l(output, article["summary"])
    for output, article in zip(outputs, articles)
]
print(f"Average ROUGE-L F1 Score: {sum(relevance_scores) / len(relevance_scores)}")

複数トピックが混在する記事や、見出しが本文とずれている記事をテストケースに含めると、要約が本筋を外していないかを検出しやすくなります。

4. LLM Likertスケール — トーンのようなニュアンスを測る

向くタスク: カスタマーサポートの応対のように、共感度・専門性・忍耐強さといった主観的な性質を測りたいタスク。

測るもの: 別のLLMに1〜5のリッカート尺度で応答を採点させる手法です。従来型の指標では定量化が難しい、口調やニュアンスの評価に向いています。採点には評価対象を生成したモデルとは別のモデルを使うのが基本です。

inquiries = [
    {
        "text": "This is the third time you've messed up my order. I want a refund NOW!",
        "tone": "empathetic",
    },  # エッジケース: 怒っている顧客
    {
        "text": "I tried resetting my password but then my account got locked...",
        "tone": "patient",
    },  # エッジケース: 複雑な問い合わせ
    # ... 残り97件の問い合わせ
]
 
client = anthropic.Anthropic()
 
 
def get_completion(prompt: str):
    message = client.messages.create(
        model="claude-opus-5",
        max_tokens=2048,
        messages=[{"role": "user", "content": prompt}],
    )
    return next(block.text for block in message.content if block.type == "text")
 
 
def evaluate_likert(model_output, target_tone):
    tone_prompt = f"""Rate this customer service response on a scale of 1-5 for being {target_tone}:
    <response>{model_output}</response>
    1: Not at all {target_tone}
    5: Perfectly {target_tone}
    Output only the number."""
 
    # 採点には生成に使ったモデルとは別のモデルを使うのが基本
    response = client.messages.create(
        model="claude-opus-5",
        max_tokens=50,
        messages=[{"role": "user", "content": tone_prompt}],
    )
    return int(
        next(block.text for block in response.content if block.type == "text").strip()
    )
 
 
outputs = [
    get_completion(f"Respond to this customer inquiry: {inquiry['text']}")
    for inquiry in inquiries
]
tone_scores = [
    evaluate_likert(output, inquiry["tone"])
    for output, inquiry in zip(outputs, inquiries)
]
print(f"Average Tone Score: {sum(tone_scores) / len(tone_scores)}")

4手法の使い分け早見表

タスクの性質手法判定の速さ・コスト
正解がカテゴリカルに定まる手法exact match判定の速さ・コスト最速・無料(コード判定)
言い換えへの意味的一貫性手法コサイン類似度判定の速さ・コスト速い・埋め込みモデルのローカル実行
要約の要点網羅と順序手法ROUGE-L判定の速さ・コスト速い・軽量ライブラリで計算
トーン・共感度等の主観的な質手法LLM Likertスケール判定の速さ・コスト遅め・別モデルの呼び出しコストがかかる

多次元の成功基準を1つのタスクに持たせる

実際のタスクは、1つの指標だけで成功・失敗を判定できないことがほとんどです。感情分析であれば、F1スコアだけでなく、有害な出力の割合、致命的でないエラーの許容比率、応答時間まで含めて基準を組みます。公式ドキュメントが示す多次元の例では、保持データセット1万件のツイートに対して、F1スコア0.85以上・非有害な出力99.5%以上・エラーの90%が些細なもの(重大な誤りでない)・応答時間の95%が200ミリ秒未満、という4指標を同時に満たすことを成功基準としています。exact matchはこの4指標のうちF1スコアの算出に使う土台であり、残りの指標は別の集計ロジックで測ります。1つのタスクに複数のeval手法を組み合わせて、多次元の基準をカバーするのが実務での標準的な構成です。

採点方式そのものの選び方

4手法はどれも「グレーディング(採点)」の実装です。公式ドキュメントは、採点方式を選ぶ優先順位を明確にしています。最速かつ最も信頼できるのはコードベースの採点(exact match・文字列一致)で、スケーラビリティも高い一方、複雑な判断が必要なタスクには向きません。人手による採点は最も柔軟で高品質ですが遅く高コストなため、可能な限り避けます。LLMベースの採点は速度と柔軟性を両立し、複雑な判断が必要な場面に向きますが、信頼性を先に検証してからスケールさせる必要があります。ROUGE-Lとコサイン類似度はコードベース採点の一種、LLM Likertスケールはこの3番目に当たります。

LLMに採点させる場合は、次の3点が精度を左右します。①ルーブリックを詳細かつ明確にする(「回答は必ず1文目で社名に言及する。していなければ自動的に不正解」のように)②出力形式を絞る(「correct」「incorrect」の2値か1〜5の尺度で出させ、自由記述の定性評価にしない)③採点前に理由づけをさせてから結論を出させる(<thinking>タグで理由を書かせたあとに<result>タグで結論だけを出させ、理由づけ自体は破棄する)。この3点目は、特に複雑な判断が要るタスクで採点性能を底上げすると公式ドキュメントが説明しています。

よくある質問

ROUGE-Lとコサイン類似度はどちらも要約評価に使えるか

どちらも使えますが、測っている性質が異なります。ROUGE-Lは参照要約との語順を含めた重なりを見るため、決まった要点を漏らさず拾えているかの判定に向きます。コサイン類似度は語順を無視して意味的な近さだけを見るため、参照要約と言い回しが大きく違っていても内容が同じなら高いスコアが出ます。要点の網羅性を厳密に見たいならROUGE-L、表現の自由度を許容しつつ意味の一致だけを見たいならコサイン類似度、という使い分けになります。

LLM Likertスケールは自分自身での採点でもよいか

公式ドキュメントは、評価対象の出力を生成したモデルとは別のモデルで採点するのが基本だと説明しています。同じモデルに生成と採点の両方をさせると、自分の出力の癖を甘く評価しやすいバイアスが入り込む可能性があるためです。

エッジケースをテストケースに混ぜる

4手法いずれも、テストケース自体の設計が結果の信頼性を決めます。公式ドキュメントが挙げるエッジケースの型は、無関係または存在しない入力データ、極端に長い入力、チャット用途では低品質・有害・無関係なユーザー入力、そして人間同士でも評価が割れるような曖昧なケースの4つです。感情分析の例では皮肉と感情の混在、FAQの例ではタイプミスと回りくどい長文、要約の例では複数トピックの混在、トーンの例では怒っている顧客と複雑な問い合わせが、それぞれのタスクにおけるエッジケースとして機能しています。

evalの設計では、質より量を優先します。少数の高品質な人手採点よりも、自動採点で精度がやや落ちても件数の多いテストセットのほうが、実運用の分布を反映しやすいというのが公式の方針です。

関連するeval設計の論点

採点者の3類型や非決定性への対処(pass@k / pass^k)はAIエージェントのEval設計で扱っています。評価環境のリソース設定がスコアに与える影響についてはAnthropicが示したコーディング評価の落とし穴を参照してください。

まとめ

Claudeアプリの成功基準を定量評価する4手法は、タスクの性質で選び分けます。正解がカテゴリカルに定まるならexact match、言い換えへの意味的一貫性を測るならコサイン類似度、要約の要点網羅ならROUGE-L、トーンのような主観的な質を測るならLLM Likertスケールです。どの手法でも、SMART原則で成功基準を先に言語化すること、テストケースにエッジケースを必ず混ぜること、採点方式は速さと信頼性のバランスで選ぶことが土台になります。

この記事を共有:XはてブLinkedIn