DeepEvalを使ってLLMアプリのUnit Testingを自動化する方法

Artificial Intelligence tutorial - IT technology blog
Artificial Intelligence tutorial - IT technology blog

深夜2時、stagingサーバーがエラーを報告した。チャットボットの回答がおかしい――クラッシュでも例外でもなく、ただ単にでたらめを言っている。レスポンスは技術的に問題なさそうに見えるが、内容は完全にハルシネーションだった。ログには何も残っていない。失敗したテストもない。

そのとき初めて、LLMを通常のコードと同じようにテストする方法を真剣に探し始めた。そして見つけたのがDeepEvalだ。

5分で動かす

長いドキュメントを読む必要はない。インストールしたらすぐ試せる:

pip install deepeval
deepeval login  # オプション — Confident AIのダッシュボードで確認する場合

test_chatbot.pyを作成する:

from deepeval import assert_test
from deepeval.metrics import AnswerRelevancyMetric
from deepeval.test_case import LLMTestCase

def test_answer_relevancy():
    metric = AnswerRelevancyMetric(threshold=0.7)
    test_case = LLMTestCase(
        input="Redisは何に使いますか?",
        actual_output="Redisはキャッシュ、セッション管理、pub/subに使うインメモリデータストアです。"
    )
    assert_test(test_case, [metric])

実行:

deepeval test run test_chatbot.py

以上。テストがパスすれば、回答が質問に関連していることを意味する。テストが失敗すれば、AIが的外れな回答をしているということだ――これがまさに従来のユニットテストでは検出できなかった種類のエラーだ。

DeepEvalの仕組み

文字列マッチングによるアサート(LLMには脆弱な方法)の代わりに、DeepEvalは別のLLM――judge model――を使って、特定の基準ごとに品質を評価する。各テストはjudge modelを1〜2回呼び出し、約2〜5秒かかる。

各テストは3つの要素で構成される:

  • LLMTestCase — 1つのテストケースの入出力データセット
  • Metric — 評価基準(relevancy、faithfulness、hallucinationなど)
  • Threshold — pass/failの閾値(0.0〜1.0)

主要なメトリクス

AnswerRelevancyMetric — 回答が質問に関連しているかどうかを評価する。汎用チャットボットのテストに使う。

from deepeval.metrics import AnswerRelevancyMetric
metric = AnswerRelevancyMetric(threshold=0.7)

FaithfulnessMetric — RAGパイプラインで最も重要なメトリクスだ。提供されたコンテキストに対して回答が忠実かどうかを検証し、AnswerRelevancyでは見逃すタイプのハルシネーションを検出できる。

from deepeval.metrics import FaithfulnessMetric
from deepeval.test_case import LLMTestCase

test_case = LLMTestCase(
    input="Redisは永続ストレージをサポートしていますか?",
    actual_output="Redisはデータをディスクに一切保存しません。",  # 誤り!
    retrieval_context=[
        "RedisはRDBスナップショットとAOFロギングで永続ストレージをサポートしています。"
    ]
)
metric = FaithfulnessMetric(threshold=0.8)

このテストケースは失敗する。actual_outputがコンテキストと矛盾しているためだ――あの深夜2時に遭遇したハルシネーションとまったく同じパターンだ。

ContextualRelevancyMetric — generationではなく、retrievalの品質を検証する。FaithfulnessMetricが失敗した場合、このメトリクスがエラーの原因をretrieverかgeneratorかに切り分けるのに役立つ。

GEval — 自然言語で定義するカスタムメトリクスだ。最も強力だがトークン消費量も最大:

from deepeval.metrics import GEval
from deepeval.test_case import LLMTestCaseParams

correctness_metric = GEval(
    name="Correctness",
    criteria="回答は技術的に正確ですか?",
    evaluation_params=[LLMTestCaseParams.INPUT, LLMTestCaseParams.ACTUAL_OUTPUT],
    threshold=0.7
)

実際のpytestワークフローへの統合

DeepEvalはpytestとネイティブに統合できる――新しいAPIを覚える必要はない:

import pytest
from deepeval import assert_test
from deepeval.metrics import AnswerRelevancyMetric, FaithfulnessMetric
from deepeval.test_case import LLMTestCase

# あなたのアプリのLLM呼び出し関数と仮定する
from your_app import get_rag_response

@pytest.mark.parametrize("question,context,expected_topics", [
    (
        "DockerとVMの違いは何ですか?",
        ["Dockerはコンテナアイソレーションを使用し、ホストOSとカーネルを共有するためVMより軽量です。"],
        ["container", "kernel"]
    ),
    (
        "Kubernetesは何に使いますか?",
        ["Kubernetesはコンテナオーケストレーションプラットフォームで、コンテナのデプロイ・スケーリング・管理を自動化します。"],
        ["orchestration", "deploy"]
    ),
])
def test_rag_quality(question, context, expected_topics):
    response = get_rag_response(question, context)
    
    test_case = LLMTestCase(
        input=question,
        actual_output=response,
        retrieval_context=context
    )
    
    assert_test(test_case, [
        AnswerRelevancyMetric(threshold=0.7),
        FaithfulnessMetric(threshold=0.8)
    ])

