AIエージェントニュース編集部

LLMエージェントの幻覚をテストで検出する、CI/CD組み込みチェックリスト

LLMエージェントを本番環境に投入しようとしたとき、多くのチームが最初にぶつかる壁が「出力の正確性をどう担保するか」という問題です。ユニットテストやインテグレーションテストが当たり前になったWebバックエンドとは違い、LLMの出力は確率的で、同じ入力でも毎回異なる結果が返ります。テストを書こうにも、何をアサーションすればよいか分からない——そんな状況が続いているプロジェクトは少なくありません。この記事では、 LLM幻覚検出 を軸にした エージェントテスト の設計から、 CI/CD自動化 への組み込みまでを、実際のコードレベルで整理します。

エージェント導入の現実:幻覚・不正確・矛盾する出力がなぜ起きるのか

LLMが誤った情報を自信満々に返す「幻覚(Hallucination)」は、RAGや外部ツール呼び出しを加えても完全には消えません。その原因は大きく三つに分類できます。

  1. 知識の断絶 :モデルの学習データにカットオフがあるため、最新情報や社内固有知識が欠落している
  2. コンテキストの希薄化 :プロンプトが長くなるにつれ、参照すべき文脈が埋もれて無視される
  3. エージェントの連鎖エラー :ツール呼び出し結果が次のステップへ引き継がれる際に誤りが増幅する

特にマルチステップエージェントでは、一つのステップの小さな誤りが後段のアクション全体を狂わせます。ドキュメント検索→要約→API呼び出しという三段構成なら、要約の時点での幻覚がAPIへの誤ったパラメータ送信に直結します。

出力検証の三層構造:セマンティック検証・事実性スコアリング・パターンマッチング

現時点で実務に使えるLLM出力検証は、三つのアプローチの組み合わせで成り立っています。

セマンティック検証

生成テキストと正解テキストをembeddingで比較し、コサイン類似度が閾値を超えるかどうかを判定します。完全一致を要求しないため、表現の揺れには強い反面、意味が逆転した誤りを見逃すことがあります。

事実性スコアリング

RagasTruLens のような RAG検証 フレームワークが提供する指標(Faithfulness・Answer Relevancy・Context Precisionなど)を用い、生成回答が参照コンテキストに忠実かどうかをスコア化します。これらのフレームワークはLangChainおよびLlamaIndexとのインテグレーションが整備されており、比較的少ない設定で計測を始められます。

パターンマッチング

正規表現や構造検証(JSONスキーマ、Pydanticモデル)で出力形式の整合性を確認します。もっとも計算コストが低く、フォーマット違反を即座に検出できます。

この三層を 「パターンマッチング → 事実性スコアリング → セマンティック検証」 の順番で適用すると、コストを抑えながら精度を上げられます。

実装の第一歩:LangChain Evaluators と LlamaIndex Correctness Checks を組み合わせたテストスイート

具体的なコードで確認しましょう。まず依存関係を整理します。

# requirements-test.txt
langchain-community>=0.2
llama-index-core>=0.11
ragas>=0.2
pytest
pytest-asyncio

LangChainの QAEvalChain を使った事実性チェックの例です。

import pytest
from langchain.evaluation import load_evaluator
from langchain_openai import ChatOpenAI

@pytest.fixture
def evaluator():
    llm = ChatOpenAI(model="gpt-4o", temperature=0)
    return load_evaluator("qa", llm=llm)

def test_agent_answer_faithfulness(evaluator, agent_output):
    result = evaluator.evaluate_strings(
        input="2025年度の売上合計はいくらですか?",
        prediction=agent_output["answer"],
        reference="2025年度の売上合計は1億2000万円です。",
    )
    # scoreが0.5未満なら幻覚の疑いあり
    assert result["score"] >= 0.5, f"Faithfulness too low: {result}"

LlamaIndexの CorrectnessEvaluator と組み合わせると、RAGパイプライン全体の検証が可能になります。

from llama_index.core.evaluation import CorrectnessEvaluator, FaithfulnessEvaluator
from llama_index.llms.openai import OpenAI

llm = OpenAI(model="gpt-4o", temperature=0)
correctness_eval = CorrectnessEvaluator(llm=llm)
faithfulness_eval = FaithfulnessEvaluator(llm=llm)

async def run_evals(query, response, contexts, reference):
    correctness = await correctness_eval.aevaluate(
        query=query,
        response=response,
        reference=reference,
    )
    faithfulness = await faithfulness_eval.aevaluate(
        query=query,
        response=response,
        contexts=contexts,
    )
    return correctness, faithfulness

