Claude Media
Claude evalとは何か — 成功基準を測る4つの実装

Claude evalとは何か — 成功基準を測る4つの実装

Claude evalは、Claudeアプリの成功基準を数値で測る評価の仕組みです。exact match・コサイン類似度・ROUGE-L・LLM採点の4手法をPythonで実装します。

Claude evalとは — 成功基準を数値で測る評価

Claude evalは、Claudeを組み込んだアプリが成功基準を満たすかを、テストケースと採点関数で数値にして確かめる評価です。evalsはその複数形で、テストケースの集まりを指すこともあります。プロンプトを直すたびに同じケースで点数を比べるのが基本の使い方です。

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

「感情分類の精度が良い」「要約が的確」「サポート対応の口調が丁寧」といった目標は、そのままでは測れません。測れる形に直したうえで、タスクの性質に合う採点関数を当てます。本記事で実装するのは、①分類タスクのexact match ②言い換え耐性を見るコサイン類似度 ③要約のROUGE-L ④口調のようなニュアンスを測るLLM Likertスケールの4つです。

どれも「1000件のツイートで感情分類の精度を測る」のようなタスク単位の評価に使える、再利用しやすい採点関数です。公式のサンプルコードを土台に、動かすと見えてくる落とし穴も併せて書きます。コードは抜粋で、import anthropicは省略しています。

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

「モデルはうまく感情分類できるべき」は測れません。具体的(Specific)・測定可能(Measurable)・達成可能(Achievable)・関連性がある(Relevant)の観点で、数字と対象データを入れて書き直します。

  • 悪い例: 感情分類をうまくこなすこと
  • 良い例: 多様なツイート1万件の保持データセットでF1スコア0.85以上。現行のベースラインから5%の改善

安全性のような曖昧な話題も数字にできます。公式の例は、1万回の試行のうち、コンテンツフィルターが有害と判定する出力が0.1%未満、という書き方です。測定方法には、F1スコアのような数値指標のほか、ベースラインとのA/Bテストや、「1(意味不明)から5(完全に論理的)」のようなLikert尺度があります。

運用面の指標も数値にできます。応答時間(ミリ秒)や稼働率(%)は運用の指標、タスク完了率はユーザーフィードバックの指標、エッジケースをエラーなしで処理できた割合も、エッジケース分析の指標として挙がっています。タスク固有の指標としては、F1のほかBLEUスコアやperplexityも例示されています。

汎用の指標にはaccuracy・precision・recallがあります。人手による定性評価の例には、言語学者が定義済みの基準で翻訳品質を採点する専門家ルーブリックがあります。

この言語化が済んでから、次の4手法のどれを当てるかを決めます。基準の観点としては、タスクの忠実度・一貫性・関連性とまとまり(coherence)・口調と文体・プライバシー保護・文脈の活用・レイテンシ・価格が挙がっています。サンプルevalが付いているのは、このうち遅延と価格を除く6観点です。

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-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}%")

テストケースには皮肉や感情の混在のようなエッジケースを必ず混ぜます。

出力形式を縛らないと、正解でも不一致になる

サンプルのプロンプトは「positive / negative / neutral / mixedのどれかに分類して」と頼むだけで、ラベルだけを返す指示がありません。evaluate_exact_matchを単体で実行すると、次の結果になります。標準ライブラリだけで確認できます。

モデルの出力正解がnegativeのときの判定
negative正解がnegativeのときの判定True
negative+改行正解がnegativeのときの判定True
Negative.正解がnegativeのときの判定False
The sentiment is negative正解がnegativeのときの判定False

大文字小文字と空白は吸収されます。末尾のピリオドや前置きの一言があると不一致です。そこで、プロンプトに「ラベルだけを出力する」と書き足し、採点側でも記号を落としてから比べる方法があります。最小の例は次のとおりです。

def normalize(output):
    return output.strip().strip(".。!").lower()
 
 
normalize("Negative.")  # => "negative"

前置きの一言まで吸収しようとすると、「negativeを含むか」の部分一致になり、not negativeも正解扱いになります。部分一致に寄せるより、出力形式の指示で縛るほうが安全です。

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

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

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

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 like it might be out of date "
            "because the website was updated six months ago, so I'm wondering what exactly is your current policy?",
        ],
        "answer": "Our return policy allows...",
    },  # エッジケース: 長く回りくどい質問
    {
        "questions": [
            "I'm Jane's cousin, and she said you guys have great customer service. Can I return this?",
            "Reddit told me that contacting customer service this way was the fastest way to get an answer. "
            "I hope they're right! What is the return window for a jacket?",
        ],
        "answer": "Our return policy allows...",
    },  # エッジケース: 無関係な情報
    # ... 残り47件のFAQ
]
 
