Agent Mentor Learn
検証と品質保証: 「正しそう」をすり抜けさせない · 第 6 回 / 全 6 回

レッスン6: ハンズオン: エージェント用の評価トラックを構築する

学習目標:

  • 評価セット、階層化した採点、ハーネスのループをつなぎ合わせ、繰り返し実行できる eval-runner.mjs にする——評価タスク1件につき独立したループを1本
  • レポートに通過率だけでなく、タスクごとの所要時間、ツール呼び出し回数、トークン消費、ツールエラーも記録させ、それらの列で問題を診断する
  • このトラックでシステムプロンプトの変更が持つ本当の影響を測り、同じトラックで「正しい出力を落とす厳しすぎる検証器」を捕まえて直す

前提: レッスン1〜5を読み終え、本シリーズ7つめのコースのハーネスのループが手元で動かせること | 前: レッスン5 <<

これまでの5つのレッスンはすべて部品でした。ステップごとの照合ではなく終状態を検証する(レッスン2)、決定的チェックを優先し、厳しすぎる検証器に気をつける(レッスン3)、自由記述テキストになって初めて LLM ジャッジの出番(レッスン4)、評価セットは現実のタスク20件ほどから始める(レッスン5)。単独ではどれも筋が通っていますが、プロンプトを変えたあと、コマンド1つで走らせて「良くなったのか悪くなったのか」を数字に語らせる何かが、まだ手元にありません。

このレッスンで部品を溶接します。出来上がるのは300行ほどのファイルで、1回の実行に2秒かかりません。「評価をどう回すか」に対する公式の指針は直截です。直接の LLM API 呼び出しを使い、プログラム的に回すこと。シンプルなエージェント的ループ——LLM 呼び出しとツール呼び出しを交互に行う while ループ——を使い、評価タスク1件につきループを1本回すこと1。これはまさに本シリーズ7つめのコースの、stop_reason で駆動されるあのループです。そのまま移植できます。

動かした様子を先に見る

このレッスンの後半にある eval-runner.mjs 全文をローカルに保存し、node eval-runner.mjs:

text
=== レポート · プロンプト v1 · 検証器 normalized(修正後) ===タスク            判定方式        結果      スコア  呼出  エラー    トークン    所要時間----------------------------------------------------------------------------------------t1-total          決定的          pass        1.00     3       0       1,800       127mst2-pending        決定的          pass        1.00     1       0         995        82mst3-no-orderid     決定的          FAIL        0.00     2       1       1,550       125mst4-refund-note    LLM ジャッジ    FAIL        0.67     1       0       1,432       129mst5-missing-order  決定的          pass        1.00     1       1         966        84ms----------------------------------------------------------------------------------------通過率 3/5 (60%) · ツール呼び出し 8 · ツールエラー 2 · トークン 6,743 · 合計 547ms
未通過の詳細:  [t3-no-orderid] 判定基準: パラメータが不完全なときはツールを一度も呼ばず、注文番号を尋ね返すこと  Agent の回答: 注文 SO-1001 のステータスは完了済みです。  [t4-refund-note] 判定基準: 金額は注文と一致し、トーンも適切。ただし返金の入金時期が書かれておらず、顧客が見通しを持てない。3項目のうち1項目が欠落。  Agent の回答: お世話になっております。ご注文 SO-1003(金額 ¥320.00)のキャンセルを承りました。返金は元のお支払い方法へお戻しいたします。ご不便をおかけし申し訳ございません。
=== レポート · プロンプト v2 · 検証器 normalized(修正後) ===タスク            判定方式        結果      スコア  呼出  エラー    トークン    所要時間----------------------------------------------------------------------------------------t1-total          決定的          pass        1.00     3       0       1,800       126mst2-pending        決定的          pass        1.00     1       0         995        83mst3-no-orderid     決定的          pass        1.00     0       0         487        43mst4-refund-note    LLM ジャッジ    pass        1.00     1       0       1,518       124mst5-missing-order  決定的          pass        1.00     1       1         966        84ms----------------------------------------------------------------------------------------通過率 5/5 (100%) · ツール呼び出し 6 · ツールエラー 1 · トークン 5,766 · 合計 460ms
=== スコアの変化 v1 -> v2 ===タスク                 v1     v2  変化--------------------------------------------------t1-total             1.00   1.00  横ばいt2-pending           1.00   1.00  横ばいt3-no-orderid        0.00   1.00  fail => passt4-refund-note       0.67   1.00  fail => passt5-missing-order     1.00   1.00  横ばい--------------------------------------------------通過率 3/5 -> 5/5

