Webエンジニア向けプログラミング解説動画をYouTubeで配信中!
▶ チャンネル登録はこちら

【ITニュース解説】Assert LLM output in the test runner you already use

2026年10月09日に「Dev.to」が公開したITニュース「Assert LLM output in the test runner you already use」について初心者にもわかりやすく解説しています。

作成日: 更新日:

ITニュース概要

AIが出力する複雑なデータ(JSONなど)のテストは、手動での処理が多くエラーも分かりにくい課題があった。`expect-llm`は、既存のテストツール(Vitest/Jest)にAI出力の検証に特化した機能を追加するライブラリだ。これにより、出力の形式や内容、スキーマ検証などを簡潔なコードで正確にテストでき、開発効率が向上する。

ITニュース解説

大規模言語モデル(LLM)は現代のソフトウェア開発において不可欠な存在となりつつあるが、その一方で、LLMの出力をプログラムで確実に扱うためのテストは多くの課題を抱えている。LLMは自然言語を扱うため、厳密なルールに従った出力を常に保証することは難しい。例えば、JSON形式でデータが欲しいと指示しても、出力がMarkdownのコードブロックで囲まれていたり、不要な説明文が混ざっていたり、あるいは少しだけJSONの構文が間違っていたりすることが頻繁に起こる。

このようなLLMの出力をテストしようとすると、開発者は手作業で複雑なコードを書く羽目になる。具体的には、まず出力文字列から不要なMarkdownのフェンス(json や といった囲み)を取り除き、それをJSONとして解析(パース)する必要がある。このパース処理でエラーが発生した場合に備えて、エラーを捕捉するtry/catchブロックを記述するが、この方法ではJSONパースが失敗した際の具体的な原因が隠されてしまい、「有効なJSONではありません」といった漠然としたエラーメッセージしか得られないことが多い。さらに、解析されたデータが期待通りの構造や値を持っているかを検証するために、Zodのようなスキーマバリデーションライブラリを使うことになるが、これも検証が失敗した際に「期待される結果と異なりました」といった一般的なメッセージしか表示されず、どのフィールドがどのような理由で問題だったのかが分かりにくいという問題がある。また、出力に特定のキーワードが含まれているか、あるいは含まれていないかをチェックするためにincludesメソッドを使うこともあるが、これも同様に、どのキーワードが見つからないのか、または含まれてしまったのかといった具体的な情報が得にくい。これらの手作業によるテストコードは冗長になりがちで、他のテストで少し内容が変わるだけでも、また似たようなコードを書き直す必要が生じ、開発効率が低下する原因となる。

LLMの評価には、promptfooやDeepEvalといった専用の評価フレームワークも存在する。これらはLLM全体の品質や性能を総合的に評価するのに非常に強力なツールだが、開発中のアプリケーションにおいて「この特定のLLM呼び出しの応答が、ユニットテストとして正しく動作しているか」という目的で使うには、設定ファイルの記述や専用の実行環境の用意など、学習コストや手間がかかりすぎる場合が多い。開発者は、普段から使い慣れているVitestやJestといったテストランナーの中で、もっと手軽にLLMの出力を検証したいと考えている。

このような課題を解決するために登場したのが、「expect-llm」というライブラリだ。これは、VitestやJestといった既存のJavaScriptテストランナーに、LLMの出力検証に特化した便利な機能(「マッチャー」と呼ぶ)を追加するライブラリである。expect-llmを一度セットアップファイルに登録するだけで、これまで手作業で書いていた複雑で分かりにくいテストコードを、より簡潔で直感的、かつ具体的なエラーメッセージを提供する形に置き換えることができる。

expect-llmが提供する主要なマッチャーを見てみよう。

toBeValidJSON(options?)は、LLMの出力が有効なJSON文字列であるかを検証する。特に便利なのは、{ allowFences: true }というオプションを指定すると、出力がMarkdownのコードフェンス(例えば、json\n...json\n のような形式)で囲まれていても、自動的にそれらを取り除いてからJSONの検証を行ってくれる点だ。これにより、LLMから生の出力が返された際に、前処理なしで直接テストできる。

