最終更新: 2026年6月22日
AIエージェントの評価(Evals)とは、LLMベースのエージェントが「期待された出力・振る舞いを再現できるか」を、決定論的なアサーションとLLM-as-a-Judgeを組み合わせて自動測定する仕組みのことです。プロンプトを書き換えるたびに本番で炎上する状況から抜け出す唯一の方法は、CIで回るeval suiteを最初に組むことです。本記事では、私が実運用で回している「決定論的チェック7割 + LLMジャッジ3割」のハイブリッド構成を、Python(OpenAI/Anthropic SDK)とpromptfooのコードでまるごと公開します。プロンプト職人芸から脱出するための、最短ルートです。
エージェント評価は「決定論的チェック(exact match / 正規表現 / ツール呼び出し検証)」と「LLM-as-a-Judge(rubric採点)」の二層構成が現実解。片方だけでは破綻します。
LLM-as-a-Judgeはpairwise比較(A vs B)の方が、絶対スコア(1〜5点)より人間評価との相関が0.15〜0.25高い(2026年のArena Hard / MT-Benchの再現実験)。
promptfoo、DeepEval、LangSmith、Braintrustの4つが主要evalハーネス。CIで回したいならpromptfoo、本番トレース連携ならLangSmithが2026年時点の最適解。
評価データセットは20件から始めて十分です。100件超えても、回帰検出能力は対数的にしか伸びません。
ツール呼び出しエージェントのevalでは、「最終出力」より「ツール呼び出しシーケンス」を主検査対象にすべき。出力だけ見るとサイレントな退行を見逃します。
目次
なぜプロンプト改善にevalが不可欠なのか
決定論的チェックとLLM-as-a-Judgeの二層構成
決定論的アサーションの実装パターン
LLM-as-a-Judgeの設計とプロンプト
ツール呼び出しエージェントの評価方法
主要evalフレームワーク比較(2026年版)
CIに組み込むワークフロー
よくある落とし穴と回避策
なぜプロンプト改善にevalが不可欠なのか
「プロンプトを少し変えたら出力が良くなった気がする」。この主観的な感覚に基づく開発が、現代のLLMアプリ開発における最大の負債です。Claude Sonnet 4.6やGPT-5系のモデルでは、temperatureを0にしても出力が決定論的になるわけではなく、同じプロンプトでも実行ごとに微妙にトーンや構造が変わります。つまり、目視確認は再現性ゼロの検査になります。
正直に言うと、私が直近で関わった社内ナレッジ検索エージェントのプロジェクトで、まさにこれをやらかしました。リリース初週にプロンプトを11回更新した結果、3つの基本的なクエリで回答品質が退行していたんです。原因はシンプルで、「最新の質問だけ確認していたから」。eval suiteを20件のテストケースで最初に組んでおけば、CIが赤くなって即座に気付けたはずでした。
OpenAIの公式evalsリポジトリ もAnthropicの評価手法に関するペーパー も、共通して「評価データセットなしのプロンプトチューニングはギャンブルである」と主張しています。本記事ではこの原則を、コードに落として再現可能な形にします。
決定論的チェックとLLM-as-a-Judgeの二層構成
実運用で破綻しない評価アーキテクチャは、必ず二層構成になります。第一層は決定論的チェック(exact match、正規表現、JSON schema検証、ツール呼び出しシーケンスの一致)。第二層がLLM-as-a-Judge(文章の意味的な正しさ、トーン、ハルシネーション検知)です。
なぜ二層が必要か。決定論的チェックだけだと「正しく言い換えられた回答」が×になります。逆にLLM-as-a-Judgeだけだと、APIキーが返ってしまう、JSONがbrokenになる、ツール呼び出しが冪等性を失う、といったクリティカルな退行を見逃します。「LLMでLLMを評価する」のは安価で速いが信頼性に欠ける手段なので、最終防衛線は必ず決定論側に置きます。
私の運用比率は決定論70%・LLM-as-a-Judge 30%です。本番のSLAに直結するアサーション(フォーマット、必須キー、禁止文言)は全部決定論側へ。意味的な正確性やユーザー体験寄りの項目だけJudgeに任せます。これはClaude Agent SDK本番運用ガイド でも触れたHooks設計の考え方と同じで、「安いガード層を先に通す」原則の応用です。
決定論的アサーションの実装パターン
決定論的チェックは、コスト$0で1秒以内に終わるので、評価の主軸に据えるべきです。Python標準ライブラリとjsonschemaだけで十分実装できます。以下は実プロジェクトでそのまま使っているアサーション関数群です。
from typing import Any, Callable
import json
import re
from jsonschema import validate, ValidationError
def assert_contains(output: str, substring: str) -> bool:
return substring.lower() in output.lower()
def assert_not_contains(output: str, banned: list[str]) -> bool:
return not any(b.lower() in output.lower() for b in banned)
def assert_regex(output: str, pattern: str) -> bool:
return re.search(pattern, output) is not None
def assert_valid_json(output: str, schema: dict | None = None) -> bool:
try:
parsed = json.loads(output)
if schema:
validate(instance=parsed, schema=schema)
return True
except (json.JSONDecodeError, ValidationError):
return False
def assert_max_tokens(output: str, max_tokens: int) -> bool:
# ざっくり日本語1文字 ≒ 1トークン換算(厳密にはtiktokenを使う)
return len(output) <= max_tokens
# 顧客サポートエージェントの基本アサーション例
def eval_support_response(output: str) -> dict:
return {
"no_pii_leak": assert_not_contains(output, ["@", "電話番号", "クレジットカード"]),
"has_apology": assert_contains(output, "申し訳"),
"json_valid": assert_valid_json(output, schema={
"type": "object",
"required": ["message", "next_action"],
"properties": {
"message": {"type": "string"},
"next_action": {"enum": ["resolve", "escalate", "ask_more"]}
}
}),
"length_ok": assert_max_tokens(output, 500),
}
このパターンの強みは、CI上で並列実行できることと、失敗時に「どのアサーションが落ちたか」が一目瞭然なこと。曖昧な「品質が下がった」ではなく「no_pii_leakが3件中2件で失敗」という具体的なシグナルになります。
Tip: アサーション関数は必ずboolを返す純粋関数にしてください。副作用(API呼び出し、ファイル書き込み)を入れると、テストの並列実行で詰みます。
LLM-as-a-Judgeの設計とプロンプト
LLM-as-a-Judge(LLMによる審査)は、決定論では検査できない「意味的な正しさ」を判定するための仕組みです。2026年時点での重要な知見は、絶対スコア(1〜5点)よりpairwise比較(AとBどちらが良いか)の方が圧倒的に信頼できる こと。LMSYSのArena Hard benchmark の再現実験で、人間評価との相関係数が0.15〜0.25高くなることが繰り返し確認されています。
具体的な実装例を示します。Claude Sonnetをジャッジに使い、ベースライン回答と候補回答をpairwise比較します。
import anthropic
client = anthropic.Anthropic()
JUDGE_SYSTEM = """あなたは公平なAI回答評価者です。
2つの回答(AとB)を比較し、ユーザーの質問に対してどちらがより優れているか判定してください。
評価基準:
1. 事実の正確性(最重要)
2. 質問への直接的な回答度
3. 簡潔さと明瞭さ
出力は厳密に次のJSONのみ:
{"winner": "A" | "B" | "tie", "reason": "1文の根拠"}
"""
def judge_pairwise(question: str, answer_a: str, answer_b: str) -> dict:
msg = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=200,
system=JUDGE_SYSTEM,
messages=[{
"role": "user",
"content": f"# 質問\n{question}\n\n# 回答A\n{answer_a}\n\n# 回答B\n{answer_b}"
}],
)
return json.loads(msg.content[0].text)
# Position biasを除去するため両方向で評価し、一貫したときだけ勝者を確定
def judge_robust(question: str, baseline: str, candidate: str) -> str:
r1 = judge_pairwise(question, baseline, candidate)
r2 = judge_pairwise(question, candidate, baseline)
if r1["winner"] == "B" and r2["winner"] == "A":
return "candidate_wins"
if r1["winner"] == "A" and r2["winner"] == "B":
return "baseline_wins"
return "tie"
注意点が一つ。LLMジャッジには明確なposition bias (前に提示された回答を好む傾向)があります。GPT-5系で約8%、Claude Sonnet 4.6系で約5%の偏りが観測されているので、必ず順序を入れ替えた2回の評価で「一貫した勝者」だけを採用してください。これを怠るとジャッジ自体が信頼できない検査機になります。
Warning: ジャッジに評価対象と同じモデルを使うと、self-preference biasにより自分のモデルファミリーの出力を有利に評価します。本番に出すモデルとは別系統のモデルをジャッジに使ってください(例: GPT-5を本番、Claudeをジャッジ)。
RAGや関数呼び出しを伴うエージェントでは、「最終出力」だけ見ていると致命的な退行を見逃します。具体例で言うと、「在庫検索ツールを呼ぶべき場面でなぜか商品DBツールを呼んだが、たまたま回答が正しかった」というケース。出力テストは通過しますが、API課金と応答速度は劣化しています。
解決策はツール呼び出しシーケンスを評価対象に含める こと。期待されるツールチェーン(順序付きリスト)を正解として保存し、実行時のシーケンスと比較します。
def assert_tool_sequence(actual_calls: list[dict], expected_tools: list[str]) -> bool:
"""期待ツール名のリストと、実行されたツール名のシーケンスを比較"""
actual_names = [c["tool_name"] for c in actual_calls]
return actual_names == expected_tools
def assert_tool_called_with(actual_calls: list[dict], tool_name: str, arg_key: str, expected_value: Any) -> bool:
"""特定ツールが特定の引数で呼ばれたかを検証"""
for call in actual_calls:
if call["tool_name"] == tool_name and call["arguments"].get(arg_key) == expected_value:
return True
return False
# 評価例: 商品問い合わせシナリオ
test_case = {
"input": "ノートPCの在庫教えて",
"expected_tools": ["search_products", "check_inventory"],
"expected_args": {"search_products": {"category": "laptop"}},
}
actual = run_agent(test_case["input"]) # tool_callsをトレースに含む
assert assert_tool_sequence(actual.tool_calls, test_case["expected_tools"])
assert assert_tool_called_with(actual.tool_calls, "search_products", "category", "laptop")
このトレースベース評価は、LangGraphのAgentic RAG のように複数ノードを経由するワークフローで特に効果があります。各ノードの入出力をスナップショットしておけば、回帰の発生箇所を秒で特定できます。
主要evalフレームワーク比較(2026年版)
「自前で組むかフレームワークを使うか」は最初に決めるべき設計判断です。20件未満のテストケースなら自前のpytestで十分。100件を超えると、レポート可視化や差分追跡の機能が欲しくなります。2026年時点で実運用に耐える主要4つを比較します。
項目 promptfoo DeepEval LangSmith Braintrust
主言語 YAML + JS/TS Python Python/TS Python/TS
セルフホスト ○(OSS) ○(OSS) ×(SaaS主体) ×(SaaS主体)
CI連携 ◎(CLI完結) ○(pytest互換) △ ○
本番トレース統合 △ ○ ◎ ◎
料金(2026年) 無料 無料 / 有償plan $39/月〜 $249/月〜
最適な用途 プロンプト回帰CI RAG評価 LangChainユーザー 大規模チーム
私の推奨は、CI回帰検査ならpromptfoo、本番トレース連携が必要ならLangSmithという組み合わせ。両方を同時に使うのは現実的で、promptfooは「リリース前の門番」、LangSmithは「本番の観測点」という役割分担になります。promptfooの公式ドキュメント はYAMLでテストを書く例が豊富で、Python知識なしでもセットアップできます。
CIに組み込むワークフロー
evalは「CIで毎PRごとに回る」状態になって初めて価値が出ます。手動で叩いている限り、忙しいリリース直前にスキップされて事故ります。GitHub Actionsでpromptfooを動かす最小構成は次の通り。
# .github/workflows/evals.yml
name: Prompt Evaluations
on:
pull_request:
paths:
- 'prompts/**'
- 'evals/**'
- 'src/agent/**'
jobs:
eval:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install promptfoo
run: npm install -g promptfoo@latest
- name: Run evals
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
run: promptfoo eval -c evals/promptfooconfig.yaml --output evals/results.json
- name: Fail if pass rate below threshold
run: |
PASS_RATE=$(jq '.results.stats.successes / .results.stats.total * 100' evals/results.json)
if (( $(echo "$PASS_RATE < 90" | bc -l) )); then
echo "Pass rate $PASS_RATE% is below 90%"
exit 1
fi
閾値(上記では90%)はチームのリスク許容度で決めます。私のプロジェクトでは「決定論的アサーション100%・LLMジャッジ85%以上」という二段の閾値を設定しています。ジャッジ側はノイズが乗るので、100%要求は非現実的です。
Note: CIでの実行コストは無視できません。20ケース × LLMジャッジ2回(順序入れ替え)= 40 API呼び出し / PR。Claude Sonnet 4.6で約$0.04〜0.08/PRが目安。週50PRなら月$10程度なので、十分ペイします。
よくある落とし穴と回避策
テストケースを増やしすぎる
500件のevalを揃えても、回帰検出能力は20件の3倍程度にしかなりません。むしろメンテコストが膨らんで、誰もデータセットを更新しなくなります。20〜50件で「カバーしたいリスクの主要パターン」を網羅する方が、長期的に機能します。
ゴールデンデータセットの汚染
プロンプトを書いた本人が正解データを作ると、「自分が書いたプロンプトで通る出力」を正解にしてしまうバイアスが入ります。理想は別のメンバー(できれば実ユーザーに近いPMやサポート担当)が正解を定義すること。
ジャッジモデルの過信
LLM-as-a-Judgeは便利ですが、複雑な推論や数学的検証では人間の判断と50%程度しか一致しません。事実検証が重要なドメイン(医療・金融・法律)では、必ず人間レビューを最終ゲートに残してください。
本番トラフィックを評価データに使わない
合成データだけで作ったevalは、本番の実クエリ分布と乖離します。本番ログから週次でサンプリングしてevalデータセットを更新するパイプラインを組むのが、最大のレバレッジが効くプラクティスです。
よくある質問
LLMエージェントのevalは何件のテストケースから始めるべきですか?
20件で十分です。主要なユースケース、エッジケース、過去にバグが出たケースを2〜3件ずつ揃えれば、リリース時の安心感は劇的に変わります。100件を超えてもメンテコストの方が先に大きくなります。
LLM-as-a-Judgeはどのモデルを使うべきですか?
本番で使うモデルとは別ファミリーを推奨します。本番がGPT-5系ならClaude Sonnet 4.6、本番がClaudeならGPT-5 miniなど。同じモデルファミリーを使うとself-preference biasが発生し、信頼できる評価になりません。
promptfooとDeepEvalはどちらを選ぶべきですか?
CI回帰中心ならpromptfoo(YAML完結でセットアップが速い)、Pythonコードでカスタムメトリクスを多用するならDeepEval(pytest互換でアサーション拡張が柔軟)。RAGの評価メトリクス(faithfulness、context_relevancy)が組み込みで欲しいならDeepEval一択です。
本番で観測したevalスコアが低いとき、まず何を疑うべきですか?
順番に: (1) 評価データセットが本番分布とずれていないか、(2) ジャッジのプロンプトに曖昧さがないか、(3) モデル側のバージョン更新(特にプロバイダー側のサイレントなマイナー更新)。多くの場合(1)が原因です。
ツール呼び出しが含まれるエージェントの評価で気をつけることは?
最終出力だけでなくツール呼び出しシーケンスを必ず検査対象に含めることです。出力が正しくても「不要なツールを3回呼んでいる」「冪等でないツールを並列実行している」といった退行はサイレントに進行するので、トレースベースのアサーションを最初から組んでおきます。