これは手で書いた例ではありません——一時ディレクトリでの実際の実行結果を、そのまま写したものです。全コードをコピーして1回走らせてみてください。「所要時間」の列(実際の壁時計時間で、マシンの負荷によって揺れます)以外は、ミリ秒単位まで一致します。数字が同じなのは、スタブ client が決め打ちの応答を返すからです。

この出力には、このレッスンで扱うことがすべて詰まっています。5件のタスクがそれぞれ自分のループを回し、2種類の採点方式が1つの表に混在し、通過率に加えて4列の診断指標があり、2つのバージョンの差分が1枚の比較表に落ちています。残りのページはこれを解きほぐしていきます。

トラックの5つの部品

  1. 被テストシステム: ツール定義、ツールの実際の実装、そしてそれらの背後にあるデータ。評価が回すのは「エージェントがあなたのツールを使って仕事をする」ことなので、ツールも被テスト対象の一部です。
  2. スタブ client: 決め打ちのキュー順に応答を返す偽の messages.create。これでトラック全体が再現可能になります。
  3. 評価セット: tasks 配列で、各要素は {id, prompt, verify}。公式の要求は、各評価プロンプトを検証可能な応答または結果と対にすることです1——検証器のないプロンプトは評価タスクではなく、ただのお試しです。
  4. 採点: 決定的に判定できるものは verify 関数へ、自由記述テキストになって初めてジャッジへ回します。
  5. ループとレポート: タスク1件につき while ループ1本、終わったら指標を表に集計します。

先に1つ確定させておきます。タスクは messages を共有しません。 各タスクの messages はそのタスクのユーザープロンプトだけから始まり、自分のループを回し、そして捨てられます1。なぜこれがそれほど重要なのかは、中盤のクイズで直接問います。

部品1: ツールとその背後のデータ

被テストシステムは注文アシスタントで、注文が4件、ツールが2つあります。search_orders(顧客名またはステータスで検索し、注文 ID の一覧を返す)と get_order(注文 ID から単一注文の詳細を照会する)です。2つの細部は意図的なものです。search_orders は注文 ID だけを返して金額を返さないので、エージェントは注文ごとにもう一度 get_order を呼ばざるを得ません——レポートの「呼出」列がこの設計上の欠陥を暴きます。もう1つは、2つの絞り込み条件がどちらも空のときにエラーを投げることです。

これが「無効なパラメータ」型のツールエラーです。公式ガイダンスは、この種のエラーがまとまって出るときは、たいていツール説明をより明確にするか例を足すべきだという意味だと述べています1。すぐあとでレポートの中に見ることになります。ツールエラーはクラッシュではありません——ツール実行のブロックが例外を捕まえ、is_error: true を付けた tool_result に包んでモデルに返し、カウンタを1つ増やします。tool_usetool_resulttool_use_id で対応づきます——これは7つめのコースで敷いた土台で、ここではカウンタを2つ足しただけです。

部品2: スタブ client と検証のための幕間

ここで一度立ち止まる必要があります。そうしないと、以下の数字がすべて成り立たなくなります。

本物の Claude は非決定的です。同じプロンプトを2回走らせれば、経路がまったく違うこともあります2。本番では良いことですが、デモのレッスンにとっては致命的です——今日回して 3/5、明日は 4/5 では、その差がプロンプトの変更によるものかモデルの機嫌によるものか判別できません。そこで8つめと9つめのコースのハンズオンと同じ方法を採ります。モデルを、決め打ちのキュー順に応答を返すスタブに差し替えることで、テスト対象の挙動を統制された変数にするのです。これで検証されるのは、あなたが書いた制御ロジックであって、モデルのその日の調子ではありません。

