atama plus techblog

langchain/openevalsでLLM-as-a-judgeの基本を理解

2025/03/23に公開

はじめに

LLMアプリケーションを本番環境にデプロイする際、従来のソフトウェアテストと同様、評価は非常に重要な役割を果たします。適切な評価を行わなければ、モデルが期待通りに動作しているかどうかを確認することができません。

今回、LangChainが提供するopenevalsというライブラリを使用して、複数のLLMモデルの出力を比較評価する方法を試してみたので書き留めておきます。

LLM-as-a-judge とは

LLM-as-ajudgeとは、他のAIモデルのパフォーマンスを含め、様々なタイプのコンテンツ、反応、パフォーマンスを評価するためにLLMを使用することを指します。

“LLM as a judge” refers to the use of LLMs to evaluate or assess various types of content, responses, or performances, including the performance of other AI models.[1]

GPT4くらいから提唱されていた概念らしいのですが、BedrockがLLM-as-a-judgeのサービスを提供したり[2]、今回紹介するようなライブラリが作られたり、ここ数ヶ月でより一般的な技術になってきた感じがします。

openevalsとは

openevalsは、LLMアプリケーションの評価を支援するためのパッケージです。
従来のソフトウェアテストと同様に、LLMアプリケーションにおいても評価(evals)は非常に重要な要素であり、本番環境へのデプロイ前に行うべき重要なステップとなります。

openevalsは以下のような機能を提供しています:

  • LLM-as-judge:別のLLMを使用して出力を評価
  • 構造化データの出力評価
  • RAGワークフローの評価
  • コード出力の評価

今回はシンプルなLLM-as-judge機能を試しに触ってみたいと思います。

・・・ LLM-as-a-judge と、 LLM-as-judge でプロバイダーによって言葉が微妙に違いますね。

使い方

基本的な使い方

openevalsの基本的な使い方はREADMEを参照してください。

最小の実行コードは以下のようになります。

from openevals.llm import create_llm_as_judge
from openevals.prompts import CORRECTNESS_PROMPT

correctness_evaluator = create_llm_as_judge(
    prompt=CORRECTNESS_PROMPT,
    model="openai:o3-mini",
)

inputs = "How much has the price of doodads changed in the past year?"
# These are fake outputs, in reality you would run your LLM-based system to get real outputs
outputs = "Doodads have increased in price by 10% in the past year."
reference_outputs = "The price of doodads has decreased by 50% in the past year."
# When calling an LLM-as-judge evaluator, parameters are formatted directly into the prompt
eval_result = correctness_evaluator(
  inputs=inputs,
  outputs=outputs,
  reference_outputs=reference_outputs
)

print(eval_result)
  • 評価用のプロンプトはCORRECTNESS_PROMPTとして、ライブラリ内に事前定義されています。
    • もちろん、自分で評価用プロンプトをカスタマイズすることも可能です。[3]
  • inputs にはLLMに渡す入力プロンプト、 reference_outputs には期待される出力を事前に定義します。
  • このサンプルコードでは、LLMからの出力も output として仮定義していますが、実際にはここはLLMからの出力を渡す部分になります。
  • correctness_evaluator として初期化された評価器に入力、出力管理画面、期待される出力を渡すと、別のLLM(例ではo3-mini)に対して評価用プロンプトとともに渡され、評価が実行されます。

出力は以下のようになります。

{
    'key': 'score',
    'score': False,
    'comment': 'The provided answer stated that doodads increased in price by 10%, which conflicts with the reference output...'
}
  • score として出力される評価結果と、評価に至った思考のコメントが返却されます。

実際の使用例

サンプルコードでは、LLMからの出力がダミーになっていたり、複数モデルでの評価実行がされていなかったりと、実用にはまだステップが必要なので、以下のような実装で活用してみています。

複数モデル比較コード
import os
import traceback
import sys
from dotenv import load_dotenv
import pandas as pd
from langchain_openai import ChatOpenAI
from langchain_anthropic import ChatAnthropic
from langchain_google_genai import ChatGoogleGenerativeAI
from openevals.llm import create_llm_as_judge
from openevals.prompts import CORRECTNESS_PROMPT