通常のpytestと同じように実行する:

deepeval test run test_rag_quality.py -v
# または
pytest test_rag_quality.py --deepeval  # pytestの出力フォーマットを維持したい場合

データセットベースの評価

テストケースが10件以上ある場合、EvaluationDatasetを使うとスッキリ管理できる:

from deepeval.dataset import EvaluationDataset
from deepeval.test_case import LLMTestCase

dataset = EvaluationDataset(test_cases=[
    LLMTestCase(
        input="Git rebaseとmergeの違いは何ですか?",
        actual_output=get_response("Git rebaseとmergeの違いは何ですか?"),
        expected_output="rebaseはcommit historyを書き換え、mergeはmerge commitを新たに作成します。"
    ),
    LLMTestCase(
        input="SSH key-based authenticationはどのように機能しますか?",
        actual_output=get_response("SSH key-based authenticationはどのように機能しますか?"),
        expected_output="public/private keyペアを使い、サーバーがpublic keyで認証します。"
    )
])

dataset.evaluate([AnswerRelevancyMetric(threshold=0.7)])

CI/CDへの統合

これが私がproductionで運用しているCI/CDワークフローだ。promptやLLMコードを変更するPRがあるたびに起動する:

# .github/workflows/llm-eval.yml
name: LLM Quality Gate

on:
  pull_request:
    paths:
      - 'prompts/**'
      - 'app/llm/**'

jobs:
  evaluate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.11'
      
      - name: Install dependencies
        run: pip install deepeval anthropic
      
      - name: Run LLM evaluations
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}  # DeepEvalはデフォルトでGPTをjudgeとして使用
        run: deepeval test run tests/llm/

結果として、promptを変更するPRはすべてquality gateを通過しなければならなくなった。productionにデプロイしてからチャットボットがおかしなことを言っているのに気づくことはもうない。

実践的なTips

1. 予算に合ったjudge modelを選ぶ

DeepEvalはデフォルトでGPT-4oをjudgeとして使用する。コストが高い。オーバーライドできる:

from deepeval.models import GPTModel

# GPT-4o-miniを使う(約30倍安い)
metric = AnswerRelevancyMetric(
    threshold=0.7,
    model=GPTModel(model="gpt-4o-mini")
)

あるいは、あなたのアプリがOpenAIを使っているなら、利益相反を避けるためにClaudeをjudgeとして使うこともできる:

from deepeval.models.base_model import DeepEvalBaseLLM
import anthropic

class ClaudeJudge(DeepEvalBaseLLM):
    def __init__(self):
        self.client = anthropic.Anthropic()
    
    def load_model(self):
        return self.client
    
    def generate(self, prompt: str) -> str:
        message = self.client.messages.create(
            model="claude-haiku-4-5-20251001",
            max_tokens=1024,
            messages=[{"role": "user", "content": prompt}]
        )
        return message.content[0].text
    
    async def a_generate(self, prompt: str) -> str:
        return self.generate(prompt)
    
    def get_model_name(self):
        return "claude-haiku"

2. すべてのメトリクスを毎回テストしない

1つのメトリクス = 1〜2回のLLMコール = コストと時間がかかる。テストを分類しよう:

  • クイックユニットテスト(毎コミット実行):AnswerRelevancyMetricのみ
  • インテグレーションテスト(PR毎に実行):FaithfulnessMetricを追加
  • フル評価(週次またはリリース前):全メトリクス + カスタムGEval

3. テストケースをYAML/JSONファイルに保存する

PythonにテストデータをハードコードするのはNG:

from deepeval.dataset import EvaluationDataset

# ファイルから読み込む
dataset = EvaluationDataset()
dataset.pull(alias="production-eval-set")  # Confident AIを使う場合

# またはローカルJSONから
import json
with open("test_cases.json") as f:
    cases = json.load(f)
    
dataset = EvaluationDataset(test_cases=[
    LLMTestCase(**case) for case in cases
])

4. Thresholdはドメインごとにチューニングする

0.7は安全な出発点だが、魔法の数字ではない。より現実的なアプローチ:まずゴールデンデータセットで評価を実行し、スコアの分布を確認する。その後、下から10〜15パーセンタイル付近にthresholdを設定する――キリのいい数字にこだわらなくていい。

追記:専門的な技術的質問は、回答が正しくても単純な質問より低いスコアになる傾向がある。judge modelにもバイアスがある――スコアを絶対的な真実として扱わないこと。

DeepEvalを数週間使った結果、system promptを変更するたびに不安を感じることはなくなった。変更したらテストを実行し、スコアの変化を確認するだけだ。FaithfulnessMetricがthresholdを下回れば、新しいpromptがモデルのハルシネーションを増やしていることがすぐわかる――どこかにデプロイする前に。

Share: