SSD Nodes Learn 🎉 VPS $4.99/月〜
ガイド Matt Connor著者 Matt Connor

AIエージェントのセルフホストeval入門

実際のトレースからgolden caseを作り、決定的チェックとLLM判定を組み合わせます。commitごとに合格率をSQLiteへ保存し、60件中58件から51件への低下も追跡できます。

AI エージェント向けのセルフホスト eval とは

AI エージェント向けのセルフホスト eval は、自分のリポジトリで管理する 4 つの要素です。保存したケースのファイル、ケースに対してエージェントを実行するスクリプト、各回答を評価するチェック群、そしてクエリ可能な結果テーブルです。この構成にベンダーは必要ありません。一連の処理は、数百行の Python と 1 つの SQLite ファイルで実装できます。

デモでエージェントが動作したのは、入力を 5 件、自分で選んだからです。2 週目に壊れたのは、プロンプトの 1 行、モデル、またはツールの説明が変わったのに、それらを測定する仕組みがなかったためです。eval ループを使うと、「今は悪くなった気がする」という感覚を、「commit 4f1c9ab で 60 件中 58 件だった合格率が、60 件中 51 件になった」という測定結果に変えられます。

このループには 4 つの手順があります。このガイドでは、各手順を 1 セクションで説明します。実際のトレースを収集し、重要なものをケースに昇格し、変更のたびにすべてのケースを評価し、合格率をその結果を生成した commit とともに保存します。エージェントを何に対して実行する場合でも、同じループを利用できます。実行する価値のあるセルフホストのエージェントフレームワークの主な違いは、トレースのどの範囲を標準で提供するかです。

2 週目にエージェントが機能しなくなる理由

エージェントは、プロンプト、モデル、ツール定義、実行時に取得されるコンテキストで構成されます。この4つはアプリケーションコードを変更しなくても変わるため、通常のコードレビューでは問題を検出できません。

最も一般的な原因は、プロンプトの編集です。無礼な応答を止めるために、1 文を追加したとします。その文によって、再テストしていない入力に対する動作も変わります。トレースにはその違いが明確に現れます。同じ質問に対する先週のトレースには create_refund ツール呼び出しが含まれているのに、今週のトレースには含まれず、代わりに丁寧な謝罪が返されます。エラーは発生していないため、アラートも発火しません。

2 つ目の原因はモデルです。各実行で送信した正確なモデル文字列を記録してください。頭の中で使っている略称ではなく、claude-haiku-4-5-20251001 を記録します。モデルを切り替えた日に成功率が低下した場合でも、モデル名が記録に含まれていなければ原因を特定できません。

3 つ目はツールです。ツールの説明を書き換えると、モデルがツールを呼び出すタイミングが変わります。ツールが VPS 上で動作する MCP サーバー 経由で提供されている場合、スキーマは別のプロセスに存在します。そのため、リポジトリに差分がまったくなくても、スキーマが変更される可能性があります。4 つ目は検索による取得です。同じ質問でも、一晩のうちに再構築されたインデックスが参照され、新しいドキュメントに基づいて回答が生成されます。

すでに収集しているトレースから基準セットを作成する

評価ケースを新たに作らないでください。トラフィックから取得します。すでに エージェント用のセルフホスト Langfuse トレーシング を実行している場合、各リクエストは入力、ツール呼び出し、出力とともに保存されています。これはケースに必要な生データそのものです。

パブリック API から、一定期間の root オブザベーションをエクスポートします。認証には Basic 認証を使用し、ユーザー名に public key、パスワードに secret key を指定します。

export LF_HOST="https://langfuse.example.com"
curl -sS -u "$LF_PUBLIC_KEY:$LF_SECRET_KEY" \
  "$LF_HOST/api/public/v2/observations?limit=50&isRootObservation=true&fromStartTime=2026-07-01T00:00:00Z" \
  | jq '.data[0]'

パース処理を書く前に、レコードを 1 件読み取ってください。行は data の下に返されます。ただし、質問と回答を保持するフィールド名は、エージェントがスパンを計測する方法によって異なります。想定したフィールド名ではなく、実際に確認したフィールドを対応付けてください。その後、evals/cases.jsonl に 1 行 1 JSON オブジェクトの形式でケースを手動で記述します。