キューが尽きたらエラーを投げ、フォールバックの応答は返しません——ループが1回余計に回れば Error: [stub] v1/t2-pending の応答キューが尽きました(1 回のリクエストを発行済み) がすぐ見えます(これは t2 のキューから最後の応答を削って実際に出したエラー文です)。偽の end_turn がすり抜けることはありません。各応答は自前の latency_ms を持ち、スタブは実際にその時間だけ眠るので、「所要時間」の列はループが何ターン回ったかを測る指標になります。タスクごとに新しい client が自前のスクリプトとともに作られ、カーソルがタスクをまたぐことはありません。

2つのプロンプトバージョンの差は、スタブの2つの応答キューに固定されています。 現実のシナリオならシステムプロンプトを変えればモデルの挙動が追随しますが、ここにはモデルがないので、SCRIPT_V1SCRIPT_V2 をあらかじめ書き、v2 では2つのタスクで異なる応答を返させています——「v2 のプロンプトが効いてモデルはこう答えると仮定する」を、データとして符号化したわけです。

スプレッド構文で v1 を継承し、変わった項目だけを列挙します——コードを読む人が差分の範囲を一目で把握できます。このトラックが検証するのはトラック自身です。検証器は正しく採点するか、指標は正確に記録されるか、レポートの計算は合っているか、2回の実行を比較できるか。実クライアントに差し替えても、トラックは変わりません——変わるのは数字が揺れ始めることだけです。

部品3: 評価セット——通常4件 + エッジケース1件

レッスン5では、評価セットは現実の分布に合わせ、エッジケースをカバーすべきだと述べました3。公式も、十分な複雑さでツールに負荷をかけない単純すぎるサンドボックス環境を避けるよう警告しています1。ここでは紙幅の都合で5件しか収めませんが、構成は本物の評価セットに従っています。

タスク何をテストするか採点
t1-total複数ステップの集計: 一覧を検索し、それぞれの金額を取りに行く決定的
t2-pending集合の絞り込み: 注文 ID がちょうどこれだけ、過不足なし決定的
t3-no-orderidエッジケース: ユーザーが注文番号を出していない決定的
t4-refund-note自由記述テキスト: 顧客への返金案内LLM ジャッジ
t5-missing-orderツールエラーの後、正直に報告し、データを捏造しない決定的

t3-no-orderid は特筆に値します。プロンプトは「あの注文のステータスを確認してください」——どれのことでしょうか。指定されていません。理想の挙動は、適当に1件推測して照会するのではなく、注文番号を尋ね返すことです。公式ドキュメントはこの挙動について慎重です。ユーザーのプロンプトに必須パラメータをすべて埋めるだけの情報がない場合、Claude Opus は欠けているパラメータを認識してそれを尋ねる可能性がはるかに高いが、この挙動は保証されない、とくにより曖昧なプロンプトや能力の低いモデルでは保証されない、と4「保証されない」挙動こそ、評価セットがカバーすべきものです——保証されているものはテストする必要がありません。

verify が受け取る r には answer だけでなく toolCallstoolErrorstokens も入っているので、検証器はテキストだけでなく「終状態と主要な指標」をチェックできます。t3 は実際に「ツールを一度も呼ばなかったこと」を、t5 は「エラーがちょうど1回発生し、見つからないと正直に述べたこと」を確認しています——レッスン2の終状態優先は、これらのフィールドを通じて実現されます。note は人間のためのもので、タスクが落ちたときレポートが判定基準とエージェントの実際の応答を並べて表示します。

レッスン5の宿題で {id, prompt, expected, verifier, rubricRef, tags, split} のフィールド構成を使ったなら、ここで対応づけて混乱を避けてください。レッスン5の verifier はここでは grader と呼ばれ、表示専用です——実際の採点タイプは、そのタスクが verify 関数を持つか judge: true を持つかで決まります。expected の宣言的なアサーションは、ここでは verify 関数の本体に直接書かれます(タスクごとにアサーションの形が違うので、汎用のアサーション形式を設計するより関数として書くほうが簡単です)。rubricRef は、スイート全体でジャッジのケースが1件しかないため JUDGE_PROMPT としてインライン化しました。tagssplit は簡潔さのため省いています。ホールドアウトの規律は「スコープ」の節でいつもどおり繰り返します。レッスン5の JSON が無駄になったわけではありません——それはこの TASKS 配列の宣言的な版です。前に進むということは、各アサーションを関数に翻訳するということです。