toMatchSchema(schema)は、Zodのようなスキーマ定義ライブラリで作成されたデータ構造の設計図(スキーマ)に従って、LLMの出力が正しく形成されているかを検証する。出力がJSON文字列であれば自動的にパースされ、toBeValidJSONと同様にコードフェンスも許容されるため、期待するデータの形になっているかを簡単に、かつ厳密に確認できる。

toContainAll(items), toContainNone(items), toContainAny(items)は、LLMの出力文字列の中に、特定のキーワードが全て含まれているか、全く含まれていないか、あるいは少なくとも一つが含まれているかをチェックする。例えば、toContainNone(["as an AI", "TODO"])と記述することで、LLMが出力すべきでない特定のフレーズ(「AIとして」といった自己言及や、開発中のメモなど)が誤って含まれていないかを検証できる。これらのマッチャーが失敗した際には、どのキーワードが問題だったのかを具体的に示してくれるため、修正が容易になる。

toBeOneOf(allowed)は、LLMの出力が、あらかじめ定められた選択肢(例えば、アプリケーションが処理すべき「refund」「escalate」「deny」のようなカテゴリ)のいずれか一つであるかを検証する。特定のカテゴリ分類や意思決定の結果をテストする際に非常に役立つ。

toMatchStructure(reference)は、LLMの出力が、特定の参照オブジェクトと同じ「構造」(オブジェクトのキーとその値のデータ型)を持っているかをチェックする。このマッチャーは、値そのものではなく、データ構造の安定性を保証するためのものだ。例えば、LLMが生成するIDや数値がテストごとに変わる可能性があっても、そのデータの「形」が常に一定であることを確認できるため、プロンプトの調整によって具体的な値が変わってもテストが壊れないというメリットがある。

これらのマッチャーは、ほとんどの場合、同期的に動作し、ネットワーク呼び出しを伴わない。そのため、ユニットテストは高速に実行され、CI/CDパイプラインの時間を無駄にしない。

ただし、LLMの出力が「丁寧な返答であるか」といった主観的な評価を必要とする場合もある。これに対応するのがtoSatisfy(rubric, judge)マッチャーだ。このマッチャーは、ユーザーが独自の評価ロジック(「ジャッジ」と呼ぶ)を実装して提供することを前提としている。ジャッジ関数は、LLMの出力と評価基準(ルブリック)を受け取り、別のLLMモデルを呼び出して評価を行うといった、非同期の処理を含むことができる。この設計により、客観的に検証可能な項目は高速なマッチャーでテストし、主観的な評価が必要な場合にのみ、ユーザーが定義した評価ロジックを通して外部のモデルを呼び出す、という柔軟なテスト戦略が可能になる。

expect-llmは、開発者が日常的に使用するVitestやJestといったテストランナーにシームレスに統合できる。Vitestを使用している場合はexpect-llmから、Jestを使用している場合はexpect-llm/jestからマッチャーをインポートし、一度expect.extend(llmMatchers)を呼び出すだけで準備は完了だ。

また、expect-llmは他のツールと組み合わせることで、さらに強力な開発ワークフローを構築できる。例えば、「coerce-json」というライブラリは、LLMの出力が完璧なJSONでなくても、可能な限りスキーマに合うように修正(矯正)してくれる。このcoerce-jsonでLLMの出力をまず修正し、その修正された結果に対してexpect-llmのtoMatchSchemaで検証を行うという流れは、LLMの不安定な出力を安定した形で扱うための堅牢なアプローチとなる。このように、expect-llmは単独で利用できるだけでなく、一連のLLM開発ツールチェーンの一部としても機能する。

expect-llmは、ゼロランタイム依存性、非常に小さいファイルサイズという特徴も持っており、既存のプロジェクトに導入する際の負担が少ない。そして何よりも、このライブラリの最大の目的は、テストが失敗した際に「何が、なぜ問題だったのか」を開発者に具体的に伝える、読みやすいエラーメッセージを提供することだ。手動で書かれた冗長なtry/catchやincludesの記述をなくし、開発者がLLMとの対話における信頼性を高めるために、expect-llmは非常に有効なツールとなるだろう。

関連コンテンツ

関連IT用語

関連ITニュース