{"id": "refund-double-charge", "tags": ["smoke"], "input": "I was charged twice for order 41822.", "must_call": ["lookup_order", "create_refund"], "must_not_include": ["I cannot help"], "rubric": "The reply confirms exactly one refund for order 41822 and states the amount."}

次の 5 つのルールに従うと、実行する価値のあるセットになります。

  • 開始時は 40〜80 ケースで十分です。20 未満では、1 件の不安定なケースによって合格率が 5 ポイント変動します。理由なく変動する数値は無視されるようになります。
  • 本番環境のバグを修正したら、修正した当日に必ずケースへ追加します。この習慣によって、セットが適切な方向に成長します。
  • 1 ケースにつき 1 つの動作だけを確認します。返金額と応答のトーンを同時に確認するケースでは、失敗しても原因が分かりません。
  • id は変更しません。この ID によって、今回の実行結果と先月の実行結果を比較できるためです。
  • コミットする前にマスキングします。このファイルは git に格納されるため、顧客名と、自分が所有していない注文番号を削除してください。

まず決定的なチェックで評価します。これらは無料で実行できます。

正解が明確な項目には、単純なアサーションを使います。モデル呼び出しは不要で、コストも曖昧さもありません。決定的なチェックでは構造上のリグレッションを検出できます。こうしたリグレッションは、エージェントを取り巻くシステムを壊します。JSON を解析できない、ツールが一度も呼び出されていない、禁止されたフレーズが再び出現する、回答に出典がない、といった問題です。

エージェントを把握する関数は 1 つだけにします。ハーネスのその他の部分はすべて汎用にします。

import json, os, urllib.request

def run_agent(case):
    req = urllib.request.Request(
        os.environ["AGENT_URL"],
        data=json.dumps({"input": case["input"]}).encode(),
        headers={"content-type": "application/json"},
    )
    with urllib.request.urlopen(req, timeout=120) as resp:
        return json.load(resp)


def deterministic(case, result):
    text = result.get("output", "")
    called = [c["name"] for c in result.get("tool_calls", [])]
    failures = []
    for tool in case.get("must_call", []):
        if tool not in called:
            failures.append(f"tool not called: {tool}")
    for phrase in case.get("must_not_include", []):
        if phrase.lower() in text.lower():
            failures.append(f"forbidden phrase: {phrase}")
    if len(called) > case.get("max_tool_calls", 12):
        failures.append(f"too many tool calls: {len(called)}")
    return failures

そのリストにはツールの予算も含めます。あるケースを今日 3 回の呼び出しで解決し、明日は 11 回必要になった場合、最終的な回答が正しくてもリグレッションです。エージェントが行う呼び出しにはすべてコストがかかるためです。

LLM を判定者として使う場合と、失敗する4つのパターン

アサーションを通過したものには、内容を読む採点者が必要です。LLM による判定は、2 回目のモデル呼び出しです。質問、エージェントの回答、1 つの評価基準を受け取り、判定結果を返します。「回答がユーザーの質問に答えているか」を評価する現実的な方法は、これだけです。

判定者を実用的にするルールは4つあります。

  • 判定は二値にし、1 から 10 の点数は使いません。段階評価では、ほぼすべてに 7 や 8 を返すため、数値が変わらず、そこから何も学べません。
  • 1 回の呼び出しにつき、評価基準は1つにします。返金額について尋ねるか、文体について尋ねます。両方を同時に尋ねてはいけません。
  • ケースに期待される回答がある場合は、必ず判定者に渡します。抽象的に評価するより、参照回答と照合するほうがはるかに簡単です。
  • 出力形式を固定し、厳密にパースします。
from anthropic import Anthropic

client = Anthropic()  # reads ANTHROPIC_API_KEY from the environment


def judge_prompt(case, output):
    return (
        "You grade one answer against one criterion.\n"
        "Reply with JSON only, in this exact shape:\n"
        '{"verdict": "pass", "confidence": "high", "reason": "one short sentence"}\n'
        f"Criterion: {case['rubric']}\n"
        f"Question: {case['input']}\n"
        f"Answer: {output}\n"
        "Length is not a criterion. Judge only the criterion above."
    )


