# LLMアプリの評価・テスト実践ガイド 2026｜LLM-as-judge・回帰テスト・CIゲート

> 生成AI/LLMアプリを本番で壊さないための評価・テスト実践ガイド。非決定的な出力をどう測るか、評価データセット（ゴールデンセット）の作り方、決定的採点とLLM-as-judgeの使い分け、RAGの評価指標（RAGAS）、そして回帰をCIでゲートする方法まで、一次情報準拠のTypeScript実コードで解説します。

- 公開日: 2026-07-23
- 著者: 友田 陽大
- タグ: 生成AI, LLM, 評価, テスト, LLM-as-judge, RAG, CI/CD, AI駆動開発, 品質保証, アーキテクチャ設計
- URL: https://tomodahinata.com/blog/llm-application-evaluation-testing-llm-as-judge-ci-guide
- カテゴリ: 本番LLMアプリ実装（信頼できるAI）
- 総合ガイド: https://tomodahinata.com/blog/production-llm-application-engineering-guide

## 要点

- LLMアプリの品質は目視では守れない。評価を『データセット＋自動採点＋CIゲート』の工学にするのが本質。合否ではなくスコアの分布で見る。
- 採点は3方式を使い分ける：決定的（exact match・JSON schema・数値許容誤差）で測れるものは決定的に、自由文の品質はLLM-as-judge、最後の砦が人間。
- LLM-as-judgeは強モデルなら人間同士と同等（80%超一致）だが、位置・冗長性・自己優遇のバイアスがある。rubricを明示し、順序を入れ替え、人間と一致を確認してから軽量化する。
- RAGは検索と生成を分けて測る：faithfulness・answer relevancy・context precision/recall（RAGAS）。
- 評価はCIに組み込んでこそ効く。固定データセットの合格率が閾値未満ならデプロイを止める（オフライン評価）。本番はサンプリングで継続評価しドリフトを検知する（オンライン評価）。

---

「デモは完璧に動いたのに、本番でユーザーの入力に晒したら壊れた」——LLMアプリで最も多い失敗です。原因は明快で、**LLMの品質は“目視”では守れない**からです。プロンプトを1行変えただけで、直したはずのケースが直り、直っていたケースが壊れる。決定論的なコードなら単体テストが守ってくれる領域を、LLMでは**評価（Evaluation）という別の工学**で守る必要があります。

結論から言えば、本番のLLMアプリの品質は次の3本柱に集約されます。

1. **評価データセット（ゴールデンセット）** — 代表例・エッジケース・敵対的入力を集めた“試験問題”。
2. **自動採点** — 決定的チェック＋LLM-as-judgeで、出力をスコア化する。
3. **CIゲート** — デプロイを評価スコアで止める。回帰を人間の目ではなくパイプラインで検知する。

この記事は、生成AIパイプラインを本番運用してきた立場から、**一次情報（OpenAI / Anthropic のeval公式ガイド、LLM-as-judgeの原典論文、RAGAS）に準拠**して、TypeScriptの実コードで“本番で回る評価”を設計します。これは、[LLMアプリのセキュリティ](/blog/llm-application-security-owasp-top-10-prompt-injection-guide)・[MCP本番サーバー](/blog/mcp-model-context-protocol-production-server-guide)と並ぶ、「**信頼できる本番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スキーマで出力の**構造**を、単純なチェックで**内容**を測ります。

```ts
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の公式推奨）。

```ts
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の本番アンチパターン](/blog/production-rag-pitfalls-accuracy-improvement-guide)、検索設計は [pgvectorによる本番RAG](/blog/pgvector-postgres-production-rag-hybrid-search) を参照してください。

## 回帰テストをCIに組み込む

評価は、**CIに組み込んで初めて品質を“守る”**ものになります。手元で1回良くても意味がない。**デプロイを評価スコアでゲート**します。固定データセットに対する合格率が閾値を割ったら、ビルドを失敗させます。

```ts
// 集計と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](/blog/github-actions-oidc-keyless-cicd-aws-gcp-guide) と同じ発想で組めます。

> 補足：これは [AI駆動開発の品質ゲート](/blog/ai-driven-development-quality-gates-ci-type-safety-test-security)（AIが“書いたコード”の品質を守る話）とは別レイヤーです。本記事は、AIアプリが“生成する出力”そのものの品質を守る話です。両方が揃って初めて本番品質になります。

## オフライン評価とオンライン評価：両輪で回す

- **オフライン評価（事前）** — 固定した評価データセットに対し、**デプロイ前**に品質・エッジケース・堅牢性を測る。CIゲートはここ。
- **オンライン評価（本番）** — **本番トラフィックをサンプリング**して継続的に評価し、**ドリフト・品質劣化を検知**する。ユーザーのフィードバック（👍/👎）や、judgeによる本番出力の抜き取り採点を組み合わせます。

オフラインは「壊れたものを出さない」、オンラインは「出した後の劣化に気づく」。片方では不十分です。オンライン評価はログ・トレースが土台になるので、[OpenTelemetryによる本番可観測性](/blog/opentelemetry-observability-production-tracing-metrics-logs)と一体で設計します。LLM出力の実行時検証（構造化出力）は [structured output の信頼性設計](/blog/structured-output-reliability-constrained-decoding-semantic-validation)へ。

## 導入チェックリスト（本番投入前）

- [ ] **ゴールデンセット** — 典型例・エッジケース・敵対的入力を含む評価データを用意し、失敗を足して育てているか。
- [ ] **決定的採点を最優先** — 形式・構造・キーワード・数値は決定的グレーダーで測っているか（安く再現可能）。
- [ ] **LLM-as-judge** — rubricを固定し、判定をスキーマで構造化し、順序入れ替えでバイアスを抑えているか。強モデルで開始したか。
- [ ] **RAGは分離評価** — faithfulness / relevancy / context precision・recall で検索と生成を切り分けているか。
- [ ] **CIゲート** — 合格率が閾値未満ならデプロイを止めているか。PRで品質のdiffが見えるか。
- [ ] **オンライン評価** — 本番トラフィックをサンプリングして継続評価し、ドリフトを検知しているか。
- [ ] **可観測性** — 入出力・スコア・レイテンシ・コストを記録し、劣化を追えるか。

## まとめ

LLMアプリの品質保証は、勘や目視ではなく**工学**です。**「非決定的で自由文な出力を、データセットと自動採点でスコア化し、CIでゲートする」**——この規律が、デモと本番を分けます。

- **測れるものは決定的に測る。** 残りを **LLM-as-judge**（rubric固定・バイアス対策・強モデル開始）で。
- **RAGは検索と生成を分けて測る。**
- **評価はCIに組み込んでこそ品質を守る。** オフライン（事前ゲート）とオンライン（本番監視）を両輪で。

セキュリティ・MCP・評価——この3つが揃って、生成AIは「速く作れて、安全で、壊れない」本番システムになります。作る速さと、守る堅さは、正しく設計すれば両立します。