部品4: 階層化した採点、決定的が先

採点方法には順序があります。コードベースの採点は最も速く最も信頼でき、極めてよくスケールしますが、複雑な判断ではニュアンスを欠きます。LLM ベースの採点は速く柔軟で複雑な判断を扱えますが、まず信頼できることをテストしてからスケールさせます。人間による採点は最も柔軟で品質も最高ですが、遅くコストがかかるので可能なら避けます3

したがってルールはこうです。コードで採点できるものは決してジャッジに回さない。 ここでは5件のうち4件が verify を使い、ジャッジに回るのはあの自由記述テキストである t4-refund-note だけです——「この文章は顧客に送れるか」は文字列一致では答えられません。ジャッジの形はレッスン4に従います。ルーブリックは3項目に固定、出力フォーマットは JSON に固定、先に根拠、次にスコアです。

どの点にも出所があります。ジャッジには先に根拠を書かせてからスコアを出させ、その推論は捨てる——これは採点の質を上げ、とくに複雑な判断を要するタスクで効きます3。出力は経験的または具体的であるべきで、純粋に定性的な評価にしてはいけません3。そして「単一の LLM 呼び出し、単一のプロンプト、0.0–1.0 のスコアと合否の出力」は、公式が自社のマルチエージェント・リサーチシステムで複数のジャッジ方式を試したうえで、最も一貫し人間の判断とも合致すると分かった組み合わせです2

ここでのジャッジもスタブです。v1 の返信には入金時期が欠けており、3項目のうち2項目で 0.67、判定は fail。v2 はそれを足したので3項目すべてが当たり 1.00、判定は pass です。スコアはルーブリックと自己整合しています——二値の3項目を平均すれば 0、0.33、0.67、1.00 にしか着地しません。0.85 というスコアが出たら、それはジャッジがルーブリックの計算に従わなかったということです。ジャッジ自身もトークンを消費し、その消費はそのタスクのトークンに加算されます。t4 はツールを1回しか呼んでいないのにトークンが低くないのはそのためです。

レッスン4の規律をもう1つ。作業したモデルが自分を採点してはいけません。公式は、まっさらなモデルインスタンスに結果を反証させよ——作業した本人が採点する側になってはいけない、と述べています5。コードの上では、ジャッジは自前の client、自前のシステムプロンプト、自前の messages 配列を使い、タスクのプロンプトと採点対象の返信しか見えず、エージェントのツール呼び出しのトランスクリプトは見えません。

部品5: ループとレポート

ループは7つめのコースのループそのままで、骨格は変えていません——実 API が要求する modelmax_tokens を足し(スタブは無視します)、カウンタで包んだだけです。

messagesrunTask の中のローカル変数です。関数が返れば消えます。それが「タスクはコンテキストを共有しない」の実装のすべてです——追加の仕組みは要らず、外に持ち出さないだけです。

指標について、公式のチェックリストはこうです。トップレベルの正確さに加えて、個々のツール呼び出しとタスクの総実行時間、ツール呼び出しの総数、トークンの総消費量、ツールエラーも集めること1。レポートの表の列は、このチェックリストにそのまま従っています。通過率が教えるのは「通ったかどうか」だけですが、これらの列は「どう通ったか」を教えます——通ったけれどツールを12回呼んだのと、2回で通ったのとでは、品質の水準が2つ違います。これらの列はそれ自体が診断でもあります。冗長なツール呼び出しが多いのは、たいていページングやトークン上限のパラメータを適正化すべきというサインです。無効なパラメータによるツールエラーが多いのは、たいていツール説明をより明確にするか、より良い例を足すべきというサインです1。演習ではこれを直接使います。