def judge(case, output, model):
    msg = client.messages.create(
        model=model,
        max_tokens=200,
        messages=[{"role": "user", "content": judge_prompt(case, output)}],
    )
    return json.loads(msg.content[0].text)

次に、失敗パターンを確認します。それぞれ午後のうちに実行できるテストがあります。テストは重要です。検証していない判定者は、精密に見えて意味のない数値を生成するためです。

長さバイアス。 回答が長いほど、合格しやすくなります。テスト方法は次のとおりです。判定者が不合格にした回答を10件選び、それぞれに新しい事実を追加しない、確信に満ちた埋め草の段落を2つ追加して、もう一度判定します。合格に変わる判定があれば、それは長さバイアスです。修正すべきなのは評価基準です。

自己選好。 判定者は、自分と同じモデル系列の出力を、別の系列の出力より寛大に評価することがあります。テスト方法は次のとおりです。同じ30件の回答を、異なる2つのモデル系列の判定者で評価し、ケースごとに判定結果を比較します。結果が異なるケースは、自分で内容を確認します。

位置バイアス。 判定者を使って2つの回答 A と B を比較する場合は、順序を入れ替えてもう一度実行します。入れ替えによって判定が変わる場合、その評価基準ではまだペア比較を安全に使えません。

評価基準のずれ。 曖昧な基準では、何でも合格にする判定者ができます。「回答は役に立つか」では、ほとんど何でも合格します。「回答は返金額をドル単位で示しているか」なら、意図した内容だけが合格します。すべての基準を、確認する事実が明確になるまで書き直します。

4つすべてに有効な対策が1つあります。手作業でラベル付けしたケースを30件維持し、判定モデルまたは判定プロンプトを変更するたびに、そのラベルと判定結果を比較します。10件中1件を超えて自分のラベルと異なる場合は、生成された合格率を信頼する前に評価基準を修正します。判定者はコードです。コードと同じようにバージョン管理し、レビューします。

安価なモデルで判定し、必要な場合だけ最先端モデルへエスカレーションする

すべてのコミットで、すべてのケースを最も高価なモデルで判定すると、評価費用が評価対象のエージェント自体の費用を上回ります。判定モデルを価格順に並べ、回答が明確になった時点で打ち切ります。

ChartCost to judge 1,000 eval cases, list prices, August 2026
The data behind this chart
[
  {
    "label": "Haiku 4.5, Batch API",
    "usd_per_1000_judge_calls": "0.90"
  },
  {
    "label": "Haiku 4.5",
    "usd_per_1000_judge_calls": "1.80"
  },
  {
    "label": "Sonnet 5",
    "usd_per_1000_judge_calls": "3.60"
  },
  {
    "label": "Opus 5",
    "usd_per_1000_judge_calls": "9.00"
  }
]

これらの数値は、1 回の判定呼び出しで入力約 1,200 トークン、出力 120 トークンを使用する前提です。1 つの質問、1 つの回答、1 つの基準を判定する場合の現実的な規模です。1,000 ケースを判定すると、Claude Haiku 4.5 では 1.80 US dollars、Claude Opus 5 では 9.00 かかります。差額は小さく見えますが、件数を増やすと無視できません。60 ケースを毎回のコミットで判定し、週に 40 回コミットする場合、夜間ジョブを実行する前から、週 2,400 回の判定呼び出しが発生します。

評価作業では、2 つの割引を無理なく適用でき、併用も可能です。評価の実行は対話的ではありません。そのため、非同期配信と引き換えに Batch API を使うと、入力と出力の料金がどちらも半額になります。これがグラフの 1 本目の棒です。ルーブリックと指示はすべての呼び出しでバイト単位まで同一なので、prompt caching も適しています。キャッシュ読み取りは通常の入力料金の 10 分の 1 で、5 分間のキャッシュ書き込みは通常の入力料金の 1.25 倍です。そのため、1 回でも再利用すればキャッシュの費用を回収できます。これらは 2026 年 8 月時点の Anthropic の公開価格です。Sonnet 5 は 2026 年 8 月 31 日まで導入価格で提供されるため、3 本目の棒はその後に高くなります。

