「デモは完璧に動いたのに、本番でユーザーの入力に晒したら壊れた」——LLMアプリで最も多い失敗です。原因は明快で、LLMの品質は“目視”では守れないからです。プロンプトを1行変えただけで、直したはずのケースが直り、直っていたケースが壊れる。決定論的なコードなら単体テストが守ってくれる領域を、LLMでは評価(Evaluation)という別の工学で守る必要があります。
結論から言えば、本番のLLMアプリの品質は次の3本柱に集約されます。
- 評価データセット(ゴールデンセット) — 代表例・エッジケース・敵対的入力を集めた“試験問題”。
- 自動採点 — 決定的チェック+LLM-as-judgeで、出力をスコア化する。
- CIゲート — デプロイを評価スコアで止める。回帰を人間の目ではなくパイプラインで検知する。
この記事は、生成AIパイプラインを本番運用してきた立場から、一次情報(OpenAI / Anthropic のeval公式ガイド、LLM-as-judgeの原典論文、RAGAS)に準拠して、TypeScriptの実コードで“本番で回る評価”を設計します。これは、LLMアプリのセキュリティ・MCP本番サーバーと並ぶ、「信頼できる本番AI」の第3の柱です。
なぜLLMアプリのテストは“従来と違う”のか
従来の単体テストは assert(f(x) === expected) です。入力に対して唯一の正解があり、一致するかを二値で判定します。LLMではこれが3点崩れます。
- 非決定的 — 同じ入力で出力が揺れる。1回のパスは“たまたま”かもしれない。
- 出力が自由文 — 「正解」が一意でない。言い回しは違っても意味が正しいことがある。
- 品質が連続量 — 合否ではなく「どれくらい良いか」。だから**アサーションではなく評価(スコアリング)**が要る。
そこで発想を変えます。合否の1/0ではなく、データセット全体の“合格率とスコア分布”で品質を見る。 個々のテストが赤/緑ではなく、「90%のケースで基準を満たすか」をゲートにするのです。
評価データセット(ゴールデンセット)の作り方
すべての評価は“試験問題”から始まります。ここで手を抜くと、以降の採点がいくら精緻でも意味を持ちません。
- 代表例・エッジケース・敵対的入力を混ぜる。 OpenAIもテストデータに「典型的なケース、エッジケース、敵対的なケース」を含めるべきとしています。無関係な入力、極端に長い入力、有害な入力、本質的に曖昧な入力——これらを意図的に入れます。
- 少数から始め、反復的に育てる。 権威ある一次情報は「最低◯件」といった特定の件数を定めていません。手元の代表例から始め、本番で見つけた失敗を1件ずつ足していくのが実務です。OpenAIは評価データを「時間とともに広げていく動的な空間」と表現します。
- 量を優先する。 Anthropicは明快で、「質より量」——少数の高品質な人手採点より、多少ノイズがあっても自動採点できる多数の設問の方が良い、としています。理由は次章に直結します。
採点の3方式を使い分ける
出力をどう採点するか。3つの方式があり、**「決定的に測れるものは決定的に測る」**が鉄則です(安く・速く・再現可能)。
| 方式 | 何を測るか | コスト | 使いどころ |
|---|---|---|---|
| 決定的(コードベース) | 完全一致・JSON schema適合・正規表現・数値の許容誤差・キーワード包含 | 極小 | 形式・構造・事実の一部。最優先 |
| LLM-as-judge | 自由文の意味的品質(忠実性・関連性・トーン) | 中 | 決定的に測れない“良さ” |
| 人間 | 最終的な妥当性・微妙な判断 | 大 | 少数の重要ケース・judgeの較正 |
決定的採点はコードで書けます。Zodスキーマで出力の構造を、単純なチェックで内容を測ります。
import { z } from "zod";
type Case = { input: string; expected?: string; mustInclude?: string[] };
type Score = { name: string; pass: boolean; value: number };
// 完全一致(期待値がある設問だけ判定)
const exactMatch = (out: string, c: Case): Score => {
const pass = c.expected === undefined || out.trim() === c.expected.trim();
return { name: "exact_match", pass, value: pass ? 1 : 0 };
};
// 必須キーワードの包含(事実の欠落を安く検知)
const containsAll = (out: string, c: Case): Score => {
const missing = (c.mustInclude ?? []).filter((k) => !out.includes(k));
return { name: "contains_all", pass: missing.length === 0, value: missing.length ? 0 : 1 };
};
// 出力が期待するJSON構造かをスキーマで判定(形式崩れを回帰として捕まえる)
const matchesSchema = <T>(out: string, schema: z.ZodType<T>): Score => {
const parsed = schema.safeParse(safeJson(out));
return { name: "schema", pass: parsed.success, value: parsed.success ? 1 : 0 };
};
LLM-as-judge を“正しく”使う
自由文の品質は、強いLLMに採点させる LLM-as-judge が現実解です。原典論文(Zheng et al., 2023)は、GPT-4級のjudgeが人間同士の一致度と同等(80%超)でヒトの選好を近似できると示しました。スケーラブルで説明可能——ただし同じ論文が明確なバイアスも報告しています。
- 位置バイアス — 先に提示された回答を有利に採点しがち。
- 冗長性バイアス — 長い回答を過大評価しがち。
- 自己優遇バイアス — 自分(同系統モデル)の出力を好みがち。
- 推論の弱さ — 数学・論理の採点は苦手。
対策は設計で効かせます。(1) rubric(採点基準)を先に固定し曖昧さを消す、(2) ペア比較では順序を入れ替えて両方向で採点し位置バイアスを相殺、(3) 判定は必ずスキーマで構造化する、(4) まず強モデルで始め、人間の評価と一致することを確認してから軽量モデルへスケール(OpenAIの公式推奨)。
import { generateObject } from "ai";
import { z } from "zod";
// judge の出力は必ず構造化する(自由文で"良い"と言わせない)
const Judgment = z.object({
faithful: z.boolean(), // 与えた文脈に忠実か(ハルシネーションなし)
relevant: z.boolean(), // 質問に答えているか
score: z.number().int().min(1).max(5), // rubric に基づく5段階
reason: z.string().max(300),
});
async function judge(input: string, output: string, context: string) {
const { object } = await generateObject({
// 判定は強モデルで開始。人間と一致したら軽量モデルへ置換してコストを下げる。
model: "anthropic/claude-opus-4-8",
schema: Judgment,
prompt:
"次の回答を rubric で採点。基準: (1)文脈に忠実か (2)質問に関連するか。\n" +
`# 質問\n${input}\n# 文脈\n${context}\n# 回答\n${output}`,
});
return object; // 検証済みの構造化スコア
}
RAG特有の評価:検索と生成を分けて測る
RAG(検索拡張生成)は、検索の失敗と生成の失敗が混ざります。切り分けて測らないと、どこを直すべきか分かりません。RAG評価の定番 RAGAS の指標(公式定義):
- Faithfulness(忠実性) — 回答が検索された文脈に対してどれだけ事実整合的か(ハルシネーション検知)。
- Answer / Response Relevancy — 回答がユーザーの質問にどれだけ関連するか。
- Context Precision — 取得した文脈のうち、関連チャンクを上位に並べられたか(検索の適合率)。
- Context Recall — 必要な関連文書をどれだけ取りこぼさず取得できたか(検索の再現率)。
Faithfulnessが低ければ生成(プロンプト・モデル)を、Context Recallが低ければ検索(埋め込み・チャンク・認可フィルタ)を疑います。RAGの精度改善は RAGの本番アンチパターン、検索設計は pgvectorによる本番RAG を参照してください。
回帰テストをCIに組み込む
評価は、**CIに組み込んで初めて品質を“守る”**ものになります。手元で1回良くても意味がない。デプロイを評価スコアでゲートします。固定データセットに対する合格率が閾値を割ったら、ビルドを失敗させます。
// 集計とCIゲート:合格率が閾値未満なら exit 1 でデプロイを止める
type Grader = (out: string, c: Case) => Score;
async function runEval(
cases: Case[],
runApp: (input: string) => Promise<string>,
graders: Grader[],
threshold = 0.9,
): Promise<void> {
let passed = 0;
for (const c of cases) {
const out = await runApp(c.input);
// 決定的グレーダーは全通過を要求(ここに judge を足して重み付けもできる)
const ok = graders.every((g) => g(out, c).pass);
if (ok) passed++;
}
const rate = passed / cases.length;
console.log(`pass rate: ${(rate * 100).toFixed(1)}% (${passed}/${cases.length})`);
if (rate < threshold) {
console.error(`❌ eval ${(rate * 100).toFixed(1)}% < 閾値 ${threshold * 100}% — デプロイを中止`);
process.exit(1); // CI が赤 → マージ/デプロイされない
}
}
ツールを使うなら promptfoo が実務的です。GitHub Actionが、PRでプロンプト変更の前後を評価して結果をPRに投稿します——コードレビューと同じ粒度で“品質のdiff”が見えるわけです。CIの土台は GitHub Actions の鍵レスCI/CD と同じ発想で組めます。
補足:これは AI駆動開発の品質ゲート(AIが“書いたコード”の品質を守る話)とは別レイヤーです。本記事は、AIアプリが“生成する出力”そのものの品質を守る話です。両方が揃って初めて本番品質になります。
オフライン評価とオンライン評価:両輪で回す
- オフライン評価(事前) — 固定した評価データセットに対し、デプロイ前に品質・エッジケース・堅牢性を測る。CIゲートはここ。
- オンライン評価(本番) — 本番トラフィックをサンプリングして継続的に評価し、ドリフト・品質劣化を検知する。ユーザーのフィードバック(👍/👎)や、judgeによる本番出力の抜き取り採点を組み合わせます。
オフラインは「壊れたものを出さない」、オンラインは「出した後の劣化に気づく」。片方では不十分です。オンライン評価はログ・トレースが土台になるので、OpenTelemetryによる本番可観測性と一体で設計します。LLM出力の実行時検証(構造化出力)は structured output の信頼性設計へ。
導入チェックリスト(本番投入前)
- ゴールデンセット — 典型例・エッジケース・敵対的入力を含む評価データを用意し、失敗を足して育てているか。
- 決定的採点を最優先 — 形式・構造・キーワード・数値は決定的グレーダーで測っているか(安く再現可能)。
- LLM-as-judge — rubricを固定し、判定をスキーマで構造化し、順序入れ替えでバイアスを抑えているか。強モデルで開始したか。
- RAGは分離評価 — faithfulness / relevancy / context precision・recall で検索と生成を切り分けているか。
- CIゲート — 合格率が閾値未満ならデプロイを止めているか。PRで品質のdiffが見えるか。
- オンライン評価 — 本番トラフィックをサンプリングして継続評価し、ドリフトを検知しているか。
- 可観測性 — 入出力・スコア・レイテンシ・コストを記録し、劣化を追えるか。
まとめ
LLMアプリの品質保証は、勘や目視ではなく工学です。「非決定的で自由文な出力を、データセットと自動採点でスコア化し、CIでゲートする」——この規律が、デモと本番を分けます。
- 測れるものは決定的に測る。 残りを LLM-as-judge(rubric固定・バイアス対策・強モデル開始)で。
- RAGは検索と生成を分けて測る。
- 評価はCIに組み込んでこそ品質を守る。 オフライン(事前ゲート)とオンライン(本番監視)を両輪で。
セキュリティ・MCP・評価——この3つが揃って、生成AIは「速く作れて、安全で、壊れない」本番システムになります。作る速さと、守る堅さは、正しく設計すれば両立します。