レポートが人間に読める形であること自体に価値があります。公式の提案はこうです。Claude には成功を主張させるのではなく証拠を示させること——テストの出力、実行したコマンドとその戻り値、あるいは結果のスクリーンショット。証拠を確認するほうが自分で検証を回し直すより速く、見ていなかったセッションにも効きます5。このレポートの表がその証拠です——PR の説明に貼るなり同僚に送るなりすれば、相手は回し直さずに判断できます。(表示上の唯一の落とし穴は、CJK の全角文字が幅2としてカウントされることです。素の padEnd では桁がずれるので、コードには幅を考慮した pad を用意してあります。)

eval-runner.mjs 全文

コピーして eval-runner.mjs として保存し、node eval-runner.mjs でそのまま動きます。依存なし、package.json なし、Node 18 以上(トップレベル await を使うので拡張子は .mjs である必要があります)。

レッスン3の罠を回収する: 厳しすぎる検証器

レッスン3である罠を扱いました。公式の言葉そのままでは、フォーマット、句読点、妥当な言い換えといった本質的でない差異のせいで正しい応答を却下してしまう、厳しすぎる検証器を避けること1。常識のように聞こえますが、コードの上ではほぼ避けられません。厳しすぎる検証器が、いちばん書きやすいからです。

このトラックには1つ埋め込んであります。t1-total には検証器のバージョンが2つあり、旧版は pass: r.answer.includes("1280.00") です——盤石に見えます。正解は 1280.00 なのだから、回答にその文字列が含まれるかを見ればいい、と。node eval-runner.mjs --strict-verify を走らせてみます(以下は v1 のレポートだけを貼っています。v2 のレポートと差分表はいつもどおり出力されます)。

text
=== レポート · プロンプト v1 · 検証器 strict(旧・正規化なし) ===タスク            判定方式        結果      スコア  呼出  エラー    トークン    所要時間----------------------------------------------------------------------------------------t1-total          決定的          FAIL        0.00     3       0       1,800       126mst2-pending        決定的          pass        1.00     1       0         995        83mst3-no-orderid     決定的          FAIL        0.00     2       1       1,550       125mst4-refund-note    LLM ジャッジ    FAIL        0.67     1       0       1,432       125mst5-missing-order  決定的          pass        1.00     1       1         966        83ms----------------------------------------------------------------------------------------通過率 2/5 (40%) · ツール呼び出し 8 · ツールエラー 2 · トークン 6,743 · 合計 542ms
未通過の詳細:  [t1-total] 判定基準: 回答に 1280.00 という文字列がそのまま含まれること  Agent の回答: 顧客「啓明テック」の 2026年8月の完了済み注文は2件(SO-1001、SO-1002)で、合計 ¥1,280.00 です。  [t3-no-orderid] 判定基準: パラメータが不完全なときはツールを一度も呼ばず、注文番号を尋ね返すこと  Agent の回答: 注文 SO-1001 のステータスは完了済みです。  [t4-refund-note] 判定基準: 金額は注文と一致し、トーンも適切。ただし返金の入金時期が書かれておらず、顧客が見通しを持てない。3項目のうち1項目が欠落。  Agent の回答: お世話になっております。ご注文 SO-1003(金額 ¥320.00)のキャンセルを承りました。返金は元のお支払い方法へお戻しいたします。ご不便をおかけし申し訳ございません。

これも実際の実行結果です。t1-total の詳細を見てください。エージェントは「合計 ¥1,280.00」と答えています——金額は正しく、注文も正しく、言い回しも普通です。唯一の罪は 1 と 280 のあいだに桁区切りのカンマを入れたことで、そのせいで includes("1280.00") が false を返し、完全に正しい回答が fail と採点されました。

ここで直すのは検証器であって、エージェントではありません。 レポートが教えるのは「t1 が fail」までで、誰のせいかは教えてくれません。見分ける方法は、詳細に載っているエージェントの実際の言葉を読むことです——レポートが生の回答を出力するのは、まさにそのためです。直し方は正規化です。完全一致についての公式の説明にも、すでにこの手順が含まれています。完全一致の評価は、通常は空白と大文字小文字を正規化したうえで、モデルの出力があらかじめ定めた正解と一致するかを測ります3。金額のシナリオではもっと洗う必要があります——通貨記号、桁区切り、単位。そこで修正後の検証器は先にノイズを洗い落とし、数値を取り出して数値として比較します。

