
Evalの実装——データセット生成・実行・採点【CCA Foundations対策】
CCA Foundations対策 · Claude API編 第5回(全17回)
Anthropic Academyの「Claude APIを使用した構築」コースをもとに解説しています。
前回(#4)でEvalのワークフローと設計の考え方を整理した。この記事では実際にコードを書いてパイプラインを動かす。テストデータの自動生成から実行・採点・改善サイクルまで一通り実装する。
この記事でわかること:
- Claudeでテストデータを自動生成する方法と、手動データとの使い分け
run_prompt/run_test_case/run_evalの3関数でEvalパイプラインを組む方法- モデルベース採点でfew-shotを使って採点のブレを減らすテクニック
- バリデーション失敗時のリトライパターンと、リトライが効かないケースの見分け方
- コードベース採点で構文の正しさを検証する方法
テストデータセットを自動生成する
Evalに必要なテストデータは手動で作ることもできるが、Claudeに生成させると大量のケースをすばやく用意できる。
手動作成 vs 自動生成の使い分け
| 方法 | 向いているケース |
|---|---|
| 手動作成 | 実際のユーザー入力パターン・必ず通過させたいエッジケース |
| 自動生成 | 多様なバリエーションの網羅・大量のケースを素早く用意したい |
自動生成には注意点がある。 Claudeが生成したテストデータは、Claudeが得意なパターンに偏る傾向がある。「Claudeが作ったデータで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
score = 10 をハードコードしている理由: グレーダーより先にパイプライン全体の動作を確認するため。最初から全部を実装しようとすると、どこが壊れているのか切り分けが難しくなる。まずデータ生成・プロンプト実行・結果収集の流れが正しく動くことを確認してから、グレーダーを独立したステップとして追加していく。
実行してみると、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))
(コードブロック終わり)
この関数は...(説明が続く)
このままではコードとして使えない。次のセクションで採点を実装する。
モデルベースの採点
まずモデルベースのグレーダーを実装する。
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 だけ返させると、モデルは迷ったときに 6 や 7 あたりを返しがちになる。strengths・weaknesses・reasoning も一緒に求めることで、採点の根拠を言語化させてから数値を出すよう促せる。
few-shot でグレーダーの採点基準を安定させる
モデルベース採点のもう一つの問題は、採点のブレ。「このスコアは7点か8点か」という境界線の判断が実行ごとに揺れると、Eval全体の信頼性が下がる。
few-shot例をグレーダーに追加すると、採点の基準を具体的に伝えられる:
def grade_by_model(test_case, output):
eval_prompt = f"""
あなたはコードレビューの専門家です。以下のAI生成コードを評価してください。
【採点例】
タスク:メールアドレスを検証するPython関数
出力:def validate(email): return "@" in email
→ strengths: ["短く書かれている"], weaknesses: ["正規表現を使っておらず精度が低い", "型ヒントがない"], score: 4
タスク:名前と年齢をJSON形式で返す
出力:{{"name": "田中太郎", "age": 30}}
→ strengths: ["要件通りのフィールドが揃っている", "フォーマットが正確"], weaknesses: [], score: 9
【評価対象】
タスク:{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)
few-shotを入れると「このくらいの出力が何点か」という基準がClaudeに伝わり、採点のブレが小さくなる。詳細な説明文を書くより、具体的な例を見せる方が安定した採点につながる。
📋 試験ガイドより
公式試験ガイドのDomain 4 Task 4.2では、few-shotの例示が採点の一貫性を高める方法として取り上げられている。具体的な例がなぜ説明より効くかも同タスクのポイント。
コードベースの採点
モデルベースの採点だけでは「内容の品質」しか測れない。生成されたコードが実際に構文として正しいかどうかは、プログラムで検証する方が確実。
構文バリデーション関数
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 を返す。「ほぼ正しい」は存在しない——構文として動くかどうかのバイナリ判定。
バリデーション失敗時のリトライパターン
コードベース採点でスコアが 0(構文エラー)になったとき、エラー内容を含めて再リクエストすると修正される確率が上がる。
def run_prompt_with_retry(test_case, max_retries=2):
"""構文エラーが出た場合、エラー内容を添えてリトライする"""
output = run_prompt(test_case)
for attempt in range(max_retries):
syntax_score = grade_syntax(output, test_case)
if syntax_score == 10:
break # 構文OK → リトライ不要
# 構文エラーの内容を特定する
fmt = test_case.get("format", "")
try:
if fmt == "python":
ast.parse(output.strip())
elif fmt == "json":
json.loads(output.strip())
elif fmt == "regex":
re.compile(output.strip())
except Exception as e:
error_detail = str(e)
# エラー内容を含めて再リクエスト
retry_messages = []
add_user_message(retry_messages, PROMPT_TEMPLATE.format(task=test_case["task"]))
add_assistant_message(retry_messages, output)
add_user_message(retry_messages,
f"バリデーションエラーが発生しました:{error_detail}\n"
f"修正してください。{fmt}の構文として正しい形式のみを返してください。"
)
output = chat(retry_messages)
return output
実行例:
1回目: 説明文込みのPythonコード → syntax_score: 0
エラー: invalid syntax (<unknown>, line 1)
2回目: 純粋なコードだけ返ってくる → syntax_score: 10
リトライが効かないケースを見分ける
リトライが有効なのはフォーマットの問題(説明文が混入、コードブロック記法が残っているなど)のとき。次のケースではリトライしても改善されない:
- 情報がそもそも存在しない — 「この関数の複雑度を計算してください」と聞いたが、元のコードが提供されていない
- タスク自体が不正確 — 実現不可能な要件(「正規表現でネストした括弧を完全に検証する」など)
エラーが続く場合、リトライを重ねるよりタスクの設定自体を見直す方が効率的。
📋 試験ガイドより
公式試験ガイドのDomain 4 Task 4.4では、エラー内容を含めたリトライパターンと、リトライが効かないケースの判断が取り上げられている。フォーマットミスと情報不足は根本的に異なる問題として区別することがポイント。
モデルスコアと構文スコアを合算する
モデルスコアは「タスクの要件を満たしているか・回答の質」を測り、構文スコアは「構文として正しく動くか」を測る。2つを平均することで、品質と技術的な正確さを両面から評価できる:
from statistics import mean
def run_test_case(test_case):
output = run_prompt_with_retry(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"],
}
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プロンプトは説明文が混じるので構文スコアが低くなる:
メールアドレス(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の方が良さそう」という感覚ではなく、スコアという根拠を持って次の判断ができる。
よくある誤解まとめ
| 誤解 | 実際 |
|---|---|
| 自動生成データだけでEvalを組めば十分 | Claudeが得意なパターンに偏る。手動データと組み合わせる |
score = 10 のハードコードはすぐ直すべき |
まずパイプライン全体の動作確認が先。グレーダーは後から追加する |
| スコアだけ採点させれば中間値が出ない | 根拠(strengths/weaknesses)を先に言語化させると採点が安定する |
| リトライすれば必ず改善される | フォーマットミスには効く。情報が存在しない場合は効かない |
| モデルスコアだけで十分 | 内容は良くても構文エラーのコードは使えない。コードベース採点を合わせる |
まとめ
- テストデータはClaudeで自動生成できるが、Claudeが得意なパターンに偏るため手動データと組み合わせる
- パイプラインの核は
run_prompt/run_test_case/run_evalの3関数 score = 10のハードコードからスタートして、パイプライン全体の動作を確認してからグレーダーを追加する- few-shot例をグレーダーに追加すると採点のブレが小さくなる。説明より具体例の方が効く
- 構文エラー時はエラー内容を含めてリトライすると修正率が上がる
- リトライが効くのはフォーマットミスのとき。情報がそもそも存在しない場合は効かない
- モデルスコアとコードベーススコアを合算して「内容の品質」と「技術的な正確さ」を両面から評価する
次回はEvalで品質を測る前提となる、プロンプトの書き方を体系的に整理する——明確さ・具体性・XMLタグ・マルチショットの4つのテクニックを扱う。