from datasets import math_questions, english_questions, all_questions, get_dataset_info

EVAL_MODEL = "openai:o3-mini"

load_dotenv()

os.environ["OPENAI_API_KEY"]
os.environ["ANTHROPIC_API_KEY"]
os.environ["GOOGLE_API_KEY"]

def init_models():
    print("モデルの初期化を開始...")
    models = {}
    
    # 比較対象のモデル
    # 新規にモデルを定義する場合はこのdictに追記する
    model_configs = [
        {"name": "claude-3-5-sonnet-latest", "class": ChatAnthropic, "params": {"model": "claude-3-5-sonnet-latest", "temperature": 0}},
        {"name": "claude-3-7-sonnet-latest", "class": ChatAnthropic, "params": {"model": "claude-3-7-sonnet-latest", "temperature": 0, "thinking": {"type": "disabled"}}},
        {"name": "gpt-4o", "class": ChatOpenAI, "params": {"model": "gpt-4o", "temperature": 0}},
        {"name": "gemini-2.0-flash", "class": ChatGoogleGenerativeAI, "params": {"model": "gemini-2.0-flash", "temperature": 0}}
    ]
    
    for config in model_configs:
        model_name = config["name"]
        try:
            models[model_name] = config["class"](**config["params"])
            print(f"  {model_name}の初期化成功")
        except Exception as e:
            print(f"  {model_name}の初期化エラー: {e}")
            print(f"  エラーの詳細:\n{traceback.format_exc()}")
    
    if not models:
        print("警告: すべてのモデルの初期化に失敗しました。")
        sys.exit(1)
    
    print(f"初期化完了: {', '.join(models.keys())}")
    return models

# 評価用データセットの情報を取得
dataset_info = get_dataset_info()


print("評価者モデルを初期化中...")
try:
    correctness_evaluator = create_llm_as_judge(
        prompt=CORRECTNESS_PROMPT,
        model=EVAL_MODEL,
    )
    print(f"評価者モデルの初期化完了: {EVAL_MODEL}")
except Exception as e:
    print(f"評価者モデルの初期化エラー: {e}")
    print(f"エラーの詳細:\n{traceback.format_exc()}")
    print("評価者モデルの初期化に失敗したため、処理を中止します。")
    sys.exit(1)


def evaluate_models(models, questions):
    print("\n===== モデル評価プロセス開始 =====")
    results = []
    total_questions = len(questions)
    
    for i, question_data in enumerate(questions, 1):
        question = question_data["question"]
        reference = question_data["reference"]
        
        for model_name, model in models.items():
            print(f"  モデル '{model_name}' で回答を生成中...")
            try:
                # モデルからの回答を取得
                response = model.invoke(question)
                model_answer = response.content
                print(f"  回答生成完了 (文字数: {len(model_answer)})")
                
                # 回答を評価
                print(f"  評価者モデルで回答を評価中...")
                try:
                    eval_result = correctness_evaluator(
                        inputs=question,
                        outputs=model_answer,
                        reference_outputs=reference
                    )
                    
                    print(f"  評価結果: {eval_result}")
                    
                    results.append({
                        "model": model_name,
                        "question": question,
                        "answer": model_answer,
                        "evaluation": eval_result
                    })
                    
                except Exception as eval_error:
                    print(f"  評価処理中にエラーが発生しました: {eval_error}")
                    print(f"  評価エラーの詳細:\n{traceback.format_exc()}")
            
            except Exception as e:
                error_type = type(e).__name__
                print(f"  回答生成中にエラーが発生しました ({model_name}): {error_type}: {e}")
                print(f"  エラーの詳細:\n{traceback.format_exc()}")
    
    print("\n全ての質問の評価が完了しました。結果を集計中...")
    df = pd.DataFrame(results)
    return df


