ankuro.dev
← ブログ一覧に戻る
【CCA Foundations対策 / Claude API編 #5】Evalの実装——データセット生成・実行・採点
2026-04-01#Claude#API#Python#生成AI#入門#Claude Certified Architect

【CCA Foundations対策 / Claude API編 #5】Evalの実装——データセット生成・実行・採点

Anthropic Academyの「Claude APIを使用した構築」コースをもとに解説しています。

前回(#4)でEvalのワークフローと5ステップを整理した。この記事では実際にコードを書いてパイプラインを動かす。テストデータの自動生成から、実行・採点まで一通り実装する。


テストデータセットを自動生成する

Evalに必要なテストデータは手動で作ることもできるが、Claudeに生成させると大量のケースをすばやく用意できる。

手動作成 vs 自動生成の使い分け

方法 向いているケース
手動作成 実際のユーザー入力パターン・必ず通過させたいエッジケース
自動生成 多様なバリエーションの網羅・大量のケースを素早く用意したい

手動作成は少数精鋭で使う。実際のユーザーが送ってきそうな典型的な入力や、バグが出やすいコーナーケースを厳選してデータセットに含める。

自動生成は多様性と量の確保に向いている。Claudeに多数のバリエーションを生成させることで、手動では思いつかないパターンをカバーできる。どちらか一方ではなく、両方を組み合わせるのが実践的なアプローチ。

今回の例題は「テキスト処理ツール」——ユーザーのリクエストに対してPython・JSON・正規表現のいずれかを返すプロンプトを評価する。

generate_dataset() の実装

import anthropic
import json
from dotenv import load_dotenv

load_dotenv()

client = anthropic.Anthropic()
model = "claude-haiku-4-5-20251001"  # データ生成にはHaikuで十分

def add_user_message(messages, text):
    messages.append({"role": "user", "content": text})

def add_assistant_message(messages, text):
    messages.append({"role": "assistant", "content": text})

def chat(messages, system=None, temperature=1.0, stop_sequences=[]):
    params = {
        "model": model,
        "max_tokens": 1000,
        "messages": messages,
        "temperature": temperature,
    }
    if system:
        params["system"] = system
    if stop_sequences:
        params["stop_sequences"] = stop_sequences
    response = client.messages.create(**params)
    return response.content[0].text

FENCE = "```"  # プリフィル・ストップシーケンスで使うトリプルバッククォート

def generate_dataset():
    prompt = f"""
テキスト処理ツールのEvalデータセットを生成してください。
このツールはユーザーのリクエストに対して、Python関数・JSON・正規表現のいずれかを返します。

以下の形式でJSON配列を出力してください:
{FENCE}json
[
  {{
    "task": "タスクの説明",
    "format": "python" または "json" または "regex"
  }}
]
{FENCE}

条件:
- 1つの関数・1つのJSONオブジェクト・1つの正規表現で解決できるシンプルなタスク
- python・json・regexをそれぞれ1件以上含める

3件のタスクを生成してください。
"""
    messages = []
    add_user_message(messages, prompt)
    add_assistant_message(messages, f"{FENCE}json")  # プリフィル
    text = chat(messages, stop_sequences=[FENCE])    # 閉じ ``` で停止
    return json.loads(text)

ポイントは2つ:

  • FENCE 変数でトリプルバッククォートを一元管理(Markdownのコードブロックと衝突しないための工夫)
  • プリフィルで ```json を先置き → Claudeはその続きからJSONの中身だけを生成 → ストップシーケンスで閉じ ``` 手前で停止(#3で扱ったテクニック)

実行するとこんなデータセットが得られる:

dataset = generate_dataset()
print(dataset)
[
  {"task": "メールアドレスを検証するPython関数を書いてください", "format": "python"},
  {"task": "名前・年齢・役職を含む社員情報をJSON形式で作成してください", "format": "json"},
  {"task": "日本の郵便番号(000-0000形式)にマッチする正規表現を書いてください", "format": "regex"}
]

生成されたデータセットはファイルに保存しておく:

with open("dataset.json", "w", encoding="utf-8") as f:
    json.dump(dataset, f, indent=2, ensure_ascii=False)

Evalパイプラインを走らせる

データセットができたら、プロンプトと組み合わせてClaudeに回答させ、結果を収集する。パイプラインは3つの関数で構成する。

3つのコア関数

# 評価対象のプロンプト(v1)
PROMPT_TEMPLATE = """
以下のタスクを解決してください:

{task}
"""

def run_prompt(test_case):
    """テストケースとプロンプトをマージしてClaudeに送る"""
    prompt = PROMPT_TEMPLATE.format(task=test_case["task"])
    messages = []
    add_user_message(messages, prompt)
    return chat(messages)

def run_test_case(test_case):
    """1件のテストケースを実行して採点する"""
    output = run_prompt(test_case)
    score = 10  # ← この時点ではハードコード(後で本実装に差し替える)
    return {
        "output": output,
        "test_case": test_case,
        "score": score,
    }

def run_eval(dataset):
    """データセット全件を実行して結果を収集する"""
    results = []
    for test_case in dataset:
        result = run_test_case(test_case)
        results.append(result)
    return results

実行してみると、v1プロンプトでのClaudeの出力がわかる:

with open("dataset.json", "r", encoding="utf-8") as f:
    dataset = json.load(f)

results = run_eval(dataset)
print(results[0]["output"])

v1では説明文とコードブロックが混在した出力が返ってくる:

メールアドレスを検証するPython関数の実装例です。

(コードブロック)
import re

def validate_email(email: str) -> bool:
    pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
    return bool(re.match(pattern, email))
(コードブロック終わり)

この関数は...(説明が続く)

このままではコードとして使えない。スコアも 10 のハードコードのままで意味がない——次のセクションで採点を実装する。

score = 10 をハードコードしている理由は、グレーダーより先にパイプライン全体の動作を確認するため。最初から全部を実装しようとすると複雑になる。まずデータ生成・プロンプト実行・結果収集の流れが正しく動くことを確認してから、グレーダーを独立したステップとして追加していく。


モデルベースの採点

グレーダーには3種類ある:

種類 方法 向いているタスク
コードベース プログラムで検証 構文チェック・形式検証
モデルベース 別のClaudeで評価 品質・指示への忠実さ
人手 人が直接採点 総合的な判断が必要なもの

まずモデルベースのグレーダーを実装する。

grade_by_model() の実装

def grade_by_model(test_case, output):
    eval_prompt = f"""
あなたはコードレビューの専門家です。以下のAI生成コードを評価してください。

タスク:{test_case["task"]}
出力:{output}

以下の形式でJSONを返してください:
- "strengths": 優れている点(1〜3個の配列)
- "weaknesses": 改善できる点(1〜3個の配列)
- "reasoning": 評価の理由(簡潔に)
- "score": 1〜10の評価スコア
"""
    messages = []
    add_user_message(messages, eval_prompt)
    add_assistant_message(messages, f"{FENCE}json")  # プリフィル
    text = chat(messages, stop_sequences=[FENCE])
    return json.loads(text)

「スコアだけ聞くと中間値に偏る」問題

グレーダーに score だけ返させると、モデルは迷ったときに 67 あたりを返しがちになる。strengthsweaknessesreasoning も一緒に求めることで、採点の根拠を言語化させてから数値を出すよう促せる。

run_test_case に組み込む

from statistics import mean

def run_test_case(test_case):
    output = run_prompt(test_case)
    grade = grade_by_model(test_case, output)
    return {
        "output": output,
        "test_case": test_case,
        "score": grade["score"],
        "reasoning": grade["reasoning"],
    }

def run_eval(dataset):
    results = []
    for test_case in dataset:
        result = run_test_case(test_case)
        results.append(result)
    avg = mean([r["score"] for r in results])
    print(f"平均スコア: {avg:.1f}")
    return results

v1プロンプトで実行すると、説明文が混入しているのでスコアが低めになる:

平均スコア: 5.7

コードベースの採点

モデルベースの採点だけでは「内容の品質」しか測れない。生成されたコードが実際に構文として正しいかどうかは、プログラムで検証する方が確実。

構文バリデーション関数

import ast
import re

def validate_json(text):
    try:
        json.loads(text.strip())
        return 10
    except json.JSONDecodeError:
        return 0

def validate_python(text):
    try:
        ast.parse(text.strip())
        return 10
    except SyntaxError:
        return 0

def validate_regex(text):
    try:
        re.compile(text.strip())
        return 10
    except re.error:
        return 0

def grade_syntax(output, test_case):
    fmt = test_case.get("format", "")
    if fmt == "json":
        return validate_json(output)
    elif fmt == "python":
        return validate_python(output)
    elif fmt == "regex":
        return validate_regex(output)
    return 0

各関数はパースを試みて、成功なら 10、失敗なら 0 を返す。「ほぼ正しい」は存在しない——構文として動くかどうかのバイナリ判定。

モデルスコアと合算する

モデルスコアは「タスクの要件を満たしているか・回答の質」を測り、コードスコアは「構文として正しく動くか」を測る。どちらか片方だけでは不十分——内容は良くても構文エラーのコードは使えないし、構文は正しくてもタスクを満たさないコードも意味がない。2つを平均することで、品質と技術的な正確さを両面から評価できる。

def run_test_case(test_case):
    output = run_prompt(test_case)
    grade = grade_by_model(test_case, output)
    model_score = grade["score"]
    syntax_score = grade_syntax(output, test_case)
    final_score = (model_score + syntax_score) / 2
    return {
        "output": output,
        "test_case": test_case,
        "score": final_score,
        "model_score": model_score,
        "syntax_score": syntax_score,
        "reasoning": grade["reasoning"],
    }

v1プロンプトは説明文が混じるので構文スコアが低くなる。final = (model_score + syntax_score) / 2 で内容と構文を等ウェイトで合算している:

メールアドレス(python)→ model: 6 / syntax: 0 → final: 3.0
  ※説明文がついているのでast.parseが失敗する

社員情報(json)       → model: 7 / syntax: 0 → final: 3.5
  ※コードブロック記法が残っているのでjson.loadsが失敗する

郵便番号(regex)      → model: 8 / syntax: 10 → final: 9.0
  ※正規表現はシンプルなので混入が少ない

平均スコア v1: 5.2

プロンプトを改善してv2のスコアと比較する

v1の問題点は「説明文とコードブロック記法が混入すること」。プロンプトに明示的な指示とプリフィルを加える:

def run_prompt(test_case):
    prompt = f"""
以下のタスクを解決してください:

{test_case["task"]}

* Python・JSON・正規表現のいずれかのみを返してください
* 説明・コメント・コードブロック記法は不要です
"""
    messages = []
    add_user_message(messages, prompt)
    add_assistant_message(messages, f"{FENCE}code\n")  # プリフィルでコードから始める
    return chat(messages, stop_sequences=[FENCE])
平均スコア v2: 8.1   (v1: 5.2 → +2.9)

数値でプロンプトの改善を確認できた。「v2の方が良さそう」という感覚ではなく、スコアという根拠を持って次の判断ができる。


まとめ

  • テストデータはClaudeで自動生成できる。Haikuを使うとコストを抑えられる
  • パイプラインの核は run_prompt / run_test_case / run_eval の3関数
  • モデルベース採点では強み・弱み・理由もセットで求めると採点の精度が上がる
  • コードベース採点は構文の正しさをバイナリで検証する(10 or 0)
  • 両スコアを合算することで「内容の品質」と「技術的な正確さ」を両面から評価できる
  • スコアを比較することでプロンプトの改善が数値で見える

次回はEvalで品質を測る前提となる、プロンプトの書き方を体系的に整理する——明確さ・具体性・XMLタグ・マルチショットの4つのテクニックを扱う。


← 前回:Evalの設計と仕組み 振り返りクイズ:Eval編(#4〜#5)