UIと同じようにLLMをテストしよう

Dev.to / 2026/4/8

📰 ニュース

要点

  • チュートリアルでは、チャットボット向けのUIスタイルのPlaywrightテストは不十分だと主張している。理由は、多くの場合「回答が存在すること」だけを確認し、「正確であること」や「根拠があること(grounded)」を検証できていないから。
  • @llmassert/playwright は、Playwrightの expect() を拡張し、幻覚、PII(個人を特定できる情報)の露出、トーン、形式、セマンティックな正確性を評価する5つのLLM搭載マッチャを追加する。
  • LLMAssert はLLMジャッジ(既定では GPT-5.4-mini)を用いて出力を採点し、{ pass, score, reasoning } を返す。これにより、二値の合否チェックではなく「数値化された品質シグナル」を提供する。
  • ジャッジが利用できない、または採点できない場合(例:APIの遅延や障害)には、アサーションは INCONCLUSIVE になる。そのうえで意図的にテストをパスさせ、CIパイプラインを壊さないようにする。
  • セットアップは軽量:パッケージをインストールし、.env 経由で OpenAI API キーを用意し、既存のPlaywrightワークフロー内でアサーションを書く。
  • category slugs: [

このチュートリアルは@llmassert/playwright v0.6.0向けに書かれました。

チャットボットを作りました。Playwrightのテストも通っています。ですがユーザーから、「幻覚(hallucination)」された回答が報告されています。つまり、もっともらしく聞こえるのに、完全に作り話の自信満々な返答です。

問題は?テストではチャットボットが返答することを確認しているだけで、正しく返答しているかは見ていない点です。toContainのアサーションでは、根拠のある回答と幻覚を見分けられません。出力を本当に理解するアサーションが必要です。

@llmassert/playwrightは、Playwrightのexpect()にLLMパワーのマッチャーを5つ追加します。幻覚、PII(個人情報)、トーン、形式、そしてセマンティックな正確さをチェックします。同じテストフレームワーク、同じワークフロー、そして新しいスーパーパワーです。

このチュートリアルでは、約10分でゼロから5つの動作するLLMアサーションまで到達します。新しいフレームワークは学びません——Playwrightを知っていれば、必要なものの90%はすでに分かっています。

まず最初に知っておくべきこと: 「inconclusive(判定不能)」とはどういう意味か

LLMAssertは、LLM(デフォルトではGPT-5.4-mini)をジャッジ(judge)として使い、出力を評価します。しかし、LLM APIは遅かったり、一時的に利用できないことがあります。

ジャッジがスコアを返せない場合、結果はinconclusive(判定不能)となり、テストはパスします。これは仕様です。提供元の障害がCIパイプラインを止めるべきではありません。

テスト実行
    │
    ▼
ジャッジが出力を評価
    │
    ├── スコア ≥ 閾値  →  PASS  ✓
    ├── スコア < 閾値  →  FAIL  ✗
    └── ジャッジが利用不可  →  INCONCLUSIVE(パス)≈

すべてのマッチャーは{ pass: boolean, score: number | null, reasoning: string }を返します。スコアの範囲は0.0〜1.0で、判定不能の場合はnullです。単なるパス/フェイルではなく、数値による品質のシグナルが得られます。

これらの例を実行するコストは、API呼び出し1回あたり1ペニー未満です(GPT-5.4-miniの価格)。

セットアップ(2分)

パッケージをインストールします:

pnpm add -D @llmassert/playwright
# または: npm install -D @llmassert/playwright

プロジェクトのルートに.envファイルを作成し、OpenAIのAPIキーを設定します:

OPENAI_API_KEY=your_openai_api_key_here

.env.gitignoreに含まれていることを確認してください。Playwrightのプロジェクトでは通常すでに入っていますが、念のためコミット前に再確認してください。

これで完了です。最初のLLMアサーションを書ける状態になりました。

変更するインポートは1つだけです。 @playwright/testの代わりに@llmassert/playwrightからtestexpectをインポートしてください。これにより、5つのLLMマッチャーと、ワーカー(worker)スコープのジャッジ用フィクスチャが使えるようになります。playwright.config.tsはそのままで大丈夫です。このパッケージはESMとCJSの両方を提供しているため、require()でも動きます。

// Before
import { test, expect } from "@playwright/test";

// After
import { test, expect } from "@llmassert/playwright";

幻覚を検知する

ここに、チャットボットの応答をチェックする典型的なPlaywrightテストがあります:

import { test, expect } from "@playwright/test";

test("chatbot answers FAQ correctly", async ()=> {
  const response = "Our return window is 90 days from purchase.";

  // これは通ります!でも応答は間違っています...
  expect(response).toContain("return");
});

「return」という単語が応答に含まれているため、このテストはパスします。しかし、実際の返金ポリシーは30日です。チャットボットは幻覚を起こしているのに、テストがそれを捕捉できていません。

ではLLMAssertを使うと:

import { test, expect } from "@llmassert/playwright";

test("chatbot answers FAQ correctly", async ()=> {
  const response = "Our return window is 90 days from purchase.";
  const faqDocs = "Returns accepted within 30 days. No restocking fee.";

  // これは失敗します!ジャッジが90日/30日の食い違いを検知します。
  await expect(response).toBeGroundedIn(faqDocs);
});

awaitに注目してください——LLMAssertのマッチャーは、ジャッジモデルを呼び出すためasyncです。toContainのような通常のPlaywrightマッチャーは同期で、awaitは不要です。

toBeGroundedInマッチャーは、応答とソースのコンテキストの両方をジャッジモデルに送信し、証拠に対してすべての主張を照合します。「90日」という主張は、ソースドキュメントにある「30日」と矛盾しているため、テストはスコアと、何が間違っているのかを平易な英語で説明する内容付きで失敗します。

これが、LLMアサーションが正規表現やtoContainと異なる点です。判定者は文字列の一致だけでなく意味を理解します。言い換えられた幻覚(ハルシネーション)、微妙な矛盾、そして従来のアサーションをすり抜けてしまうでっちあげの詳細も見つけ出します。

5つのマッチャー

toBeGroundedIn — 幻覚を検出

出力中のあらゆる主張は、あなたが提供する文脈によって裏付けられていなければなりません。FAQボット、RAGパイプライン、そしてソース文書から回答すべきあらゆるシステムに最適です。

test("support answer is grounded in knowledge base", async () => {
  const response = "We offer a 30-day money-back guarantee on all plans.";
  const knowledgeBase = "All plans include a 30-day money-back guarantee. No questions asked.";

  await expect(response).toBeGroundedIn(knowledgeBase);
});

toBeFreeOfPII — 個人情報を検出

名前、メールアドレス、電話番号、住所などをスキャンします。スコアが1.0ならテキストはクリーン(問題なし)で、0.0ならPIIが確実に見つかったことを意味します。

test("support response does not leak customer PII", async () => {
  const response = "Your order #12345 has been shipped and should arrive Friday.";

  await expect(response).toBeFreeOfPII();
});

// Verify PII IS present (e.g., in a profile summary)
test("profile includes user details", async () => {
  const summary = "Account holder: Jane Smith, jane@example.com";

  await expect(summary).not.toBeFreeOfPII();
});

toMatchTone — ブランドボイスを強制

テキストが自然言語のトーン記述に一致していることを検証します。ユーザーが苛立っていても、ボットが常に自社らしさ(ブランド)に合う応答になるようにするために使えます。

test("support replies stay professional under pressure", async () => {
  const response = "I understand your frustration. Let me look into this right away and find a solution for you.";

  await expect(response).toMatchTone("empathetic and solution-oriented");
});

toBeFormatCompliant — 出力の構造を確認

テキストが記述されたフォーマットに準拠していることを検証します。schemaパラメータはJSON Schemaではなく、自然言語の説明です。

test("product description follows template", async () => {
  const description = "Introducing the CloudWidget Pro.

- 99.9% uptime
- Auto-scaling
- 24/7 support

Start your free trial today.";

  await expect(description).toBeFormatCompliant(
    "Three paragraphs: overview, key features as bullet list, call to action"
  );
});

toSemanticMatch — 意味の保持を検証

2つのテキスト間の意味的な類似性を比較します。翻訳、要約、言い換えられたコンテンツのテストに最適です。

test("summary preserves key meaning", async () => {
  const original = "The quarterly revenue increased by 15% driven by strong demand in the enterprise segment.";
  const summary = "Revenue grew 15% this quarter, led by enterprise sales.";await expect(summary).toSemanticMatch(original);
});

閾値の調整

すべてのマッチャーには、合否を判断するためのしきい値(デフォルト: 0.7)が使われます。インラインで上書きします:

// 医療コンテンツの厳密な根拠付け
await expect(response).toBeGroundedIn(context, { threshold: 0.95 });

// 創造的な言い換えに対する緩やかなマッチング
await expect(summary).toSemanticMatch(reference, { threshold: 0.6 });

なぜカスタムの評価スクリプトを書くだけではいけないのか?

テストから OpenAI API を直接呼び出して、レスポンスを自分で解析することもできます。しかし、その場合は次の対応が必要になります:

  • API が停止しているときのフォールバックロジック(CI が壊れないようにするため)
  • テストスイート全体をブロックしないタイムアウト処理
  • プロンプト種別が異なる場合のスコア正規化
  • 経時的なスコア追跡のための結果収集
  • レート制限(並列テスト実行で API クォータを使い切ってしまわないようにする)

LLMAssert はこれらをすべて、あなたがすでに使っている同じ expect() インターフェースの裏側で、最初から用意しています。

実行をまたいだ結果の追跡

アサーションは単体で動作します — アカウントは不要です。ですが、時間の経過に伴うスコアの追跡、退行の検知、チームとの結果共有をしたい場合は、任意のダッシュボードレポーターを追加してください。

それを playwright.config.ts に追加します:

import { defineConfig } from "@playwright/test";

export default defineConfig({
  reporter: [
    ["list"],
    [
      "@llmassert/playwright/reporter",
      {
        projectSlug: "my-project",
        apiKey: process.env.LLMASSERT_API_KEY,
      },
    ],
  ],
});

このレポーターは評価結果をバッチ処理して、各テスト実行後に LLMAssert ダッシュボード に送信します。ダッシュボードに到達できない場合でも、テストは引き続きパスします — レポーターはデフォルトで onError: 'warn' です。

ネットワーク呼び出しなしのローカル専用モードで実行するには apiKey を省略してください。

API キーを理解する

このチュートリアルでは最大 3 つの環境変数を使います。それぞれ役割が異なります:

変数 ソース 用途 漏洩した場合
OPENAI_API_KEY OpenAI ダッシュボード 主要なジャッジ(GPT-5.4-mini)を動かします。Anthropic のみを使う場合を除き必須です。 あなたの OpenAI アカウントで費用が発生
ANTHROPIC_API_KEY Anthropic コンソール フォールバックのジャッジ(Claude Haiku)を動かします。任意です。 あなたの Anthropic アカウントで費用が発生
LLMASSERT_API_KEY LLMAssert ダッシュボード 結果をダッシュボードへ送信します。任意です。 あるプロジェクトにテストデータを書き込み

OPENAI_API_KEY または ANTHROPIC_API_KEY の少なくとも 1 つは設定する必要があります。どちらも存在しない場合、すべてのアサーションが「判断不可(inconclusive)」を返します。

フォールバックのジャッジを追加する

耐障害性のために、Claude Haiku をフォールバックとして追加できます。主要モデルが失敗した場合、結果が「判断不可」とマークされる前にフォールバックが引き継ぎます。

pnpm add -D @anthropic-ai/sdk
# .env に追加
ANTHROPIC_API_KEY=your_anthropic_api_key_here

フォールバックは自動的に有効化されます — コード変更は不要です。

OPENAI_API_KEY set? ──yes──▶ GPT-5.4-mini
                              │
                         success? ──yes──▶ return score
                              │
                              no
                              ▼
ANTHROPIC_API_KEY set? ──yes──▶ Claude Haiku
                              │
                         success? ──yes──▶ return score
                              │
                              no
                              ▼
                         inconclusive (test passes)

次にテストすべきこと

従来のアサーションでは見逃してしまう問題を、5 つのマッチャーがどのように検出できるかを見てきました。次は、あなた自身のテストスイートで次のようなアイデアを試してみてください:

  • RAG パイプライン:取得したドキュメントをコンテキストとして toBeGroundedIn を使います。これは、検索拡張生成(retrieval-augmented generation)システムにとって最も価値の高い単一のアサーションです。
  • 顧客向けボット:安全性とブランド準拠のために toBeFreeOfPII + toMatchTone を組み合わせます。2 つのマッチャー、1 つのテスト、2 つの失敗パターンを検知できます。
  • コンテンツ生成:構造化されたテンプレートを強制するために toBeFormatCompliant を使います。特に、下流のシステムが特定の形式を期待している出力に有用です。
  • 多言語機能:翻訳や要約を検証するために toSemanticMatch を使います。バックトランスレーションのパターン(翻訳してから、さらに翻訳し直して、最後に元のものと比較する)は、品質シグナルとして驚くほどうまく機能します。
  • 返却形式: {"translated": "翻訳されたHTML"}
  • 回帰テスト:プロンプトのバージョン間で同じアサーションを実行し、スコア分布がシフトするかどうかを確認します。ダッシュボードのレポーターがこの可視化を行います。

5つのマッチャーすべてが .not の否定(ネゲーション)をサポートしています。クリエイティブな出力がテンプレートに基づいていないことをアサートしたい場合や、応答が特定のユーザー情報を含むことを確認したい場合に便利です。

このパッケージはMITライセンスで、無料で利用できます。ドキュメントを確認し、GitHub上のソースを閲覧するか、今すぐインストールしてください:

pnpm add -D @llmassert/playwright

LLMAssertチームが作成しました。役に立った場合はGitHubでスターを付けてください