判定を次の順序で行います。

  • すべてのケースで決定的なチェックを実行する。API 費用はかかりません。
  • それらのチェックを通過したケースを、小規模モデルで判定する。
  • 小規模モデルが不合格と判定したケース、または合格と判定したものの確信度が低いケースだけを、最先端モデルで判定する。
  • 週に 1 回、少数のサンプルを人手で確認する。
CHEAP = "claude-haiku-4-5-20251001"
STRICT = "claude-opus-5"


def grade(case, result):
    hard = deterministic(case, result)
    if hard:
        return False, "deterministic", "; ".join(hard)
    first = judge(case, result["output"], CHEAP)
    if first["verdict"] == "pass" and first["confidence"] == "high":
        return True, CHEAP, first["reason"]
    second = judge(case, result["output"], STRICT)
    return second["verdict"] == "pass", STRICT, second["reason"]

この方法では判定精度の一部をコストと引き換えにするため、想定で済ませず、実際の差を測定します。月に 1 回は、厳格な判定モデルでも全セットを評価し、2 つの結果を比較します。少数のケースを超えて結果が一致しない場合は、ルーブリックが小規模モデルには曖昧すぎます。修正すべきなのはルーブリックです。エージェント自体の支出を制御する作業は別に必要です。詳しくは VPS 上の AI エージェントのコスト制御 を参照してください。

所有権を持つシステムで合格率の推移を追跡する

コミットと関連付けられない合格率は、単なる印象にすぎません。ケースごと、実行ごとに 1 行を保存し、その行にコミットとモデルを記録します。

CREATE TABLE IF NOT EXISTS results (
  run_id      TEXT NOT NULL,
  ran_at      TEXT NOT NULL,
  git_sha     TEXT NOT NULL,
  agent_model TEXT NOT NULL,
  case_id     TEXT NOT NULL,
  passed      INTEGER NOT NULL,
  graded_by   TEXT NOT NULL,
  reason      TEXT
);
SELECT run_id, git_sha, agent_model,
       count(*) AS cases,
       round(100.0 * sum(passed) / count(*), 1) AS pass_pct
FROM results
GROUP BY run_id
ORDER BY ran_at DESC
LIMIT 10;

sqlite3 evals/results.db < evals/schema.sqlでスキーマを読み込み、sqlite3 -box evals/results.db < evals/passrate.sqlで推移を確認します。60 ケースを毎日実行する場合、1 年分でも約 22,000 行です。そのため、データストア自体が独立したプロジェクトになることはありません。VPS で SQLite を本番運用するでは、このファイルを複数のマシンで共有する場合に重要になる設定を説明しています。

runner は、同じ情報を人間が読める形式でも出力します。

run 2026-08-05T09:14:22Z  sha 4f1c9ab  model claude-sonnet-5  58/60 pass (96.7%)
FAIL refund-double-charge  deterministic: tool not called: create_refund
FAIL pto-policy-question   judge(opus): reply gives no dollar amount

エージェントに影響する変更に対してスイートを実行します。対象は、リポジトリ内のすべてのコミットではなく、プロンプトの編集、モデルの変更、ツールの変更など、エージェントを壊す可能性がある変更です。高速なサブセットには pre-push hook を使用できます。

cat > .git/hooks/pre-push <<'EOF'
#!/bin/sh
python3 evals/run.py --set smoke || exit 1
EOF
chmod +x .git/hooks/pre-push

完全な実行は時間がかかるため、スケジュールで実行します。夜間に VPS 上の systemd service と timer を実行し、デプロイ済みのプロンプトに対して全ケースを実行します。これにより、ホスト型ツールの挙動が変わった場合など、リポジトリ外から入った変更も検出できます。

人手によるレビューは、全件ではなくサンプルで実施する

