「達成の定義」から逆算するLLMエージェントの評価設計

「達成の定義」から逆算するLLMエージェントの評価設計

目次

    はじめに

    LLMエージェントを実装したあと、それが「ちゃんと動いているか」をどう評価するかは悩ましい問題です。プロンプトやモデルを変えたときに、良くなったのか悪くなったのかを判断する手立てがないと、改善は手探りになってしまいます。

    LLMの評価を行うための基本的なアプローチとして、おおまかには以下の流れがよく紹介されています。これは特定のフレームワーク固有のものではなく、MastraLangfuseMLflowなど各評価プラットフォームに共通して見られる流れです。

    1. 評価対象となるエージェントを用意する
    2. 評価用のデータセットを準備する
    3. 評価指標を定義し、スコアを算出する処理(スコアラー)を実装する
    4. 評価を実行し、結果を見て改善する

    LLM評価の基本フロー。エージェントの用意・データセットの準備・評価指標とスコアラーの実装・実行と改善の4ステップからなり、ステップ2と3で詰まりやすい

    この流れ自体は分かりやすいのですが、いざ手を動かすと「2. データセットの準備」と「3. 評価指標の定義」のどちらでも詰まりがちです。ただ、2については合成データで評価ケースを自動生成するなど、解決のアプローチを示した記事をよく見かけます。一方で3の指標をどう決めるかに正面から踏み込んだ記事は、あまり見かけません。いざエージェントに対して評価を行おうとすると、参考になる評価指標(メトリクス)が世の中には大量にあり、自分のエージェントに対して結局何を測ればいいのかが分からなくなります。

    本記事では、この「何を測ればいいのか」という問いに対する考え方を中心に扱います。具体的には、測りたいものを頂点から分解して決めていくトップダウンのアプローチを整理します。そのうえで、そうやって測りたいものが決まったあとに待ち受ける「素直には測れない」という実装上の壁と、その乗り越え方についても少し触れます。

    なお、実装例にはLLMアプリケーションのフレームワークとしてMastraを使い、データセット管理や評価結果の比較にはLangfuseを利用する前提で話を進めますが、考え方自体はフレームワークに依存しません。

    題材: 問い合わせメールをチケットに起票するエージェント

    考え方を具体的に追えるよう、本記事を通して一つのエージェントを題材にします。

    題材として、顧客からの問い合わせメールを読んで、対応チケットを起票するエージェントを考えます。自然文のメールを受け取り、ツールを呼び出しながら、以下のような構造化データを組み立てていきます。

    問い合わせ(親)
    ├── 件名 / 優先度 / 製品名
    └── 対応タスク(子): [{ title, assignee?, dueDate? }, ...]
    

    メールには複数の要望が混ざっていたり、製品名が略称で書かれていたり、期限が「来週まで」のように曖昧だったりします。こうした自然文はルールだけでは拾いきれないからこそ、LLMに構造化させる意味があります。

    さて、このエージェントを評価したいとします。プロンプトを調整したときに、起票の質が良くなったのか悪くなったのかを測りたい。ではここで、何を測ればいいでしょうか

    LLM評価について調べると、すぐにたくさんのメトリクスが出てきます。たとえばConfident AIのLLM Evaluation Metrics: Everything You Needでは20種類ほどのメトリクスが紹介されていますが、そこでは用途ごとの大項目に分類されています。

    カテゴリ 代表的なメトリクス
    RAG Faithfulness(取得文脈への忠実さ)、Answer Relevancy(回答の関連性)、Contextual Recall(文脈の網羅率)、Contextual Precision(文脈の適合率)
    AIエージェント Task Completion(タスクの達成)、Tool Correctness(ツール選択の正しさ)、Argument Correctness(引数の正しさ)、Plan Quality(計画の質)
    基盤モデル Hallucination(捏造の有無)、Toxicity(有害性)、Bias(偏見)
    ユースケース固有 Summarization(要約の質)、Helpfulness(有用性)、Prompt Alignment(指示への準拠)

    このカタログを前にすると、まず「どのカテゴリが自分に関係するか」で迷います。たとえばRAGメトリクスは検索(retrieval)で取得した文脈に対する評価が前提なので、そうした構成でないこのエージェントには当てはまりません。ここは外せます。

    ではAIエージェントメトリクスならどうかというと、今度は粒度が合いません。「Task Completion(達成できたか)」はまさに測りたいことそのものですが、抽象的すぎて、これ一つをどう計算すればいいのか分かりません。「Tool Correctness(ツール選択の正しさ)」は近い気もしますが、このエージェントにとって何をもって「正しいツール呼び出し」とするかは結局自分で定義するしかない。カタログを眺めても、「このエージェントに、このうちどれをどう当てればいいのか」はまったく見えてこないのです。

    問題は、メトリクスのカタログ(手段)から入ろうとしていることにあります。手段を先に並べても、自分のエージェントにとって何が大事かは決まりません。順番が逆なのです。

    そこで、測り方を選ぶ前に「このエージェントにとって、達成とは何か」を先に決めるトップダウンのアプローチを体系化していきます。

    頂点から分解して決める

    考え方の骨格はシンプルで、次の順序で決めていきます。

    1. 頂点を一文で定義する。このエージェントにとって「タスクを達成できた」とはどういう状態か
    2. 達成側面に分解する。その達成を構成する、欠けたら未達になる側面を洗い出す
    3. 各側面を何で測るか決める。側面ごとに適切な測り方(スコアラー)を割り当てる

    ポイントは、メトリクスのカタログから始めないことです。まず「達成とは何か」という頂点を置き、そこから下ろしていきます。こうすると、測り方は「頂点を満たすために必要だから選ぶ」ものになり、カタログから当てずっぽうに選ぶことがなくなります。


    頂点を一文で定義する

    最初にやるのは、評価対象のエージェントの「タスク達成」を一文で言い切ることです。メトリクスの用語は使わず、自分の言葉で書きます。

    このエージェントなら、たとえばこうなります。

    問い合わせメールを、決められた形式で書かれていない情報を捏造せず正しい親子関係で取りこぼしなく、チケットに起票できたか

    この一文が評価全体の頂点になります。以降のすべての判断は、この一文を満たすために必要かどうかで決めていきます。


    達成側面に分解する

    頂点の一文には、達成を構成する複数の側面が含まれています。先ほどの一文を分解すると、自然に4つの側面が出てきます。

    側面 測りたいこと
    ① 形式 必須フィールド(件名・優先度など)が揃っているか
    ② 根拠 メールに書かれていない情報(期限・金額など)を、勝手に補っていないか
    ③ 関係 対応タスクが、正しい問い合わせ(親)に紐づいているか
    ④ 網羅 メールに含まれる要望を取りこぼしていないか

    ここで大事なのは、これらの側面はすべて同格だということです。たとえば①形式が完璧でも、②根拠で捏造があれば達成とは言えません。どれか一つが欠けても頂点は満たされない、という関係にあります。

    逆に言えば、頂点から見て「欠けても達成に影響しない」側面はここに挙げる必要はありません。網羅的にメトリクスを盛るのではなく、頂点を構成する側面だけを選びます。

    側面を出し切ったら、次は「それぞれを何で測るか」です。ここでも、測り方の選択肢を一度すべて並べてから当てはめていきます。

    測り方を体系化する

    各側面を測るには、エージェントの出力や実行の履歴を受け取ってスコアを返す処理を書きます。この処理をスコアラーと呼びます。

    スコアラーは一般に、コードで機械的に判定するもの(コードによる判定)と、LLMに判定させるもの(LLM-as-a-judge)に大別されます(MastraDatabricksでもこの切り分けが使われています)。本記事では、これにもう一つ「正解データが必要か」という軸を足して、次の3つに分けて考えます。


    タイプ1: コードによる決定的チェック(正解不要)

    機械的なロジックだけで判定でき、正解データを必要としないタイプです。出力の構造や、入力との突き合わせで判定できるものが該当します。

    返すスコアは、合否の二値でも、「必須フィールドの充足率」のような連続値(割合)でも構いません。どちらもコードで計算できます。

    • 例: 必須フィールドが揃っているかの形式チェック、対応タスクが親の問い合わせに紐づいているかのチェック

    正解データが不要なので軽量で、後述するように本番のトラフィックに対しても回しやすいのが特徴です。


    タイプ2: コードによる照合(正解必要)

    判定はコードで機械的に行いますが、判定のためにあらかじめ用意した正解データが必要なタイプです。

    • 例: 「このメールからは対応タスクが3件起票されるべき」という正解を用意し、実際の出力がどれだけ拾えたかを照合する網羅率(recall)

    正解データを人手で用意する必要があるため、用意できる件数には限りがあります。本番トラフィックには正解が存在しないため、基本的にオフラインの評価でしか使えません。


    タイプ3: LLM judgeによる判定

    コードでは機械的に判定できないものを、LLMに判定させるタイプです(LLM-as-a-judge)。多くの場合、入力と出力を渡して判定させるため正解データは不要ですが(G-Evalなどで知られるreference-freeな評価です)、判定者がLLMである分、判定結果が不安定になりやすい弱点があります。

    • 例: 起票した各項目(期限や金額など)がメール本文に根拠を持つか(捏造していないか)をLLMに判定させる

    LLMを呼ぶためコストと時間がかかり、判定が揺れることもあるため、扱いには工夫が要ります(これは後半で詳しく触れます)。


    各側面に測り方を当てはめる

    3タイプを並べたところで、先ほどの4側面に割り当てます。頂点から下ろしてきた側面に対して、「その側面を守るには、どのタイプが適しているか」を判断していきます。

    側面 測りたいこと 測り方 タイプ
    ① 形式 必須フィールドが揃っているか スキーマ適合チェック タイプ1(コード・正解不要)
    ② 根拠 書かれていない情報を捏造していないか LLMによる根拠判定 タイプ3(LLM judge)
    ③ 関係 対応タスクが正しい親に紐づいているか 親子関係チェック タイプ1(コード・正解不要)
    ④ 網羅 取りこぼしがないか 正解との照合 タイプ2(コード・正解必要)

    頂点から分解して測り方を決める全体像。1つの頂点を形式・根拠・関係・網羅の4つの達成側面に分解し、各側面をコード(正解不要)・コード(正解必要)・LLM judgeの3タイプの測り方に割り当てる3層構造

    この表が、評価設計のアウトプットです。「メトリクスのカタログから何を選ぶか」ではなく、「頂点 → 側面 → 測り方」と下ろしてきた結果として、測るべきものとその手段が確定しました。

    ここまでが、本記事の主題である「何を測るか」の考え方です。頂点を一文で定義し、達成側面に分解し、各側面を測り方の3タイプから選ぶ。これで評価対象は決まります。

    ところが、いざこの表のとおりにスコアラーを実装しようとすると、いくつかの側面は素直には測れないことに気づきます。最後に、その実装上の壁と乗り越え方を見ていきます。

    測りたいのに素直に測れない: ツール結果のモック

    測りたいものは表のとおり決まりました。しかし、それを実際のコードに落とそうとすると、最初の段階でつまずきます。④網羅のように「エージェントを動かした結果」を見る側面では、そもそもエージェントを安定して動かせないからです。

    エージェントが外部ツール(検索、DB照会、外部APIへの問い合わせなど)を呼ぶ場合、その結果は実行のたびに変わり得ます。評価では同じ入力に対して結果が安定していてほしいのに、ツールの結果がブレると、エージェントの出力が同じでも評価スコアが揺れてしまう。「プロンプトを変えて良くなったのか、たまたまツールの結果が変わっただけなのか」が区別できなくなります。

    そこで、ツールの結果をあらかじめ用意したものに差し替えて実行します。ツールを一度実行した結果を記録しておき、以降はその記録を再生する、record-and-replayの考え方です。これは特殊な発想ではなく、Mastraでも「副作用のあるツールを持つエージェントを決定的に評価するために、記録したツール結果を再生したい」という要望がIssueとして挙がっています


    呼び出し順を再現する

    素直に考えると、ツールが毎回同じ値を返すようにすればよさそうです。

    // 素直な発想: ツールが常に同じ結果を返すようにする
    toolMock.mockReturnValue(recordedResult)
    

    しかし、これでは足りないことがあります。エージェントは同じツールを複数回呼ぶことがあり、呼び出しごとに違う結果を期待しているからです。1回目の検索と2回目の検索で別の結果が返るからこそ、エージェントは次の行動を決めています。常に同じ値を返すと、本来の挙動を再現できません。

    そこで、呼び出された順番に応じて、用意しておいた結果を順に返すようにします。呼び出し回数をクロージャで保持し、その回数に対応する結果を返す形です(モックにはVitestを使います)。

    async function runWithMockedTool(input: string, results: ToolResult[]) {
      let callCount = 0
    
      const spy = vi.spyOn(searchTool, "execute").mockImplementation(async () => {
        // 呼び出し順に、用意しておいた結果を返す
        const result = results[Math.min(callCount, results.length - 1)]
        callCount += 1
        return result
      })
    
      try {
        return await agent.generate(input)
      } finally {
        spy.mockRestore() // モックを必ず元に戻す
      }
    }
    

    mockResolvedValueOnce を呼び出し回数ぶん並べても同じことができますが、いずれにせよ「何回目にどの結果を返すか」を意識する必要があります。


    モックの対象は「検索結果」だけではない

    このモックが効くのは、検索のように毎回結果が変わるツールだけではありません。外部システムの状態に依存する処理も、同じ手口で固定できます。

    たとえばこのエージェントが、チケットを起票する前に「すでに同じ問い合わせが起票されていないか」を、チケット管理システムへ問い合わせるとします。この照合は、

    • チケットのスキーマ(どんな項目を持つか)
    • 既存チケットの検索結果(マッチしたか、その親問い合わせのIDは何か)

    に依存します。実際のシステムに繋ぐと、スキーマ変更やデータの増減で結果が揺れ、評価が安定しません。そこで、スキーマを返すツールも、検索を行うツールも、評価時はあらかじめ用意した値を返すよう差し替えます。

    // スキーマ取得を対象ごとに固定
    mockGetSchema.mockImplementation(async (entity) => {
      if (entity === "inquiry") return inquirySchema
      if (entity === "task") return taskSchema
      throw new Error(`unexpected: ${entity}`)
    })
    
    // 検索を呼び出し順に固定(1回目=親の問い合わせ、2回目=子の対応タスク)
    mockSearch
      .mockResolvedValueOnce({ records: [{ id: "inq-1", subject: "ログインできない" }] })
      .mockResolvedValueOnce({ records: [{ id: "task-9", inquiryId: "inq-1" }] })
    

    こうすると、外部システムに一切繋がずに、「親を見つけたあと、その親に紐づく子だけを正しく検索しているか」といった挙動を、外部の状態に左右されずに評価できます。スキーマや既存データへの依存を、すべて手元の固定値に置き換えてしまうわけです。


    気をつけること

    ツール結果のモックは強力ですが、いくつか落とし穴があります。

    • 書き込み系の副作用に注意する。レコード作成や更新のツールをそのまま実行すると、評価のたびに本番へ書き込んでしまいます。書き込み系は呼ばれたことだけ検証し、実際の副作用は起こさないようにします。
    • モックは本物とズレ得る。用意した値は、記録した時点のスキーマやAPIレスポンスの形でしかありません。外部システムの仕様が変わると、評価は通るのに本番では動かない、という状態になります。モックの値をどう最新に保つかは、別途考えておく必要があります。

    このように、ツールの結果を固定するだけでも、呼び出し順の再現や副作用の扱いなど、考えることは意外と多くあります。ここを乗り越えてはじめて、各側面を安定して測る土台が整います。

    まとめ

    本記事では、LLMエージェントの評価において「何を測るか」をどう決めるかを中心に整理しました。

    メトリクスのカタログから測り方を選ぼうとすると、自分のエージェントに何が必要かが見えずに手が止まります。そうではなく、「達成とは何か」という頂点を一文で定義し、達成側面に分解し、各側面を測り方のタイプから選ぶというトップダウンで下ろしていけば、測るべきものとその手段は自然に決まります。

    そして、そうやって測りたいものが決まったあとにも、素直には測れないという実装上の壁が待っていました。本記事では、エージェントを安定して動かすためのツール結果のモックを取り上げました。呼び出し順を再現したり、スキーマや既存レコードへの依存を切り離したり、書き込み系の副作用に気をつけたり。測りたいことが明確でも、それをそのまま動かせるとは限らないのだと感じました。

    今回はオフラインでの評価(用意したデータセットに対してスコアラーを回す)を念頭に話を進めました。実運用では、ここで作ったスコアラーのうち正解データを必要としない軽量なもの(タイプ1)を、本番トラフィックの一部に対しても回してドリフトを監視する、といった発展も考えられます。同じスコアラーをオフラインの改善ループと本番監視の両方で使い回せると効率的ですが、このあたりはまた別の機会に整理できればと思います。

    まずは自分のエージェントについて、「達成とは何か」を一文で書き出してみるところから始めてみてください。そこから分解していくと、漠然としていた「何を測ればいいのか」が、具体的なスコアラーの一覧へと姿を変えていくはずです。

    参考資料

    アジアクエスト株式会社では一緒に働いていただける方を募集しています。
    興味のある方は以下のURLを御覧ください。