--strict-verify を外してもう一度走らせると、t1-total は 0.00 から 1.00 に反転し、v1 のベースラインは 2/5 から 3/5 に戻ります——その間、エージェントは1文字も変わっておらず、スタブの応答キューも1文字も変わっていません。スコアは変わったのに被テストシステムは変わっていない——これが「検証器の問題」を見分けるリトマス試験紙です。

スコープについて余談を1つ。正規化は緩ければ緩いほど良いわけではありません。「1280 が含まれていそうなら合格」まで緩めると、エージェントが「注文は合計 1280 件、金額は不明」と答えても通ってしまいます。検証器は「無関係な差異は通し、実質的な誤りは止める」位置に座るべきで、その位置を見つける唯一の方法は実際の回答で試すことです。

プロンプトを1か所変えて、スコアが動くのを見る

トラックの較正が済んだので、本番の作業に入れます。私が変えたのは1か所だけ——システムプロンプトに、v1 のあとへルールを2つ足しました。

この2つはでっち上げではありません。v1 のレポートの「未通過の詳細」から読み取ったものです。t3 はパラメータが不完全なのに注文番号を推測したせいで落ち、t4 は入金時期がないせいで減点されました。レポートが言ったところを、あなたが変える——トラックがある場合とない場合の、最も具体的な違いがこれです。トラックがなければ、プロンプトを変えたあと出力を眺めて「良くなった気がする」と思うだけです。トラックがあれば、「どれが良くなり、どれが横ばいで、後退したものはあるか」が数字3行になります。

再実行すると、差分表は冒頭の出力の最後の区画です。通過率は 60% から 100% へ、2件のタスクが fail から pass に反転し、残る3件は動きません。この後半の半文は前半と同じくらい重要です——この変更が、すでに動いていたものを壊さなかったと言っているからです。トラックがなければ、プロンプトを変えたあと出力を一度見て「良くなったように見える」と思うだけです。トラックがあれば、「どれが改善 / どれが横ばい / 後退はあるか」が数字の3行になります。

公式の言い回しではこうです。評価があれば、プロンプトエンジニアリングの影響をより確信を持って測定できる。ツール説明のわずかな改良でさえ、劇的な改善をもたらすことがある1。ここにはつかみ取れるお買い得もあります。エージェント開発の初期では、手の届く果実がまだ豊富にあるため変更は劇的な影響を持つ傾向があり——プロンプトを1回いじるだけで成功率が 30% から 80% に上がることもあり、これほど効果量が大きければテストケースが数件あるだけで変化を見つけられます2。いまタスクは5件しかありませんが、それは不足ではなく出発点です。

指標の列をもう一度見てください。v2 ではツール呼び出しが 8 から 6 に、ツールエラーが 2 から 1 に減り、トークンも1000近く下がっています。t3 がもう闇雲に推測してツールを呼ばなくなったからです。同じ変更が精度とコストを同時に改善した——こういうことは、これらの列を一緒に記録して初めて見えるようになります。

スコープ: このトラックが面倒を見るもの、見ないもの

面倒を見るもの: エージェント1つ、タスク1群、自分のマシンで1回走らせ、人間に読めるレポートを出す。

実モデルへの差し替え——トラックの構造は変わりません。stubClient(...)@anthropic-ai/sdk の実クライアントに置き換えるだけです。runTask の中の while ループは1行も変わりません——すでに実 API の stop_reason / tool_use / tool_result の形に合わせて書いてあり、必須パラメータの modelmax_tokens もすでに入っています(スタブは無視し、実クライアントは使います)。差し替え後に変わることが2つあります。エージェントは同一のプロンプトでも実行のたびに非決定的なのでスコアは揺れます2。1回の実行を読み込みすぎないでください。そして1周回すのにお金と時間がかかります。5件なら気になりませんが、200件なら並列度とコストを考える必要があります。