def main():
    print("\n===== メイン処理開始 =====")
    
    print("モデルを初期化中...")
    models = init_models()
    
    print("モデル評価を開始...")
    results_df = evaluate_models(models, all_questions)
    
    print("\n===== 評価結果の分析 =====")
    
    print(f"\n評価結果の概要:")
    print(f"総評価数: {len(results_df)}")
    print(f"評価対象モデル: {', '.join(results_df['model'].unique())}")
    
    print("\nCSVファイルに詳細結果を保存中...")
    export_df = results_df.copy()
    export_df['evaluation'] = export_df['evaluation'].apply(lambda x: str(x))
    export_df.to_csv("llm_evaluation_results.csv", index=False)
    print("詳細な結果は 'llm_evaluation_results.csv' に保存されました。")
    
    print("\n===== 評価プロセス完了 =====")
    return results_df


if __name__ == "__main__":
    print("\n===== スクリプト実行開始 =====")
    results = main()

評価様のデータセットは以下の様に、数学と英語の問題を解かせるものを置いてみています。

評価用データセット
"""
評価用のデータセット定義

このモジュールには、モデル評価に使用する質問と参照回答のデータセットが含まれています。
"""

# 数学問題のデータセット
math_questions = [
    {
        "question": "三角形ABCにおいて、辺ABの長さは10cm、辺BCの長さは12cm、辺ACの長さは14cmです。この三角形の面積を求めてください。",
        "reference": "三角形の面積はヘロンの公式を使って計算できます。半周をs = (10 + 12 + 14) / 2 = 18とすると、面積は√(s(s-a)(s-b)(s-c)) = √(18(18-10)(18-12)(18-14)) = √(18×8×6×4) = √3456 = 58.8平方センチメートルです。"
    },
    {
        "question": "2次方程式 3x² - 5x - 2 = 0 の解を求めてください。",
        "reference": "2次方程式 3x² - 5x - 2 = 0 の解は、2次方程式の解の公式 x = (-b ± √(b² - 4ac)) / 2a を使って求めることができます。ここで、a = 3, b = -5, c = -2 なので、x = (5 ± √(25 + 24)) / 6 = (5 ± √49) / 6 = (5 ± 7) / 6 となります。したがって、x = 2 または x = -1/3 が解です。"
    }
]

# 英語問題のデータセット
english_questions = [
    {
        "question": "英語の現在完了形と過去形の使い分けについて説明し、例文を3つずつ挙げてください。",
        "reference": "現在完了形は過去の出来事と現在の状況を結びつける時に使われ、「have/has + 過去分詞」の形を取ります。例:I have lived in Tokyo for 5 years.(今も東京に住んでいる)/ She has already finished her homework.(すでに宿題を終えた状態)/ We have never been to Australia.(オーストラリアに行ったことがない)\n一方、過去形は単に過去の出来事や状態を表し、いつ起きたかが明確か重要な場合に使います。例:I lived in Tokyo for 5 years when I was a child.(子供の頃5年間東京に住んでいた)/ She finished her homework yesterday.(昨日宿題を終えた)/ We went to Australia last summer.(去年の夏オーストラリアに行った)"
    },
    {
        "question": "以下の日本語を自然な英語に翻訳してください:「私は昨日友人と映画を見に行きましたが、予想よりも面白くなかったです。次回は別の映画館に行ってみようと思います。」",
        "reference": "I went to see a movie with my friend yesterday, but it wasn't as interesting as I had expected. I think I'll try a different movie theater next time."
    }
]

# すべての質問を結合したデータセット
all_questions = math_questions + english_questions

# データセットの情報を取得する関数
def get_dataset_info():
    """データセットの情報を返す"""
    return {
        "math_questions_count": len(math_questions),
        "english_questions_count": len(english_questions),
        "total_questions_count": len(all_questions)
    } 