テストデータは ゴールデンデータセット として管理するのがポイントです。tests/fixtures/golden_qa.jsonl のようなファイルにクエリ・正解・参照コンテキストをまとめておき、CIで毎回回すことで回帰チェックができます。

CI/CDパイプラインへの組み込み:GitHub Actions × Output Validators の実践例

GitHub Actionsでの組み込み例です。PRごとにLLM評価を自動実行し、スコアが閾値を下回るとマージをブロックします。

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

on:
  pull_request:
    paths:
      - "src/agents/**"
      - "prompts/**"

jobs:
  evaluate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.12"

      - name: Install dependencies
        run: pip install -r requirements-test.txt

      - name: Run LLM evaluation suite
        env:
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
        run: |
          pytest tests/eval/ \
            --json-report \
            --json-report-file=eval-results.json \
            -v

      - name: Upload eval results
        uses: actions/upload-artifact@v4
        with:
          name: eval-results
          path: eval-results.json

コスト管理のため、以下のルールを設定しておくことを推奨します。

  1. 変更差分トリガーpaths フィルタでエージェントやプロンプト関連ファイルの変更時のみ実行
  2. キャッシュの活用 :同一入力に対するLLM呼び出し結果はRedisや diskcache でキャッシュし、二重課金を防ぐ
  3. サンプリング :ゴールデンデータセット全体ではなく、ランダムに30〜50件をサンプリングしてCIを軽量化

落とし穴と限界:テストを増やしすぎると推論コストが爆増する理由と、実務的な妥協点

品質保証 の観点でテストを増やすほど良いと思いがちですが、LLM評価には独特のコスト構造があります。評価器自体がLLMである場合、テスト1件につきAPI呼び出しが2〜4回発生します。100件のゴールデンデータセットを全件評価すると、1回のCI実行で数百回のAPI呼び出しが走ります。

実務的な妥協点として、以下の戦略が有効です。

  • ティア分け :パターンマッチング(無料)→ 埋め込みベース類似度(安価)→ LLM-as-judge(高価)の順にゲートを設け、前段で失敗したら後段を実行しない
  • 週次フル評価 :CIでは軽量テストのみ実行し、全ゴールデンデータセットの評価は週次バッチで行う
  • プロンプト変更時のみフル実行 :プロンプトファイルの変更をgitで検知したときだけ重い評価を走らせる

また、LLM評価器そのものが誤判定するという問題も見落とせません。評価器が「正解」と判断した出力が実際には誤りであるケースは一定確率で発生します。人間によるサンプリングレビューを月1回程度挟む運用と組み合わせるのが現実的です。

マルチモーダル出力の検証:画像生成・API呼び出しをどう扱うか

テキスト出力に比べ、 画像生成外部APIコール の検証はまだ標準化されていません。ただし、いくつかのアプローチが実用段階に入ってきています。

画像生成の検証

生成された画像に対してVLM(Vision Language Model)をjudgeとして使い、「プロンプトの意図が反映されているか」「有害コンテンツが含まれていないか」をチェックする手法が広まっています。具体的には、生成画像とオリジナルプロンプトをGPT-4oなどのマルチモーダルモデルに渡し、評価スコアを取得します。

def evaluate_image_output(prompt: str, image_base64: str, judge_client) -> float:
    response = judge_client.chat.completions.create(
        model="gpt-4o",
        messages=[{
            "role": "user",
            "content": [
                {"type": "text", "text": f"以下のプロンプトに対してこの画像は適切ですか?1〜5で評価してください。\nプロンプト: {prompt}"},
                {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{image_base64}"}},
            ],
        }],
    )
    # レスポンスから数値を抽出して返す
    return extract_score(response.choices[0].message.content)

ツール呼び出し・API実行の検証

エージェントが外部APIを呼び出す場合、実際のAPIをモックに差し替えた上で「正しいエンドポイントが呼ばれたか」「パラメータは期待値と一致するか」をアサーションします。これは従来のインテグレーションテストに近く、pytest-mockresponses ライブラリで対応できます。重要なのは、 LLMが生成したAPI呼び出しパラメータを構造的に検証する という視点を持つことです。JSONスキーマバリデーションをここに組み込むと、フォーマット違反を低コストで検出できます。

テキスト・画像・ツール呼び出しという三種の出力それぞれに対して検証レイヤーを設計しておくことが、マルチモーダルエージェントを本番稼働させる際の前提条件になりつつあります。

関連記事