面倒を見ないもの: 評価を CI につなぎ、コミットごとに実行し、過去のバージョンと比較し、スコアがしきい値を下回ったらマージをブロックする——これらはよくあるエンジニアリングの実践で、実際うまく機能しますが、このレッスンでは展開しません。演習のレベル2で「2つのレポートを比較する」ところまでは一緒にやります。残りの段取りはあなたの CI の仕事です。

レッスン5の規律をもう1つ繰り返します。ホールドアウトセットに向けてチューニングしないこと。 レポートに従ってプロンプトを変えていけば、数ラウンドでスコアは確実に上がります。しかしその上昇は「この5件のタスクでのスコア」でしかないかもしれません。公式の実践は、「訓練用」の評価に過適合していないことを確かめるためにホールドアウトのテストセットに頼ることです1。ですから実際のセットアップでは、タスクを2つの山に分けるべきです。1つは日々回して方向を示す山、もう1つは鍵をかけておき「このバージョンなら大丈夫だろう」と思ったときにだけ開ける山です——前者のスコアは航法で、後者のスコアは判定です。

最後に古い注意をもう1つ。自動評価は見逃します。人間のテスターは、評価が見逃すエッジケースを必ず引き当てます——珍しいクエリでの幻覚、システム的な障害、微妙なソース選択のバイアスです2。トラックが順調に回っていることは、自分で使うのをやめてよいという意味ではありません。

💻 演習

まとめ

  • 評価の標準的な回し方は、プログラムからの直接 API 呼び出しと、シンプルなエージェント的ループ——評価タスク1件につきループ1本です。タスクは messages を共有しません。共有すれば前のタスクのコンテキストが次を汚染し、結果が比較できなくなります1
  • 各評価プロンプトは検証可能な結果と対にすべきで、検証器は完全一致の文字列比較からモデルに判定させるものまでのスペクトラムをなします——コードで採点できるものは決してジャッジに回しません。コードベースの採点が最も速く、最も信頼でき、極めてよくスケールするからです1 3
  • 自由記述テキストはジャッジへ。形は単一の呼び出し、単一のプロンプト、0.0–1.0 のスコアと合否の出力です。ルーブリックは先に根拠、次にスコアで、出力フォーマットは固定します2 3
  • レポートは通過率のほかに、タスクの所要時間、ツール呼び出し回数、トークン消費、ツールエラーを記録しなければなりません。これらの列はそれ自体が診断です——冗長な呼び出しはページング / 返却量のパラメータの調整を、無効なパラメータのエラーはツール説明の明確化を指します1
  • 厳しすぎる検証器は正しい回答を却下します。フォーマット、句読点、妥当な言い換えのどれもが文字通りの比較をつまずかせます。完全一致の前に正規化を行ってください1 3。スコアは変わったのに被テストシステムは変わっていない——それは検証器のせいです。
  • トラックがあればプロンプト変更の影響が測定可能になり、わずかな改良でも劇的な改善をもたらすことがあります。序盤は効果量が大きいので、数件のケースで差を見つけられます1 2。レポート自体が他人にレビューされうる証拠であり、自分で検証を回し直すより速く、見ていなかったセッションにも効きます5
  • レポートに従ってプロンプトを変えればスコアは上がりますが、その上昇はこのタスク群の上でだけかもしれません。ホールドアウトセットに鍵をかけて過学習を防ぎましょう1。自動評価には盲点があり、人間のテスターは依然として評価が見逃すエッジケースを捕まえます2

このコースを終えたあとに

振り返ると、本筋はじつは短いものでした。レッスン1は「完了に見える」と「完了している」を切り分けました——実行できるチェックがなければ「完了に見える」が唯一のシグナルであり、あなた自身が検証の工程になってしまいます5。レッスン2は何を検証するかを定めました——エージェントは同じゴールにまったく違う妥当な経路で到達しうるので、終状態を評価し、軌跡をステップごとに照合しないこと2。レッスン3は「チェック」を、合否を出力する実行可能な決定的検証器に落とし、同時に厳しすぎる検証器が正しい回答を却下することも警告しました1。レッスン4は自由記述テキストを扱いました——ルーブリック、出力フォーマット、そして作業したモデルが自分を採点してはいけないこと2 5。レッスン5は「何件で検証するか」を解きました——現実のタスク20件ほどで始められる、数百件たまるのを待つ必要はない2。そしてこのレッスンが、前の5つを300行のファイルに溶接しました。