実装のポイントは以下です。

  • init_models で複数のmodelを初期化し、評価スクリプトの中でmodelごとの評価実行をループさせる
  • model_configsの中で langchain_openai.ChatOpenAI などのLangChainによってwrapされているLLM呼び出しメソッドを定義することで、評価実行時には共通の .invoke() メソッドを実行できる様にする
    model_configs = [
        {"name": "claude-3-5-sonnet-latest", "class": ChatAnthropic, "params": {"model": "claude-3-5-sonnet-latest", "temperature": 0}},
        {"name": "claude-3-7-sonnet-latest", "class": ChatAnthropic, "params": {"model": "claude-3-7-sonnet-latest", "temperature": 0, "thinking": {"type": "disabled"}}},
        {"name": "gpt-4o", "class": ChatOpenAI, "params": {"model": "gpt-4o", "temperature": 0}},
        {"name": "gemini-2.0-flash", "class": ChatGoogleGenerativeAI, "params": {"model": "gemini-2.0-flash", "temperature": 0}}
    ]
for model_name, model in models.items():
    response = model.invoke(question)

:::

  • あとは小さい工夫ですが、最終的にみやすい様に出力をcsvで出しています。

scoreの調整

標準では、LLM-as-judgeの結果返却されるスコアがTrue/Falseの2値なので、もう少し細かい精度で見たい時に実用性が足りません。
そこで、openevalsに提供されているCustomizing output scoresを実装します。

変更は2箇所のみで、

  1. 評価用プロンプトを作成する
評価用プロンプト例
EVALUATION_PROMPT = """
あなたは専門の評価者として、モデルの回答の正確性を評価します。以下の5段階で評価してください:

<評価基準>
  評価は次の5段階でスコアを付けてください:
  - 0.0: 完全に不正確または関連性がない回答
  - 0.25: 多くの誤りを含むが、わずかに関連性のある回答
  - 0.5: 部分的に正確だが、重要な誤りや欠落がある回答
  - 0.75: ほぼ正確だが、小さな誤りや不完全な部分がある回答
  - 1.0: 完全に正確で包括的な回答
</評価基準>

<質問>
{inputs}
</質問>

<回答>
{outputs}
</回答>

<参考回答>
{reference_outputs}
</参考回答>

回答を評価し、上記の5段階評価でスコアを付けてください。その評価理由も説明してください。
"""
  1. 評価器を変更する
correctness_evaluator = create_llm_as_judge(
-     prompt=CORRECTNESS_PROMPT,
+     prompt=EVALUATION_PROMPT,
+     choices=[0.0, 0.25, 0.5, 0.75, 1.0],
+     feedback_key="accuracy_score",
    model=EVAL_MODEL,
)

これで、5段階評価でLLM-as-judgeできるようになります。

LangSmithと統合する

上述したように、openevalsに渡す output をLLMから生成する部分でLangChainにwrapされたcallerを実装しているので、LangSmithと統合してLangSmithで結果確認することも可能です。

os.environ["LANGSMITH_API_KEY"]
os.environ["LANGSMITH_TRACING"] = "true"
os.environ["LANGSMITH_PROJECT"] = "openevals-sample"

加えて、openevals自体にもLangSmithとの統合が実装されています。
pytest経由でLangSmithと統合したテストコードを記述して実行することで、ターミナルからLangSmithの結果やリンクを取得することができます。

+ @pytest.mark.langsmith
+ def test_evaluate_models():
+     models = init_models()
+     results_df = evaluate_models(models, all_questions)

まとめ

openevalsを使用することで、LLM-as-a-judgeを簡単に実装することができます。

単一のモデルの評価だけでなく、複数のモデルを比較して最適なモデルを選択するためのワークフローを構築することも可能です。

特に以下の点が便利だと感じました:

  • シンプルなAPI: 数行のコードで評価環境を構築できる
  • カスタマイズ性: 独自の評価基準を定義することができる
  • モデル比較: 複数のモデルの出力を定量的に比較できる
  • LangSmithとの統合: 評価結果を視覚化し、時間経過とともに追跡できる

まだ ver 0.0.13 と若いので、これからもっと進化していくツールとして期待できると思います、ぜひ触ってみて知見をシェアしていきましょう!

参考

脚注
  1. What is LLM as a Judge? ↩︎

  2. Amazon Bedrock のモデル評価で LLM-as-a-judge を提供 (プレビュー) ↩︎

  3. Customizing prompts ↩︎

atama plus techblog
atama plus techblog

Discussion