判定器は人手のラベルを基準に調整するため、誰かがそのラベルを作成する必要があります。毎週、サンプルを確認します。判定器が失敗したすべてのケースに加え、合格したケースから無作為に10件を選びます。無作為に選んだ合格ケースが重要です。判定器が悪い回答をひそかに合格させ始めると、判定器自身の判定結果で作成したダッシュボードでは、常に正常に見えるためです。

1ケース3分で15ケースなら、週45分です。その結果、あなたと判定器の意見が一致しない評価基準の修正と、誰も想定していなかった失敗種別の新しいケースが得られます。人手による判定結果を同じテーブルに、graded_byhuman に設定して記録します。判定器と人手の一致を、記憶ではなくクエリで確認できるようになります。

評価ハーネス自体で発生する問題

最初の全件実行での anthropic.RateLimitError 60 件を一度に並列実行すると、利用中の tier のリクエスト上限またはトークン上限を超えます。同時実行数を 4 worker に制限し、夜間実行は Batch API に移行します。

judge からの json.JSONDecodeError: Expecting value: line 1 column 1 (char 0) モデルが文章で回答したか、JSON をコードフェンスで囲みました。1 回だけ再試行し、その後も失敗した場合はケースをエラーとして記録します。解析失敗を合格として扱ってはいけません。エラーを合格に変換する suite では、agent の品質が低下しているのに合格率だけが 100% に近づくためです。

不安定なケース。 agent が出力をサンプリングするため、同じ入力でも実行ごとに合格したり失敗したりします。不安定なケースは 3 回実行し、削除せずに合格した割合を記録します。3 回中 2 回合格するケースは、実際の堅牢性の不具合です。顧客もその問題を見つけます。

Golden set の劣化。 suite を合格にするために、誰かが期待される回答を編集することがあります。evals/cases.jsonl への差分は agent への差分と同じように慎重にレビューします。このファイルが、正しい結果を定義する文書だからです。

一度も失敗しない suite。 合格率が 1 か月間 100% のままなら、その set は製品の状態を追跡できなくなっています。最近の trace を 10 件取り出し、agent の処理が不十分だったものを見つけて追加します。

FAQ

AI agent の評価セットには何件必要ですか?

40〜80件から始め、実際の失敗を基にセットを拡張します。20件を下回ると、1件の不安定な結果だけで合格率が5ポイント変動するため、数値の情報量がなくなります。数百件を超えると、実行のたびに実際の費用と時間がかかる一方、追加したケースによるカバレッジの増加は小さくなります。重要なのは件数ではありません。既知の本番障害の種類のうち、少なくとも1回はセットに含まれているものの割合です。

LLM judge に agent の採点を任せてもよいですか?

自分で付けたラベルとの比較による測定を行ってからにしてください。手作業で採点した30件を用意し、judge model または judge prompt を変更するたびに、それらを使って judge を評価します。judge には長さのバイアスがあります。冗長な回答ほど合格しやすくなる傾向です。また、自己選好もあります。自分と同じ model family の出力をより好意的に採点する傾向です。どちらも検証できます。失敗した回答に文を追加して再度採点するか、別の model family の judge で同じ回答を採点します。judge が10件に1件を超えて自分のラベルと食い違う場合、その rubric は曖昧すぎて使用できません。

evals を採点する model はどれを選ぶべきですか?

安価な方法から始め、必要な場合だけ上位の model に切り替えます。決定的な assertion は費用がかからないため、すべてのケースで最初に実行します。明確な合格判定には小規模な model を使用します。失敗と信頼度の低い判定だけを frontier model に送ります。2026年8月の定価では、1,000件の採点にかかる費用は Claude Haiku 4.5 で約 1.80 US dollars、Claude Opus 5 で約 9.00 です。eval の実行は非同期であるため、Batch API を使えばどちらも費用を半分にできます。

evals は本番監視の代わりになりますか?

いいえ。両者は異なる質問に答えるためです。eval suite は、これからリリースする変更によって、固定されたケース群の結果が改善するか悪化するかを示します。Tracing と monitoring は、どのケースにも含まれていない入力を含め、実際のユーザーが現在どのような処理を実行しているかを示します。両者は相互に補完します。trace から新しいケースを追加し、eval suite で修正が実際に機能したかを確認します。