そのファイルは複雑ではなく、実行に2秒もかかりませんが、変えるものは具体的です。今日からプロンプトのバージョンを変えたとき、「出力を何段落か読んで良くなった気がする」で判断する必要はありません——コマンドを1つ走らせれば、v1 から v2 への差分表があなたの代わりに語ります。ちょうど今回、t3t4 が緑になり、残る3件が横ばいだったように。次にあなたのエージェントが「完了しました」と言ったとき、その主張を検証するためのコマンド2つと終了コード1つが、あなたの手元にあります。

次にあなたのエージェントが「完了しました」と言ったとき、それを検証できる実行可能なトラックが、あなたにはあります。

Footnotes

  1. Writing effective tools for agents — with agents — Anthropic Engineering — https://www.anthropic.com/engineering/writing-tools-for-agents 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17

  2. How we built our multi-agent research system — Anthropic Engineering — https://www.anthropic.com/engineering/multi-agent-research-system 2 3 4 5 6 7 8 9 10 11

  3. Define success criteria and build evaluations — Claude API documentation — https://platform.claude.com/docs/en/test-and-evaluate/develop-tests 2 3 4 5 6 7 8

  4. Tool use with Claude — Claude API documentation — https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview

  5. Best practices for Claude Code — Claude Code official documentation — https://code.claude.com/docs/en/best-practices 2 3 4 5

練習

01

コードは不要です。レッスン冒頭の2つのレポート(v1 のベースラインと v2 の変更後)に戻ってください。サマリー行はこうでした。

レベル1: レポートを読む。急いでコードを直さない
text
v1: 通過率 3/5 (60%)  · ツール呼び出し 8 · ツールエラー 2 · トークン 6,743v2: 通過率 5/5 (100%) · ツール呼び出し 6 · ツールエラー 1 · トークン 5,766

2つの表の全体を見ながら、次の3つの問いにそれぞれ3〜5文で答えてください。

  1. t1-total はどちらのレポートでも通過していますが、ツール呼び出し回数が 3 で最多です。これは何の問題を示していますか。何を変えるべきですか。
  2. v1 のツールエラーは 2、v2 は 1 です。この2つのエラーは同じ種類の問題ですか。それぞれ何を意味し、それぞれ直すべきですか。
  3. このレッスンには3つめのレポート(--strict-verify のもの)があり、そこでは t1-total が 0.00 です。同じタスクが一方のレポートで 0.00、もう一方で 1.00——このスコアの差がエージェントではなく検証器の問題だと、どうやって見分けますか。
完了基準 · ローカルでチェック
02

コードを書きます。動くものであること。eval-runner.mjs に2つ足してください。

レベル2: トラックに「2回の実行の比較」を足す
  1. レポートの永続化: writeJsonAtomic(file, obj) を追加し、9つめのコースのアトミック書き込み(先に .tmp に書いてから rename)で、1回の実行のレポートを JSON として保存します。コマンドラインは --version v1 --out reports/v1.json をサポートすること。
  2. compare.mjs を書く: 2つのレポート JSON を読み、タスクごとのスコア差分(ベースラインのスコア、新しいスコア、差分、ステータス)を表示し、最後に通過率の変化を表示します。pass から fail に転じたタスクが1件でもあれば、サマリーを stderr に出し、非ゼロのコードで終了すること。

次の4つのコマンドを実行し、出力を貼ってください。

text
node eval-runner.mjs --version v1 --out reports/v1.jsonnode eval-runner.mjs --version v2 --out reports/v2.jsonnode compare.mjs reports/v1.json reports/v2.json   # 終了コードは 0 のはずnode compare.mjs reports/v2.json reports/v1.json   # 終了コードは 1 のはず

(引数の順序を入れ替えるのは「新しいバージョンがベースラインより悪い」状況の再現で、非ゼロ終了の経路が本当に動くことを検証するためです。)

完了基準 · ローカルでチェック