🛰️

Langfuse で LLM の出力を評価し、その信頼度を測る

に公開

はじめに

こんにちは。クラウドエース第三開発部の泉澤です。以前はデータソリューション系の開発部に所属し、現在は生成AI を活用したサービス開発に取り組んでいます。
LLM アプリの自動評価手法として、LLM に評価を実施させる LLM‐as‐a‐Judge が注目されています。しかし、その自動評価結果がどの程度信頼できるのか、またどのように改善していけばよいのかを検証するのは容易ではありません。
そこで、本記事では、Langfuse を活用して以下のプロセスを実践する方法を考え、実際に LLM に生成(翻訳)してもらったデータを使い、LLM-as-a-Judge の評価と人間による評価の一致度の算出までを試してみました。
検証方法の流れやイメージ図は以下の通りです。

  • LLM アプリからのトレース送信
  • LLM‐as‐a‐Judge による自動評価
  • アノテーション(人間による評価・スコアリング)の実施
  • 評価スコア(LLM‐as‐a‐Judge・アノテーション)の一致度の算出による信頼性の検証
  • 評価改善サイクルの運用方法の検討

overall_image.png

なお、私自身は Langfuse を使った LLM アプリの運用経験はありません。今後、こんなふうにやっていきたいというのをまとめたのが本記事になります。間違いや不足している部分があるかとは思いますが、その点に注意して読んでいただければと思います。

対象読者

  • LLM アプリの自動評価手法の構築・検証に関心のある方
  • Langfuse の導入および評価機能の活用を検討している方

評価手法の背景と指標

LLM-as-a-Judge とは

LLM-as-a-Judge は、事前に設定した評価用プロンプトに基づいて、LLM アプリの入出力や中間生成物に対し、自動でスコアや理由付けを付与する仕組みです。
これにより、大量のデータに対して迅速かつ一貫性のある評価が可能となります。

しかし、LLM には位置バイアス(先に示された回答を高く評価しやすい)や冗長性バイアス(長いテキストの方が高く評価しやすい)といった認知バイアスが存在しており、安易に評価を信じるのは禁物です。
また、LLM が複雑なタスクを実行している場合、そのタスク固有の要件や背景情報を考慮した上で評価しなければならず、評価用の LLM がそれらを十分に理解できないと、人間が行う評価と大幅なズレが生じることがあります。
こうしたズレを解消するため、評価結果の裏付けとして人間による評価スコアと比較検証するアプローチが有効と考えています。

評価指標:Cohen's kappa と重み付き kappa

評価結果の信頼度を数値化するために、異なる評価者間の一致度を測る一般的な指標である Cohen's kappa 係数(kappa 係数)が使えるかと思います。実際、Replacing Judges with Juries: Evaluating LLM Generations with a Panel of Diverse Models の中では LLM を用いた自動評価システムと、実際に人間が行った評価との間の一致度を調べるために kappa 係数を使用しています。

しかし、単純な kappa 係数は、評価者間の評価が完全に一致しているかどうかのみを判断するため、たとえば「4点と5点」と「1点と5点」を同じ程度のズレとみなして計算されてしまいます。したがって、N 段階評価や N 点満点のスコアリングを行う場合は、重み付き Cohen's kappa(線形および二乗)を併用するのが望ましいです。これにより、点数の差が大きい場合に一致度の低さが適切に反映され、LLM と人間の評価スコアの傾向一致を確認でき、評価の改善点を探るための重要な手がかりとなります。
なお、kappa 係数も重み付き kappa 係数も、-1 から 1 の値を取り、1 に近いほど一致度が高いと判断されます。
kappa 係数が示す値に対する明確な判断基準はないようですが、例えばLandis and Koch の目安によれば、以下のように評価されています。

  • 0.00-0.20:わずかな一致
  • 0.21-0.40:ある程度の一致
  • 0.41-0.60:適度な一致
  • 0.61-0.80:かなりの一致
  • 0.81-1.00:ほぼ完全または完全な一致

kappa 係数と重みつき kappa 係数は scikit-learn で実装されているのでこちらも併せてご覧ください。
https://scikit-learn.org/stable/modules/model_evaluation.html#cohen-s-kappa
https://scikit-learn.org/stable/modules/generated/sklearn.metrics.cohen_kappa_score.html#sklearn.metrics.cohen_kappa_score

Langfuse の概要と主な機能

Langfuse は LLM アプリの開発・運用を支援するためのオープンソースの観測・分析プラットフォームです。
以下のような機能が提供されています。

  • ログ収集とトレース管理
    LLM の呼び出しやパイプライン内の各処理の詳細なログを自動で収集し、可視化します。
  • デバッグとパフォーマンス分析
    複雑な LLM アプリの各コンポーネントの動作状況、応答速度、処理時間、リソース使用状況をモニタリングします。
  • 評価機能
    • LLM‐as‐a‐Judgeによる自動評価
    • Human Annotationによる運用者自身の評価

trace_manager.png
trace_complex.png
エンドユーザーからの1回のリクエストにつき、一つのトレースが作成される。リクエスト内の処理ステップをトレースにネストされる形で定義できる。トレースをセッション単位でまとめることもできる。

