LLMエージェントの「評価する側」を作る最小実装 — text-to-SQLエージェントを題材に、トレース設計から評価器の多層化まで / 2026年8月
LLMエージェントの「評価する側」を作るための最小実装です。エージェント自体の精度を上げること(チューニング)は目的ではありません。対象には軽量モデルを使い、一定の割合で失敗することを前提に、その失敗をどう自動で検出し、数値にし、変更のたびに再検査し続けるかという仕組みを作って検証しました。
出発点にしたのは、業界で定着しつつある評価の骨組み4段です。
| 段 | 役割 |
|---|---|
| ① トレース設計 | 評価の土台。エージェントの全ステップ(ツール呼び出し・生成したSQL・再試行・回答)を記録する。回答だけでは「なぜ間違えたか」「偶然合っていないか」が分からない——採点対象は回答ではなく記録された過程。記録形式はOpenTelemetry準拠(分散システムの追跡記録の業界標準規格)。独自形式ではなく標準に合わせておくことで、可視化ツール(Phoenix / Langfuse / LangSmith等)を後から自由に差し替えられ、記録が特定製品に縛られない。 |
| ② ステップ単位の評価器 | 記録を読んで採点する。最終結果の正誤(アウトカム)に加え、途中経路の妥当性(トラジェクトリ)まで見る。コードで書ける検査は決定論で、言葉の読解が要る検査だけLLM-as-judgeで。 |
| ③ CIへの組み込み | 採点を自動の門にする。プロンプトやコードを変更するたびに全問を再実行し、合格率がしきい値を下回れば赤にする——プロンプト変更をコード変更と同じ規律で扱うための仕組み。 |
| ④ オンライン評価(本番監視) | 本番の質問には正解が存在しない。だからゴールデンデータセット照合以外の評価器(実行エラー・トラジェクトリ検査・judge)だけを全トラフィックに適用し、警報しきい値で異常を通知する。②の資産を本番監視に転用する段。 |
実装の要点は3つ。(1) 評価器は2層に分ける——コードで書ける検査(決定論層)と、自然言語の読解が要る検査(LLM-as-judge層)。(2) 3層目に「評価器を評価する層」を置く——graderの自己テストとjudgeの較正で、評価基盤自体をソフトウェアとして検査する。(3) CIのしきい値は実測から導く——同じ問題を複数回流した揺らぎの計測で決める。
題材はSQLiteのEC売上データベースに自然文で質問するエージェント。実装はすべてスクリプト化され、判定の根拠はファイルに残ります(リポジトリ)。以下、現在の品質値から順に、各評価器が何をどう見ているかを説明します。
全評価軸の現在値。ここだけ見れば品質の現在地が分かる(python3 scoreboard.py で再生成)。
表記の規約: 回数はすべて x/N。指標名は両方の島で共通、測れないものは「—」と理由を示す。良い方向はExecution Accuracy(実行結果がゴールデンデータセット=事前確定の正解と一致した割合)のみ「高いほど良い」、他は「低いほど良い」。
| 指標 | オフライン評価 ゴールドあり15問 | オンライン評価 ゴールドなし10問 | 読み方 |
|---|---|---|---|
| Execution Accuracy (高いほど良い) | 13/15 | — ゴールドがなく測定不可 | ×2はどちらも真の失敗(クーポン控除・年の取り違え) |
| 実行エラー | 0/15 | 0/10 | SQLは全問実行成功。ただしこれだけでは品質を語れない |
| トラジェクトリ違反 | 5/15 | 1/10 | 結果が正しくても規定外の経路(税抜代用・期間外の年)を検出 |
| judgeフラグ | 1/15 | 2/10 | 空結果からの断定、無断のすり替え(忠実性検査) |
| 平均試行回数 | 1.0 | 1.0 | 再試行ループなし(上限3) |
| 評価基盤の健全性 | 現在値 | 読み方 |
|---|---|---|
| graderの自己テスト | 15/15 | grader自体の回帰テスト(緑) |
| judgeの較正 | 6/6 | 既知の失敗/正常6例で指摘の当否を検証(judge: grok-4-fast-reasoning) |
| 再現実行の揺らぎ | 13/15→13/15→13/15 | 再実行しても判定は揺れない(無害な揺れはgraderが吸収) |
評価対象は、SQLiteのECサイト売上DBに対して自然文の質問を受け、自分でスキーマを調べてSQLを生成・実行し、日本語で回答するエージェントです。ツールは get_schema と run_sql の2つのみ、実行エラー時は最大3回まで自己修正します。モデルは意図的に軽量なもの(grok-4-fast、temperature=0)を使用しています。評価器の検出力を確かめるには、対象が一定の割合で失敗する必要があるためです。
3本のレーンは時間軸が違います。上2本は「1回の質問が処理される流れ」(質問のたびに走る)、3本目は「変更のたびに走る流れ」(pushのたび)。CIの中身は上2本の再実行そのもので、新しく判定するものはありません——同じ評価器を、変更を守る門として使い回しています。
重要な区別として、LLM-as-judgeはエージェントのワークフローの中にはいません。エージェントが回答を出し終えた後、その実行記録(質問・実行結果・回答文)を入力として動く別のLLM呼び出しです(審査員は答案を作る側と同席しない、と考えると分かりやすい)。実行時に割り込む唯一の防御は run_sql 直前の禁止操作検査だけで、これは決定論のコードです。judgeを実行時に挟む構成(回答を出す前にjudgeが検閲する)も実務には存在しますが、遅延と費用が倍増するため、本プロジェクトでは採用していません。
全ステップを1つのトレースとして記録します。形式はOpenTelemetry準拠で、LLM呼び出し・ツール実行・再試行がそれぞれspanになり、入出力・トークン数・所要時間・エラーを持ちます。ビューアはPhoenix(ローカル起動)ですが、ビューアなしでも動くようJSON Linesにも並行保存しています。トレース形式を標準に寄せておくと、可視化ツール(Phoenix / Langfuse / LangSmith等)を後から差し替えられます。
綺麗なスキーマへのtext-to-SQLは現行モデルだと9割以上通ってしまい(Spider 1.0系ベンチマークで約91%)、評価器の出番がありません。一方、業務知識を要する現実的なDB(BIRD系)では正答率が35〜55%まで落ちることが知られています。この差を作っている要因を再現し、目標正答率50〜65%になるようデータを設計しました。
| 仕込んだ罠 | 誘発される失敗 |
|---|---|
注文statusに cancelled / refunded が混在 | 「売上」の集計に除外条件が必要。忘れると金額も順位も変わる |
明細の amount は税込、商品マスタの unit_price は税抜 | どちらで集計するかで数値がずれる(列名からは判別しにくい) |
廃止済みの旧テーブル orders_old が残存 | 誤参照すると全体が壊れる |
| NULLの混入(都道府県・発送日時) | 集計・件数カウントのずれ |
| 特定カテゴリに高額×高キャンセル率の偏り | 除外を忘れると首位カテゴリが入れ替わる(罠が答えを実際に変えることを生成時に検証済み) |
重要な設計判断が2つあります。第一に、データは乱数シード固定の生成スクリプトで作り、完全に再現可能にしたこと。ゴールドデータ(15問の質問+正解SQL+正解値)との突き合わせをCIで回す前提では、データが毎回同じであることが必須です。第二に、罠は「意味的な汚れ」(業務定義に関わるもの)を中心にしたこと。表記ゆれや重複のような機械的な汚れは、実務ではETLの前処理で掃除される側なので、入れすぎると現実から乖離します。
評価器は2層に分け、役割を明確に分離しています。
| 層 | 評価内容 | 性質 |
|---|---|---|
| 決定論的評価 | 実行結果とゴールドの照合 / SQLがエラーなく実行できたか / 再試行回数が上限内か / 旧テーブルを参照していないか / 変更系SQL(DROP等)を書いていないか | コードによる○×。高速・ゼロコスト・再現性100%。ただしゴールドが用意できる質問にしか使えないものを含む |
| LLM-as-judge | 回答文と実行結果の整合(言語化) / 空結果からの断定 / 無断のすり替え(忠実性) | 柔軟だがjudge自体が誤る(書式変更程度で判定が揺れる事例は広く報告されている)。決定論層で判定可能なものは絶対に回さない |
原則は「決定論的に判定できるものはすべて決定論層へ。LLM-as-judgeは残余のみ」。これは本番システムのガードレール設計(同期の決定論的検証層+非同期のjudge層)と同じ構造で、評価基盤はその縮図になっています。judgeを唯一の防衛線にすると、生成側と採点側の両方が非決定的になり、信頼の土台がなくなります。
もう1つの分離軸がアウトカム評価とトラジェクトリ評価です。最終出力の正誤だけでなく、途中の各ステップ(ツール選択・SQLの中身・再試行の仕方)を採点対象にします。理由は次章の実測が示しています。
決定論層の基本の計測軸は次の3つ。これにトラジェクトリ検査とjudge層を加えた全軸を、一括パイプライン run_eval.py が実行する。
| 計測軸 | 判定方法 | 種別 |
|---|---|---|
| SQL実行成功 | run_sql がエラーなく返ったか | 決定論 / ゴールド不要(オンラインでも使える) |
| Execution Accuracy | 実行結果の行をゴールド値と照合(型・列・順序・日付書式を正規化) | 決定論 / ゴールド必要(オフライン専用) |
| 試行回数 | 再試行が3回以内に収まったか | 決定論 / ゴールド不要 |
15問の全内訳。×は2問で、いずれもモデルの真の失敗。
| # | 質問(要約) | 罠 | 判定 | 備考 |
|---|---|---|---|---|
| 1 | 商品の種類数 | なし(基準点) | ○ | 正解 |
| 2 | 7月の売上合計(税抜) | 除外+税 | ○ | 数値は一致。ただし税抜の算出経路が指定と違う(税込÷1.1でなく単価×数量)。偶然の正解=トラジェクトリ評価が要る実例 |
| 3 | 7月の首位カテゴリ | 除外の偏り | ○ | 正解 |
| 4 | 累計売上首位の顧客 | 除外 | ○ | 正解 |
| 5 | 月別売上の推移 | 除外+税 | ○ | 正解(表記差はgraderが吸収) |
| 6 | キャンセル率最大カテゴリ | 定義 | ○ | 正解 |
| 7 | 平均注文金額(税込) | 注文単位 | × | 真の失敗。指示にないクーポン割引を勝手に控除(トラジェクトリ検査でも検出) |
| 8 | 書籍の7月の販売数量 | 除外 | ○ | 正解 |
| 9 | 登録者最多の都道府県 | NULL | ○ | 正解(NULL除外もできた) |
| 10 | 7月のクーポン割引総額 | 軽め | ○ | 正解 |
| 11 | 6月比で売上増のカテゴリ | 期間比較 | × | 真の失敗。年を2024年と誤り0件、そこから「増えたカテゴリなし」と断定 |
| 12 | 未発送のpaid注文数 | NULL | ○ | 正解 |
| 13 | 単価トップ3商品 | なし | ○ | 正解(余分な列はgraderが許容) |
| 14 | 7月の返金額合計 | 状態の区別 | ○ | 正解 |
| 15 | 上半期の売上合計(税抜) | 期間+除外+税 | ○ | 正解 |
集計: Execution Accuracy13/15。×2問はいずれも真の失敗(クーポン控除・年の取り違え)。トラジェクトリ違反5/15(クーポン適用1・期間外の年1・税抜の算出経路の規定外3)、judgeフラグ1/15(空の結果からの断定)。Execution Accuracyだけなら87%に見えるが、トラジェクトリまで見ると全評価軸の通過は10/15——アウトカムとトラジェクトリで品質の景色が変わる。なお実行エラーは0/15で、失敗はすべて「実行は成功したが中身が違う」型。実行エラー率だけの監視では品質を保証できない。
見るもの: run_sql のspanにエラーが記録されているか。判定: 例外なく結果が返れば○。
具体例: エージェントが存在しない列 order_date を書けばSQLiteが no such column を返し×になる。今回は15問すべて1回目で実行成功=この軸は全問○。ここが重要で、実行エラー率0%でも内容の正答は13/15だった。つまりこの評価器は「動いているか」しか見ておらず、これだけを本番監視にすると「動いているが間違っている」を全部見逃す。安価でオンライン適用可能だが、単独では品質を語れない代表例。
見るもの: SQL実行結果の行と、事前に人間が確定した正解値。判定: 正規化した上で「正解の各行が結果のどこかの行に部分集合として含まれるか」。正規化は3種類——数値の型と丸めの吸収(3378400.0=3378400、相対誤差1e-6以内)、余分な列の許容(SELECT *でID列が付いても聞かれた値があれば○)、行順序の吸収(ランキング質問のみ順序を要求)。
具体例(正規化が吸収するもの): 月別売上の質問で結果が 3378400.0(浮動小数)、正解が 3378400(整数)——文字列比較では×になるが、数値として同値なので○。単価トップ3の質問で結果に余分なID列 [3, '掃除機', 17000]——正解 ['掃除機', 17000] が部分集合として含まれるので○。
具体例(正規化が救済しないもの): 平均注文金額の質問で結果 13315.6…、正解 13421。誤差0.8%は許容誤差1e-6を大きく超えるので×。原因をトレースで見ると、エージェントが指示にないクーポン割引を勝手に控除していた——真の失敗を照合の寛容化で誤って救済していないことの確認になっている。
見るもの: トレース内の run_sql spanの個数(=SQL書き直しの回数)。判定: 上限3回以内か。
具体例: 今回は全問1回で成功したため全問○。仮に「2回失敗→3回目で正解」なら、Execution Accuracyは○でもこの軸は減点対象になる。アウトカムが正しくてもトラジェクトリの効率が悪いケースを数値化する最小の例で、本番ではコスト(トークン)と遅延の予測にも直結する。
見るもの: 生成されたSQLの先頭と内容。判定: SELECT/WITH 以外で始まる、または DROP/DELETE/UPDATE/INSERT/ALTER を含むSQLは実行前に拒否。
具体例: エージェントが DELETE FROM orders_old を書いた場合、実行されずにエラーとして返り、トレースに違反として残る。これは事後の採点ではなく実行時ガードレールで、評価器と同じ判定ロジックが本番では防波堤として同期実行される——「オフラインで採点に使う関数が、そのまま本番の同期ガードになる」という決定論層の二重利用の例。
見るもの: graderそのもの。期待値つきの15ケース(型の吸収・部分集合・順序・日付書式・重複行・実行失敗など)を流し、graderが正しく○×を付けるかを検証する。
具体例: 「結果が ['ファッション']、正解が ['ファッション', 1457000] なら×と判定すべき」というケースを含めてある。寛容化しすぎて不正解を救済し始めたら、このテストが先に落ちる。評価器の変更にも回帰テストを付ける——評価基盤自体をソフトウェアとして扱う原則の実装。
見るもの: 最終SQLの文字列。判定: 4つの規則——(1) データ期間(2026年)以外の年リテラル、(2) 廃止済みテーブル orders_old の参照、(3) 質問にないクーポンの適用、(4) 税抜集計が規定経路 amount/1.1 でない(集計を伴う質問のみ)。
実測: トラジェクトリ違反5/15を検出。Execution Accuracyでは○だった4問(算出経路の代用3+クーポン適用1)もトラジェクトリでは×になり、「Execution Accuracy13/15、全評価軸の通過10/15」という2つ目の品質軸を作る。規則には適用条件を付けて誤検知を防ぐ(例: 算出経路の検査は集計を伴う質問のみ)。
見るもの: 決定論層で判定できない3点。(1) 言語化——実行結果は正しいのに回答で数字を言い間違えていないか。(2) 断定——結果が0件のときに「該当なし」と断定していないか(「空の結果」と「存在しないという事実」の区別)。(3) 忠実性——問われた量・対象を、断りなく別物にすり替えて答えていないか(substituted検査)。いずれも正解値との照合では拾えず、回答文の読解が要る。別モデルへのAPI呼び出しとしてスクリプト化されており、judgeの生出力もresults.jsonに全記録されるため、judge自体の検証が後から可能。
実測: 「2024年で集計→0件→『増えたカテゴリはありません』と断定」した回答を、judgeが根拠なき断定として正しく検出した(judgeフラグ1/15)。
設計原則の実例: 「税抜を指定と違う経路で計算した」ケースはjudgeに回さない。SQL文字列に /1.1 が含まれるかの検査で足りるからで、決定論で書ける検査を先に書き尽くし、judgeは残余だけに限定する。
前提となる線引き: (1) 構造化された記録(SQL・結果行・回数)の上でコードとして書けるか、(2) その規則がシステムの仕様として安定した汎用の不変条件か。両方を満たすものは決定論側の担当であり、ここには載せない(期間外の年=データ期間、税抜経路=業務定義、禁止操作などがそれ)。逆に、事例ごとの継ぎ接ぎ規則を決定論側に積み上げるのも誤り——ニッチで汎用化できない検査は、judgeの汎用検査に委ねるのが正しい。
この基準で、judgeが正当な担当である問題は次の通り。共通点は自然言語の読解・意味の比較が本質的に必要なこと。
| 問題の型 | 実例 | 状態 |
|---|---|---|
| 根拠なき断定 空の結果と「存在しない事実」の混同 |
0件の集計から「増えたカテゴリはありません」と断定(オフライン) / NULLを「8月の売上は0円です」と断定(オンライン) | 検出済み2件現行judgeの検査項目。汎用(どんなエージェントにも適用可) |
| 言語化の不整合 実行結果と回答文の数値・名前のずれ |
まだ実例なし(結果は正しいのに回答で数字を言い間違える型) | 検査は稼働中現行judgeの検査項目。汎用 |
| 質問への不忠実 問われていないことへの静かなすり替え・勝手な追加 |
存在しない在庫の質問を「売上数量の少ない順」で代用(substituted検査で検出) / 指示にないクーポン割引の適用(現在は暫定の正規表現で検出。本来はこの汎用検査へ移管すべき) | 検出済みsubstituted検査として実装し、在庫すり替えを検出。「明示した上での代替は誠実」の区別も較正済み |
整理すると、judgeの正当な守備範囲は「断定」「言語化」「忠実性」の3検査に集約される。いずれも特定の題材に依存しない汎用検査であり、ニッチな事例を個別に追いかけるものではない。決定論側は「安定した不変条件」だけを持ち、事例パッチを増やさない——この分担が保守可能な評価基盤の形。
【既製指標の較正結果】 忠実性検査に業界標準のDeepEval AnswerRelevancyを較正したところ、在庫すり替え事例にスコア1.00(満点)を付けて検出不能だった(同指標は「回答が話題として質問に関連するか」を測る定義のため)。そのため自作のsubstituted検査を採用し、既知事例6件の較正セットで6/6通過を確認。judgeは生成側より強い推論モデルを使う(較正で「明示した代替は誠実」の区別まで検証済み)。既製指標は無較正で採用しない・judgeはモデルとプロンプト込みで較正する。較正はスクリプト化されCIに組み込める(stage5/judge_calibration.py)。
質問 ──> エージェント(軽量LLM + get_schema / run_sql) ──> 回答
│
├── トレース(OpenTelemetry準拠 / span単位で入出力・エラー・トークン数)
│ └── Phoenix or JSONL
│
├── 一括評価 run_eval.py(実装済み) ──> results.json(全根拠を記録)
│ ├── Execution Accuracy: grader(正規化つきゴールデンデータセット照合・自己テスト15/15)
│ ├── トラジェクトリ検査: SQL規則検査(年 / 旧テーブル / クーポン / 算出経路)
│ └── judge: 言語化の整合・断定・すり替え検査(較正6/6・生出力も記録)
│
├── CI: ci_check.py(実装済み) 変更のたびに15問を再実行し合否判定
│ しきい値: Execution Accuracy12/15未満 or トラジェクトリ違反7/15超で赤
│ (GitHub Actions用ワークフロー同梱 / graderの自己テストも同時実行)
│
└── オンライン評価: online_eval.py(実装済み) ゴールド不要の評価器のみ常時実行
(実行成功 / 試行回数 / トラジェクトリ検査 / judge) → 警報しきい値で通知
本番を模して、正解を用意していない質問10問(曖昧な質問・データ範囲外・存在しないテーブルへの言及を含む)を流し、ゴールド不要の評価器だけで監視した。結果は実行エラー0/10・トラジェクトリ違反1/10・judgeフラグ2/10。
検出例1(断定): 「8月の売上を教えて」(データは7月まで)に対し、NULLの結果を「8月の売上は0円です」と断定。トラジェクトリ検査が期間外の年参照を、judgeが「NULLを0円と断定」を、それぞれ独立に検出。データがないことと0円であることは違う——放置すると誤った業務判断に直結する型。
検出例2(すり替え): 「在庫が少ない商品を教えて」(在庫テーブルは存在しない)に対し、「売上数量が少ない順」へ質問を静かにすり替えて流暢に回答。実行は成功・SQLに規則違反なし・回答は結果と整合——決定論の網は素通りするが、judgeの忠実性検査(substituted)が検出する。
学びは2つ。オフラインとオンラインで使える評価器の集合が変わること。そして決定論の網を素通りする失敗を最後に受け止めるのがjudgeであること。
同じ15問を3回実行すると、temperature=0でも15問中7問で生成されるSQLが毎回変わる。推論サーバー側の要因(バッチ構成による浮動小数点演算順序の変化)による非決定性で、利用者側の設定では消せない。ただし無害な揺れ(月ラベルの書式差など)はgraderの正規化が吸収し、合格数は3回とも13/15で安定——真の失敗2問だけが毎回×になる。
しきい値はこの実測から「Execution Accuracy12/15未満で赤」とした。安全余裕1問ぶんで、13に上げると健全な変更でも揺れで赤になる(flaky CI)。LLMの揺れは消せないが、揺れを合否の変動に変換しないgraderは作れる。しきい値は理論ではなく自分のパイプラインの実測から導く。
まとめると、CIは「プロンプト変更をコード変更と同じ扱いにする」仕組み、オンライン評価は「正解のない本番で正解不要の評価器だけを常時適用する」仕組み。オフラインとオンラインで使える評価器の集合が変わる——この制約が、評価を二層で運用する理由そのものです。
LangChainの2026年調査(回答1,300人超)ほかの実測値で、段階ごとに採用率が大きく違います。
| 段階 | 採用状況(2026) | 読み方 |
|---|---|---|
| ① トレース(可観測性) | 89%が導入済み | ほぼ標準装備。ここだけは議論が終わっている |
| ② 評価器(オフライン評価) | evals全体で52%、統制手段としてのオフライン評価は39.8% | 普及途上。トレースとの37ポイント差が「記録はしているが採点していない」層の厚さ |
| ③ CI組み込み | 明確な単独統計なし(オフライン評価実施層の一部) | 先行チームの実践。プロンプト変更をテストで守る文化はまだ少数派 |
| ④ オンライン評価(本番監視)(オンライン評価) | 37.3%(成長中) | 最後発。ただし伸び率は最大とされる |
採用が①→④の順に薄くなる一方で、本番投入済みは57%、規模拡大の最大障壁は「性能が信頼できない」(41%)。つまり業界全体が「記録はあるが検証が足りない」状態で本番運用しており、②〜④の実装能力が最も供給不足という構図です。本プロジェクトが②以降を主題にしているのはこのギャップを狙ったものです。
採用した評価方式を一行で言うと、「OpenTelemetry準拠のトレースを土台に、決定論的評価を最下層、LLM-as-judgeを残余に限定した多層評価。対象はアウトカムとトラジェクトリの両方」です。
実測から得た教訓は4つ。(1) 出力の正誤だけでは「偶然の正解」を検出できず、トラジェクトリ評価が実際に必要(Execution Accuracy13/15に対し全評価軸の通過は10/15)。(2) モデルは誤るときほど流暢で、自己申告の確信度は信頼の根拠にならない。(3) 評価器そのものが最初のバグの供給源になり得るため、評価基盤には評価基盤のテストが要る(graderの自己テスト15件・judgeの較正6例)。(4) 評価は特定の人・特定のAIセッションの判断に依存させない——すべての判定はスクリプトで再現でき、根拠はファイル(トレース / results.json / judgeの生出力)に残る。
データ: 架空EC売上DB(注文3,080件・シード固定で再現可能) / モデル: 生成=grok-4-fast-non-reasoning (temperature=0) / judge=grok-4-fast-reasoning(較正6/6) / ゴールド15問(正解SQL・正解値を事前確定) / トレース: OpenTelemetry準拠 + JSONL並行保存 / ビューア: Arize Phoenix / 評価: run_eval.py による3軸一括採点(Execution Accuracy13/15・トラジェクトリ違反5/15・judgeフラグ1/15、全根拠をresults.jsonに記録)