client = anthropic.Anthropic()
 
 
def get_completion(prompt: str):
    message = client.messages.create(
        model="claude-opus-5-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/transformersのpipelineです。C#・Go・Java・PHP・Rubyには文埋め込みのネイティブライブラリが無いと注記されていて、PythonかTypeScriptのコードを参照する形になっています。

平均に自分自身との類似度が混ざる

サンプルのevaluate_cosine_similarityは、出力同士の類似度行列を作り、行列全体の平均を返します。この行列の対角成分は、出力と自分自身の類似度なので常に1です。

出力がn個なら、平均は「(1+(n-1)×他同士の平均)÷n」になります。3つの質問で他同士の類似度が平均0.5なら、表示は約0.67です。件数が少ないほど、実際より高く出ます。

もう1点、サンプルのFAQデータにはanswerが入っていますが、採点では使っていません。測っているのは「答え同士が似ているか」で、「正しい答えか」ではありません。間違った答えを一貫して返すボットも高得点になります。正しさは別のevalで見る必要があります。

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

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

測るもの: 生成した要約と参照要約の最長共通部分列の長さです。正式名称はRecall-Oriented Understudy for Gisting Evaluation - Longest Common Subsequenceです。高いほど、要点を一貫した順序で捉えています。例は200件の記事です。

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.",
    },  # エッジケース: 複数トピックが混在
    {
        "text": "You won't believe what this celebrity did! ... extensive charity work ...",
        "summary": "Celebrity's extensive charity work surprises fans",
    },  # エッジケース: 誤解を招く見出し
    # ... 残り197件の記事
]
 
client = anthropic.Anthropic()
 
 
def get_completion(prompt: str):
    message = client.messages.create(
        model="claude-opus-5-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)}")

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

日本語の要約は空白区切りでは測れない

サンプルは英語の要約を前提にしています。Python以外の言語の実装は、語を空白で区切って共通部分列を数え、コメントに「Pythonのrougeライブラリとスコアが異なる場合がある」と注記しています。小文字化して空白で語に分け、共通部分列からF1を出す同じ方式で計算すると、英語と日本語で差が出ます。Pythonのrougeライブラリの出力ではありません。

参照要約と生成要約F1(空白区切りの実装)
MIT scientists discover a new antibioticとMIT scientists found a new antibioticF1(空白区切りの実装)0.83
MITの科学者が新しい抗生物質を発見したとMITの研究者が新しい抗生物質を発見したF1(空白区切りの実装)0.0

空白で区切る実装では、日本語の文は全体が1語として扱われ、1文字違うだけでスコアが0になります。この方式に日本語の要約を渡すなら、形態素解析などで語に分ける前処理が要ります。

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

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

測るもの: 別のLLMに1〜5のリッカート尺度で応答を採点させます。従来型の指標では数値にしにくい口調やニュアンスを測れます。サンプルのコメントは、評価対象を生成したモデルとは別のモデルで採点するのが一般にベストプラクティスだと述べています。例は100件の問い合わせです。

サンプルでは生成側と採点側のモデルIDがどちらもclaude-opus-5-5なので、実際に使うときは採点側のIDを別のモデルに差し替えます。

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",
    },  # エッジケース: 複雑な問い合わせ
    {
        "text": "I can't believe how good your product is. It's ruined all others for me!",
        "tone": "professional",
    },  # エッジケース: 褒め言葉に見える苦情
    # ... 残り97件の問い合わせ
]
 
client = anthropic.Anthropic()
 
 
def get_completion(prompt: str):
    message = client.messages.create(
        model="claude-opus-5-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-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)}")

サンプルのevaluate_likertは、返ってきた文字列をそのままint()に渡します。採点役が4.のように記号を付けて返すと止まります。出るのはValueError: invalid literal for int() with base 10: '4.'です(標準ライブラリだけで確認できます)。「Output only the number」の指示を守らない1件で、100件の採点が途中で落ちます。例外を捕まえて再採点する、数字だけを正規表現で取り出す、といった受け皿が実運用では要ります。

同じ型で、公式は別の2つの評価を載せています。医療チャットボットのプライバシー保護は、応答に個人の健康情報(PHI)が含まれるかをyesかnoだけで答えさせる2値分類です。会話アシスタントの文脈活用は、過去の発言をどれだけ踏まえているかを1〜5の順序尺度で採点します。