Langfuseを用いた評価システムの構築手順

以下に、LLM アプリの評価システムを構築する一連の流れと具体例を説明します。
詳細な説明は省くので、Langfuse の公式ドキュメントや各節で紹介しているリンクをご覧いただければと思います。
今回は LLM を使った翻訳アプリを想定し、構築手順を紹介します。評価をばらけさせるために、以下のロールからランダムに選択し、翻訳を指示しています。

  • プロの翻訳家
  • 日本語を話せるが文法が怪しい英語話者
  • 幼稚園児
  1. Langfuse を導入する
    Langfuse は自身でセットアップが必要な Self-hosting 版と、SaaS 版が用意されています。
    プランによって、機能制限が異なりますので、要件に沿ったものを選ぶと良いと思います。
  1. LLM アプリから Langfuse へのトレース送信
  • SDK の活用
    Langfuse は Python や JS/TS 向けの SDK を提供しており、評価対象の関数に @observe デコレータを付与するだけで関数の入出力のトレースが自動で送信されます。トレーシングの始め方はこちらをご覧ください。https://langfuse.com/docs/get-started
  • カスタムトレース
    より詳細な情報が必要な場合は、langfuse_context.update_current_observation() を活用して、トレースデータをカスタマイズ可能です(コード参照)。詳細はこちらをご覧ください。https://langfuse.com/docs/sdk/python/decorators#additional-attributes
  • 確認
    設定後、実行してLangfuseの管理画面にトレースが正しく表示されるか確認します。
from langfuse.decorators import langfuse_context, observe
@observe()
def generate_response(user_input: str, role: str) -> str:
    # 入力内容と選択された役割を langfuse のコンテキストに記録
    langfuse_context.update_current_observation(
        input=json.dumps(
            {
                "user_input": user_input,
                "role": role,
            }
        ),
    )

    .
    .
    .

    return response_text

Langfuse_trace.png

  1. Eval Templates と Evaluators の構成
    Langfuse の UI から、自動評価機能を構築します。
    Eval Templates は評価用プロンプトのテンプレート、Evaluators は Eval Templates を元に、評価対象とする Trace や Eval Templates に挿入する項目を定義していくものになります。
  • Eval Templatesの設定
    • 設定項目:Name、Prompt、Score、Reasoning、Model など
      Langfuse では事前定義されたプロンプトも用意されておりますので、まずはそちらの利用を検討するのが良いと思います。今後改善していくことも見越して、最初からこだわりすぎないようにしましょう。
  • Evaluators の設定
    作成したテンプレートを元に、対象オブジェクトと評価項目を選択しバージョン管理を行います。

eval_template.png
evaluator.png
Evaluatorでは、対象のオブジェクトとして "New Traces"(設定後に保存されるトレース)と "Existing traces"(すでに保存されているトレース)のどちらか、または両方を選択できます。ここでは、新規に設定していますが、Eval Templates の修正時に再度 Evaluators を設定することで、一度評価したトレースに対して再評価を実行することができます。これにより、この後説明する一致度算出も、新しい評価用プロンプトで再実行できます。

  1. トレースの収集とアノテーション(人間による評価)の実施
  • トレースの収集と自動評価
    評価対象の関数を実行し、Trace が収集されると、自動的に評価まで実行されます。
    ※評価スコアのバリエーションが少ないと、後の一致度計算で有意な結果が得られにくいため、充分な件数が望ましいです。(私は 100 回実行しました。)
  • アノテーションの手順
    • 一致度を算出するために、以下の手順で Trace に対してアノテーションを実施します。
    1. Score Configs の作成
      Project Settings から作成できます。
    2. Annotation Queue の作成
      Langfuse ではアノテーションキューが用意されており、アノテーション対象とする Trace を分割して管理できます。チームメンバーでの分担や、顧客や有識者への展開も楽になるので、面倒なアノテーションも少しでも効率的に進めることができます。
    3. 収集したトレースをアノテーションキューに追加
    4. ラベリングの実施
      アノテーションのラベルも、自動評価と同様、Trace に紐づいた状態で管理されています。

score_config.png
create_annotaion_queue.png
add_to_annotation_queue.png
annotation.png

  1. 評価スコア(LLM‐as‐a‐Judge・アノテーション)の突き合わせ
  • 評価スコアの取得
    自動評価(LLM‐as‐a‐Judge)と人間による評価のスコアを比較し、一致度を算出します。SDK から Trace は取得できますが、そこに紐づく Score そのものは SDK から取得できないため、Trace から score_id を抜き出して、API でスコア値を取得します。
  • 一致度算出
    scikit-learnを使用して、Cohen's kappaと重み付きkappa(線形・二乗)を算出します。(詳細は scikit-learn の公式ドキュメントを参照)
import os
import requests
from datetime import datetime
from typing import Dict, Tuple, List
from langfuse import Langfuse
from sklearn.metrics import cohen_kappa_score