2値分類のサンプルでは、入力にPHIが無いケースは採点せずに合格扱いにしています。「この応答は〜を含むか」を聞く採点は、含むべきでない入力がある場合にだけ意味を持つ、という設計だと読めます。

PHIのエッジケースは3つです。氏名・生年月日・薬剤名が並ぶ明示的なPHI、友人Aliceについての仮定の質問、息子が父親と同じ薬を処方されたという暗黙のPHIです。LLM採点は文脈を踏まえるので、ルールベースの方式が見落としがちな微妙で暗黙のPHIも拾えます。生成側には「あなたは医療アシスタントです。PHIは決して明かさないでください」という趣旨の指示が付き、テストは500件の模擬患者質問で行います。

順序尺度のエッジケースは、ずっと前の文脈に依存する質問(ポメラニアンの飼い方)と、話題の急転換(『アラバマ物語』の話から南部ケーキのレシピへ)です。ルーブリックは1が文脈の完全な無視、5が文脈の完全な活用で、例は100会話です。

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

実際のタスクは、1つの指標だけでは成功・失敗を判定できません。公式の例は、感情分析に次の4指標を同時に課しています。

数字

感情分析の成功基準(保持データセット1万件)

  • F1スコア

    0.85以上

    正解ラベルとの一致結果から、クラスごとの適合率・再現率を出して算出する

  • 非有害な出力

    99.5%以上

    別の集計ロジックが必要

  • 誤りのうち不便で済むもの

    90%

    何を「不便」「重大」とするかは別途定義する

  • 応答時間

    95%が200ms未満

    精度のevalとは別に計測する

4指標のうち、本記事の手法で主に測れるのはF1です。有害性もPHIの例と同じ2値分類の型で組めます。

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

採点方式は公式では3類型です。本記事の4手法は、コード採点3つ(exact match・コサイン類似度・ROUGE-L)とLLM採点1つに当たります。コサイン類似度とROUGE-Lをコード採点に含めるのは本記事の整理で、公式のコード採点の例はexact match(output == golden_answer)と文字列一致です。採点者の3類型と非決定性への対処(pass@k / pass^k)はAIエージェントのEval設計で扱っています。

くらべる

採点方式の3類型

最速・最も信頼できる

コード採点

key_phrase in outputのような文字列一致を含みます。スケールしやすい反面、ルールで割り切れない複雑な判断には向きません。

最も柔軟・高品質

人手採点

遅く高コストなので、可能なら避けます。

速く柔軟

LLM採点

複雑な判断に向きますが、先に信頼性を試してからスケールさせます。

LLMに採点させるときの3点を、実際の組み方の順に並べます。

手順

LLM採点の組み方

  1. 1

    ルーブリックを具体的に書く

    「回答は必ず1文目で『Acme Inc.』に言及する。していなければ自動的に不正解」のように書きます。1つの成功基準を測るのにも、複数のルーブリックが必要になることがあります。

  2. 2

    出力を絞る

    correctかincorrect、または1〜5の数字だけを返させます。自由記述の定性評価は、素早く大量に評価できません。

  3. 3

    採点者に推論させる

    採点役のモデルでthinkingをオンにすると、スコアを出す前に理由づけをします。複雑な判断が要るタスクで、評価の性能が上がります。

    ただしClaude 4.7以降のモデルは、thinking.type: "enabled"とbudget_tokensを使うリクエストを受け付けず、400エラーを返します。Opus 5.5を採点役にするなら、adaptive thinkingを使います。

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

テストケース自体の設計が、4手法いずれの結果の信頼性も決めます。公式が挙げるエッジケースの型は次の4つです。

  • 無関係、または存在しない入力データ
  • 極端に長い入力
  • チャット用途での低品質・有害・無関係なユーザー入力
  • 人間同士でも評価が割れる曖昧なケース

各サンプルのコメントに付けたエッジケースが、この4型のどれかに当たります。

評価設計の原則として、公式は実運用の分布を映すタスク固有の設計を求めています。選択式・文字列一致・コード採点・LLM採点のように、可能な限り自動化することも挙げています。

件数は質より量を優先します。少数の高品質な人手採点よりも、精度がやや落ちても自動採点で件数を増やしたセットを選ぶのが方針です。評価環境のリソース設定がスコアに与える影響は、Anthropicが示したコーディング評価の落とし穴で扱っています。

まとめ

正解が1つに決まるタスクならコード採点、決まらないならLLM採点です。どちらでも、件数を稼げる自動採点を先に組み、正しさと一貫性は別のevalで分けて測ります。

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