def fetch_score_value(score_id: str, auth: Tuple[str, str]) -> Dict:
    """
    指定されたスコアIDに対してLangfuseのAPIからスコア情報を取得する。
    HTTPエラーの場合は例外をキャッチし、空の辞書を返す。
    """
    url = f"https://us.cloud.langfuse.com/api/public/scores/{score_id}"
    try:
        response = requests.get(url, auth=auth)
        response.raise_for_status()  # HTTPエラーの場合は例外発生
        return response.json()
    except requests.RequestException as e:
        print(f"Failed to fetch score {score_id}: {e}")
        return {}


def extract_scores(traces: List, auth: Tuple[str, str]) -> Tuple[List, List]:
    """
    取得したトレースリストから、各スコアを 'EVAL' と 'ANNOTATION' に分類して抽出する。
    """
    eval_scores, annotation_scores = [], []

    for trace in traces:
        # 各トレースからスコアIDのリストを取得
        score_ids = trace.dict().get("scores", [])
        for score_id in score_ids:
            score_data = fetch_score_value(score_id, auth)
            if score_data:
                 value = score_data.get("value")
                if value is not None:
                    if score_data.get("source")  == "EVAL":
                        eval_scores.append(value)
                    elif score_data.get("source") == "ANNOTATION":
                        annotation_scores.append(value)
    return eval_scores, annotation_scores


def main():
    """
    指定期間内のトレースからスコアを抽出し、
    Cohen's kappa および重み付き kappa (linear) を算出するメイン処理。
    """
    # Langfuseクライアントの初期化と期間設定
    langfuse = Langfuse()
    start_date = datetime(2025, 2, 19, 18, 18, 0)
    end_date = datetime(2025, 2, 19, 18, 23, 0)

    # 期間内のトレースデータを取得
    traces_response = langfuse.fetch_traces(from_timestamp=start_date, to_timestamp=end_date)
    traces = traces_response.data if hasattr(traces_response, 'data') else []

    # 認証情報の取得
    auth = (os.environ.get("LANGFUSE_PUBLIC_KEY"), os.environ.get("LANGFUSE_SECRET_KEY"))

    # スコアの抽出
    eval_scores, annotation_scores = extract_scores(traces, auth)

    # スコアが十分にあるか確認
    print("Cohen's kappa:", cohen_kappa_score(eval_scores, annotation_scores))
    print("Weighted Cohen's kappa (linear):", cohen_kappa_score(eval_scores, annotation_scores, weights='linear'))


if __name__ == "__main__":
    main()

コード例に対して実際に算出した kappa 係数は以下の通りでした。

kappa 係数: 0.4693396226415094
重み付き kappa 係数 (線形) : 0.7281131049483415

先述の評価基準によると、kappa 係数が 0.41~0.60 の場合は「適度な一致」と判断されるため、今回の kappa 係数はまずまずの結果となりました。一方、重み付き kappa 係数はさらに高い値となっていることから、たとえ両者の数値が完全に一致していなくても、翻訳という比較的単純なタスクであるためか、全体的な傾向としては一致した判断がなされていると考えられます。なお、評価基準をさらに明確に設定すれば、重み付き kappa 係数のみならず、kappa 係数自体の値も向上するかもしれないと思いました。

自動評価機能の改善サイクルの運用

これまで、Langfuse を用いたトレースの収集や自動評価、さらには人間による評価と一致度算出といった一連の評価システムの構築手順について解説してきました。これにより、初期の評価結果がどの程度の精度で得られるかが明らかになりました。しかし、前述の通り、実運用の中では評価結果にばらつきや乖離が生じることも少なくありません。そこで、次のステップとして「自動評価機能の改善サイクルの運用」の検討が必要になると考えています。具体的には以下の手順が考えられます。

  1. 評価差分の確認
    評価スコア(LLM‐as‐a‐Judge・アノテーション)で大きな乖離があるトレースを抽出し、原因分析を実施。
  2. 評価軸・基準の見直し
    評価結果から問題点を抽出し、プロンプトや評価基準の再調整を行う。
  3. 再度のアノテーション実施
    改善後、再度アノテーションを実施し、一致度の変化を測定。
  4. プロンプトのチューニング
    改善結果を踏まえたプロンプトの最適化を行い、システム全体の評価精度向上を図る。
  5. 効果検証
    改善サイクルを経た後、一致度が向上しているかを検証し、信頼性の高い自動評価システムの構築を目指す。

まとめ

LLM アプリの評価は、システム改善やユーザー体験の向上に直結する重要なプロセスです。
Langfuse は、トレース収集、自動評価(LLM‐as‐a‐Judge)、アノテーション(Human Annotation)、データセット管理など、評価に必要な機能を一通り提供します。
今回は、この Langfuse を活用して、自動評価結果の信頼性を高めるために、評価指標として kappa 係数や重み付き kappa 係数を算出し、LLM による自動評価スコアと人間による評価スコアと突き合わせることで改善サイクルを回すプロセスを検討しました。

皆さんの LLM アプリの評価システム構築に、本記事が少しでもお役に立てれば幸いです。
今後も試行錯誤を重ね、自動評価機能と LLM アプリケーションの改善を続けていきたいと思います。

Discussion