取り組みの背景 — なぜ LLM 評価が「最重要課題」になったのか
AI を活用したプロダクト開発では、コードを書くだけでは不十分な時代になりましました。LLM(大規模言語モデル)を組み込んだシステムにおいて、「出力が本当に正しいか」「回答の品質が劣化していないか」「コスト効率は最適か」を継続的に検証する仕組みなしに本番運用するのは、テストなしでコードをデプロイするのと同じリスクを抱えることになります。
実際、ある SaaS プロダクトで Gemini 1.5 Pro から Gemini 2.0 Flash にモデルを切り替えた際、回答の平均長が 40% 短縮されコスト削減に成功したものの、ユーザーからの問い合わせ精度に関するクレームが 2 倍に増加したケースがあります。事前に適切な評価フレームワークを持っていれば、このリグレッションを本番公開前に検出できたはずです。
LLM 評価の基礎概念と評価指標の分類
LLM の評価は大きく 3 つのカテゴリに分類できます。
参照ベース評価(Reference-based Evaluation) は、正解となるゴールデンアンサーが存在するケースで使います。たとえば、特定の質問に対して「正しい答えは○○」という教師ラベルがある場合、BLEU スコア・ROUGE スコア・Exact Match などで自動評価できます。しかし LLM の出力は自然言語であることが多く、表現が異なっても正解というケースが多いため、単純な文字列マッチングではなく意味的類似度(Semantic Similarity)を組み合わせるのが現代の標準です。
参照なし評価(Reference-free Evaluation) は、正解ラベルが存在しない・用意が困難なケースで使います。ここで威力を発揮するのが「LLM-as-a-Judge」パターンです。評価対象の AI 出力を、別の高性能 LLM(例:Gemini 1.5 Pro や Claude 3.7 Sonnet)が採点する方式で、人間の評価と相関の高い結果が得られることが研究で示されています。
タスク固有評価(Task-specific Evaluation) は、RAG(検索拡張生成)、コード生成、要約など特定のタスクに特化した指標を使います。RAG であれば「検索した文脈との忠実性(Faithfulness)」「質問への関連性(Answer Relevancy)」「コンテキスト精度(Context Precision)」などが重要な指標になります。
評価指標を選ぶ際の重要な原則は「ビジネス指標との連携」です。技術的な指標が高くてもユーザー満足度が低いケースは珍しくありません。まずプロダクトが解決したい問題を明確にし、そこから逆算して評価指標を設計することが大切です。
Antigravity を使った評価プロジェクトのセットアップ
Antigravity を使うことで、評価フレームワークの初期設定を驚くほど短時間で完了できます。Antigravity のチャット欄で以下のように指示するだけで、必要なファイル構造と設定ファイルの雛形を生成できます。
@Workspace 以下の構成でLLM評価プロジェクトをセットアップしてください:
- promptfoo によるプロンプト評価
- LangSmith によるトレーシング
- Ragas による RAG 評価
- GitHub Actions でのCI自動実行
Node.js + TypeScript 構成で、package.json、基本的な設定ファイル、
サンプルテストケースを含む構成にしてください。
Antigravity が生成するプロジェクト構造は次のようになります。
llm-eval/
├── src/
│ ├── evaluators/
│ │ ├── faithfulness.ts # RAG忠実性評価
│ │ ├── relevancy.ts # 関連性評価
│ │ └── custom-judge.ts # LLM-as-a-Judge
│ ├── datasets/
│ │ ├── qa-golden.jsonl # Q&Aゴールデンセット
│ │ └── rag-testcases.jsonl # RAGテストケース
│ └── pipelines/
│ └── evaluation-runner.ts # 評価実行メイン
├── promptfooconfig.yaml # promptfoo設定
├── langsmith.config.ts # LangSmith設定
├── .github/workflows/
│ └── llm-eval.yml # CI/CD設定
└── package.json
package.json の核心部分を見てみましょう。
{
"scripts": {
"eval:promptfoo": "promptfoo eval --config promptfooconfig.yaml",
"eval:rag": "ts-node src/pipelines/evaluation-runner.ts --type rag",
"eval:full": "npm run eval:promptfoo && npm run eval:rag",
"eval:ci": "npm run eval:full -- --output json > results/eval-$(date +%Y%m%d).json"
},
"dependencies": {
"promptfoo": "^0.72.0",
"langchain": "^0.3.0",
"@langchain/core": "^0.3.0",
"ragas": "^0.2.0"
},
"devDependencies": {
"typescript": "^5.4.0",
"ts-node": "^10.9.0"
}
}promptfoo による自動評価パイプライン構築
promptfoo は、プロンプトのバージョン管理と自動テストに特化したオープンソースツールです。異なるプロンプト・異なるモデル・異なるパラメータを組み合わせて一括評価できる「マトリクス評価」が最大の特長です。
promptfooconfig.yaml の設計
# promptfooconfig.yaml
description: "カスタマーサポート AI の評価スイート"
# 評価対象のプロバイダー(モデル)
providers:
- id: google:gemini-2.0-flash-exp
config:
temperature: 0.1
- id: google:gemini-1.5-pro
config:
temperature: 0.1
- id: openai:gpt-4o-mini
config:
temperature: 0.1
# 評価するプロンプトバリエーション
prompts:
- id: system-v1
raw: |
あなたは{{company_name}}のサポートエージェントです。
ユーザーの質問に簡潔かつ正確に回答してください。
回答は必ず{{language}}で行ってください。
- id: system-v2
raw: |
あなたは{{company_name}}のカスタマーサポート専門家です。
質問を理解し、段階的に分かりやすく回答してください。
- 言語: {{language}}
- トーン: 親しみやすく、プロフェッショナル
- 回答長: 200〜400文字
# テストケース
tests:
- vars:
company_name: "TechCorp"
language: "日本語"
question: "パスワードをリセットする方法を教えてください"
assert:
# 1. LLM-as-a-Judge でヘルプフルネスを評価
- type: llm-rubric
value: "回答はパスワードリセットの具体的な手順を含み、ユーザーが実際に操作できる内容か"
# 2. 有害コンテンツチェック
- type: moderation
value: none
# 3. 日本語で回答しているか
- type: javascript
value: output.match(/[\u3040-\u30ff\u4e00-\u9fff]/) !== null
- vars:
company_name: "TechCorp"
language: "日本語"
question: "返金ポリシーはどうなっていますか?"
assert:
- type: llm-rubric
value: "返金ポリシーに関する情報を含むか、または情報がない場合は適切に案内しているか"
- type: cost
threshold: 0.01 # $0.01以下
# 評価結果の出力設定
outputPath: "./results/promptfoo-latest.json"LLM-as-a-Judge カスタム評価器の実装
Antigravity に「より精度の高いカスタム評価器を TypeScript で書いて」と依頼すると、以下のような実装を提案してくれます。
// src/evaluators/custom-judge.ts
import { GoogleGenerativeAI } from "@google/generative-ai";
interface JudgeResult {
score: number; // 0.0 〜 1.0
reasoning: string;
pass: boolean;
}
export async function llmJudge(
question: string,
answer: string,
criteria: string
): Promise<JudgeResult> {
const genAI = new GoogleGenerativeAI(process.env.GOOGLE_API_KEY!);
const model = genAI.getGenerativeModel({ model: "gemini-2.0-flash-exp" });
const judgePrompt = `
あなたはAI出力の品質評価専門家です。以下の基準で回答を評価してください。
【評価基準】
${criteria}
【ユーザーの質問】
${question}
【評価対象の回答】
${answer}
以下のJSON形式で評価結果を返してください(コードブロックなし):
{
"score": 0.0〜1.0の数値,
"reasoning": "評価理由を100文字以内で",
"pass": trueまたはfalse(0.7以上でtrue)
}
`;
const result = await model.generateContent(judgePrompt);
const text = result.response.text().trim();
try {
return JSON.parse(text) as JudgeResult;
} catch {
// JSONパース失敗時のフォールバック
return {
score: 0,
reasoning: "評価結果のパース失敗",
pass: false,
};
}
}LangSmith によるトレーシングと品質モニタリング
LangSmith は LangChain が提供する LLM アプリケーション向けの観測・デバッグプラットフォームです。本番環境での AI 呼び出しをすべて記録し、レイテンシ・コスト・エラー率を可視化できます。
LangSmith の設定と初期化
// src/config/langsmith.ts
import { Client } from "langsmith";
import { traceable } from "langsmith/traceable";
import { wrapOpenAI } from "langsmith/wrappers";
// 環境変数設定(.envファイル)
// LANGCHAIN_TRACING_V2=true
// LANGCHAIN_API_KEY=lsv2_...
// LANGCHAIN_PROJECT=my-ai-app-prod
export const langsmithClient = new Client();
// AI呼び出し関数をtraceable でラップ
export const tracedAnswer = traceable(
async function generateAnswer(question: string, context: string) {
const genAI = new GoogleGenerativeAI(process.env.GOOGLE_API_KEY!);
const model = genAI.getGenerativeModel({ model: "gemini-2.0-flash-exp" });
const prompt = `
以下のコンテキストを参考に、質問に日本語で回答してください。
コンテキスト: ${context}
質問: ${question}
`;
const result = await model.generateContent(prompt);
return result.response.text();
},
{ name: "generate-answer", metadata: { version: "2.1.0" } }
);評価実行とデータセット管理
LangSmith の最も強力な機能のひとつが「データセット管理」です。本番環境で収集した Q&A ペアを評価データセットとして登録し、モデル更新のたびに自動で再評価できます。
// src/pipelines/langsmith-evaluation.ts
import { Client, Run } from "langsmith";
import { evaluate } from "langsmith/evaluation";
const client = new Client();
// 評価データセットの作成
async function createEvalDataset() {
const dataset = await client.createDataset("customer-support-eval-v2", {
description: "カスタマーサポートAI評価データセット",
});
// テストケースを追加
const examples = [
{
inputs: { question: "注文をキャンセルするにはどうすればいいですか?" },
outputs: { answer: "マイページ > 注文履歴から24時間以内にキャンセル可能です。" },
},
{
inputs: { question: "配送にどのくらいかかりますか?" },
outputs: { answer: "通常3〜5営業日でお届けします。" },
},
// ... さらに多くのケース
];
await client.createExamples({
datasetId: dataset.id,
inputs: examples.map((e) => e.inputs),
outputs: examples.map((e) => e.outputs),
});
return dataset;
}
// 評価実行
async function runEvaluation(datasetName: string) {
const results = await evaluate(
// 評価対象の関数
async (inputs: { question: string }) => {
return { answer: await tracedAnswer(inputs.question, "") };
},
{
data: datasetName,
evaluators: [
// 正確性評価
async (run: Run, example: any) => ({
key: "answer_accuracy",
score: await scoreAccuracy(run.outputs?.answer, example.outputs?.answer),
}),
// 簡潔性評価
async (run: Run) => ({
key: "answer_conciseness",
score: run.outputs?.answer?.length < 200 ? 1.0 : 0.5,
}),
],
experimentPrefix: `eval-${new Date().toISOString().split("T")[0]}`,
}
);
return results;
}
async function scoreAccuracy(answer: string, reference: string): Promise<number> {
// セマンティック類似度でスコア計算(簡略版)
const keywords = reference.toLowerCase().split(/\s+/);
const matchCount = keywords.filter((kw) =>
answer.toLowerCase().includes(kw)
).length;
return matchCount / keywords.length;
}Ragas による RAG システム専用評価
RAG(Retrieval Augmented Generation)システムを構築している場合は、Ragas というフレームワークが非常に強力です。RAG 特有の品質指標を自動計算できます。
Ragas の主要評価指標
Ragas が提供する主要な評価指標は以下のとおりです(GFM テーブルを使わずリスト形式で記載します)。
- Faithfulness(忠実性): 生成された回答がコンテキストの内容のみに基づいているか。ハルシネーション(幻覚)の検出に有効です。スコアが 1.0 に近いほど、コンテキストに忠実な回答です
- Answer Relevancy(回答関連性): 生成された回答がユーザーの質問にどれだけ関連しているか。質問に答えていない冗長な回答は低スコアになります
- Context Precision(コンテキスト精度): 検索で取得したコンテキストのうち、実際に回答生成に役立ったものの割合です
- Context Recall(コンテキスト再現率): 正解を生成するために必要な情報が、検索したコンテキストにどれだけ含まれていたか
Ragas の実装例
// src/evaluators/ragas-evaluation.ts
// ※ RagasはPythonネイティブのため、ここでは評価呼び出しのラッパーを示します
import { spawn } from "child_process";
import * as fs from "fs";
import * as path from "path";
interface RagasInputs {
questions: string[];
answers: string[];
contexts: string[][]; // 各質問に対する取得コンテキストの配列
groundTruths: string[];
}
interface RagasResults {
faithfulness: number;
answer_relevancy: number;
context_precision: number;
context_recall: number;
}
export async function runRagasEvaluation(
inputs: RagasInputs
): Promise<RagasResults> {
// 入力データをJSONファイルに書き出し
const inputFile = path.join("/tmp", "ragas_input.json");
fs.writeFileSync(inputFile, JSON.stringify(inputs));
// Pythonスクリプトを呼び出し
return new Promise((resolve, reject) => {
const proc = spawn("python3", ["scripts/run_ragas.py", inputFile]);
let output = "";
proc.stdout.on("data", (data) => {
output += data.toString();
});
proc.on("close", (code) => {
if (code === 0) {
try {
resolve(JSON.parse(output) as RagasResults);
} catch {
reject(new Error("Ragasの結果パースに失敗しました"));
}
} else {
reject(new Error(`Ragasプロセスがコード${code}で終了しました`));
}
});
});
}対応する Python スクリプトも Antigravity に生成してもらいます。
# scripts/run_ragas.py
import sys
import json
from datasets import Dataset
from ragas import evaluate
from ragas.metrics import (
faithfulness,
answer_relevancy,
context_precision,
context_recall,
)
def main():
input_file = sys.argv[1]
with open(input_file) as f:
inputs = json.load(f)
dataset = Dataset.from_dict({
"question": inputs["questions"],
"answer": inputs["answers"],
"contexts": inputs["contexts"],
"ground_truth": inputs["groundTruths"],
})
result = evaluate(
dataset,
metrics=[
faithfulness,
answer_relevancy,
context_precision,
context_recall,
],
)
output = {
"faithfulness": float(result["faithfulness"]),
"answer_relevancy": float(result["answer_relevancy"]),
"context_precision": float(result["context_precision"]),
"context_recall": float(result["context_recall"]),
}
print(json.dumps(output))
if __name__ == "__main__":
main()GitHub Actions への CI/CD 統合
開発フローに評価を組み込む最も効果的な方法は、プルリクエスト時に自動評価を走らせることです。以下は完全な GitHub Actions ワークフローです。
# .github/workflows/llm-eval.yml
name: LLM Quality Gate
on:
pull_request:
branches: [main]
paths:
- "src/prompts/**" # プロンプト変更時
- "src/models/**" # モデル設定変更時
- ".github/workflows/llm-eval.yml"
schedule:
- cron: "0 2 * * *" # 毎日午前2時にリグレッションテスト
jobs:
evaluate:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
- name: Setup Python (for Ragas)
uses: actions/setup-python@v5
with:
python-version: "3.11"
cache: "pip"
- name: Install dependencies
run: |
npm ci
pip install ragas datasets
- name: Run promptfoo evaluation
env:
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
run: npm run eval:promptfoo -- --output json > results/promptfoo.json
- name: Check quality gates
run: |
node scripts/check-quality-gates.js \
--results results/promptfoo.json \
--min-pass-rate 0.85 \
--max-cost-per-run 0.50
- name: Upload evaluation results
uses: actions/upload-artifact@v4
with:
name: eval-results-${{ github.sha }}
path: results/
retention-days: 30
- name: Comment PR with results
if: github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
const results = require('./results/promptfoo.json');
const passRate = results.stats.successes / results.stats.total;
const body = `## 🤖 LLM 評価結果
- **合格率**: ${(passRate * 100).toFixed(1)}%
- **テスト総数**: ${results.stats.total}
- **合格**: ${results.stats.successes} / **不合格**: ${results.stats.failures}
- **総コスト**: $${results.stats.totalCost?.toFixed(4) ?? "N/A"}
${passRate >= 0.85 ? "✅ 品質ゲートを通過しました" : "❌ 品質ゲートを通過できませんでした。プロンプトまたはモデル設定を見直してください。"}
`;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body,
});クォリティゲートのチェックスクリプトも Antigravity に依頼して作成します。
// scripts/check-quality-gates.js
const fs = require("fs");
const path = require("path");
function parseArgs() {
const args = process.argv.slice(2);
const opts = {};
for (let i = 0; i < args.length; i += 2) {
opts[args[i].replace("--", "")] = args[i + 1];
}
return opts;
}
async function main() {
const opts = parseArgs();
const results = JSON.parse(fs.readFileSync(opts.results, "utf-8"));
const passRate = results.stats.successes / results.stats.total;
const totalCost = results.stats.totalCost ?? 0;
const minPassRate = parseFloat(opts["min-pass-rate"] ?? "0.85");
const maxCost = parseFloat(opts["max-cost-per-run"] ?? "1.00");
console.log(`合格率: ${(passRate * 100).toFixed(1)}% (基準: ${minPassRate * 100}%)`);
console.log(`総コスト: $${totalCost.toFixed(4)} (上限: $${maxCost})`);
const failures = [];
if (passRate < minPassRate) {
failures.push(`合格率が基準(${minPassRate * 100}%)を下回っています: ${(passRate * 100).toFixed(1)}%`);
}
if (totalCost > maxCost) {
failures.push(`コストが上限($${maxCost})を超えています: $${totalCost.toFixed(4)}`);
}
if (failures.length > 0) {
console.error("❌ 品質ゲート不合格:");
failures.forEach((f) => console.error(` - ${f}`));
process.exit(1);
}
console.log("✅ 全品質ゲートを通過しました");
}
main().catch((err) => {
console.error(err);
process.exit(1);
});A/B テストによるモデル選定の実践
プロダクションでモデルを切り替える際は、いきなり全切り替えをせず、段階的な A/B テストを行うことが鉄則です。
評価マトリクスの設計
A/B テストで比較すべき指標をリスト形式で整理します。
- 品質スコア: LLM-as-a-Judge によるヘルプフルネス(0.0〜1.0)
- レイテンシ: P50・P95・P99 の応答時間(ミリ秒)
- コスト: 1,000 リクエストあたりのコスト(USD)
- エラー率: API エラー・タイムアウトの発生率(%)
- コンテキスト使用率: 実際に使用されたトークン数 / 最大コンテキスト長
// src/pipelines/ab-test-runner.ts
interface ModelConfig {
name: string;
provider: string;
model: string;
temperature: number;
maxTokens: number;
}
interface ABTestResult {
model: ModelConfig;
metrics: {
qualityScore: number;
avgLatencyMs: number;
p95LatencyMs: number;
costPer1000: number;
errorRate: number;
};
sampleSize: number;
}
export async function runABTest(
models: ModelConfig[],
testCases: Array<{ question: string; reference?: string }>,
runsPerModel = 50
): Promise<ABTestResult[]> {
const results: ABTestResult[] = [];
for (const model of models) {
const latencies: number[] = [];
const scores: number[] = [];
let errors = 0;
let totalCostUSD = 0;
for (let i = 0; i < runsPerModel; i++) {
const tc = testCases[i % testCases.length];
const start = Date.now();
try {
const answer = await callModel(model, tc.question);
const latency = Date.now() - start;
latencies.push(latency);
const score = await llmJudge(
tc.question,
answer,
"回答はユーザーの問いに直接答え、正確で簡潔か"
);
scores.push(score.score);
// コスト計算(概算)
const inputTokens = tc.question.length / 4;
const outputTokens = answer.length / 4;
totalCostUSD += estimateCost(model.model, inputTokens, outputTokens);
} catch {
errors++;
}
}
latencies.sort((a, b) => a - b);
results.push({
model,
metrics: {
qualityScore: average(scores),
avgLatencyMs: average(latencies),
p95LatencyMs: latencies[Math.floor(latencies.length * 0.95)],
costPer1000: (totalCostUSD / runsPerModel) * 1000,
errorRate: errors / runsPerModel,
},
sampleSize: runsPerModel,
});
}
return results;
}
function average(arr: number[]): number {
return arr.reduce((a, b) => a + b, 0) / arr.length;
}
function estimateCost(modelId: string, inputTokens: number, outputTokens: number): number {
// モデルごとの単価(2026年4月現在の概算値)
const pricing: Record<string, { input: number; output: number }> = {
"gemini-2.0-flash-exp": { input: 0.0001, output: 0.0004 },
"gemini-1.5-pro": { input: 0.00125, output: 0.005 },
"gpt-4o-mini": { input: 0.00015, output: 0.0006 },
};
const p = pricing[modelId] ?? { input: 0.001, output: 0.002 };
return (inputTokens * p.input + outputTokens * p.output) / 1000;
}よくある落とし穴と対処法
LLM 評価フレームワークを構築・運用する中で、多くのチームが同じ失敗を繰り返します。Antigravity と対話しながら設計を進める際にも念頭に置きたい注意点を共有します。
評価コストの無視: LLM-as-a-Judge で高性能モデルを使いすぎると、評価コストが本番推論コストを上回ることがあります。評価用モデルとして Gemini 2.0 Flash(低コスト)を使い、重要なテストケースのみ Gemini 1.5 Pro で再評価する「2段階評価」が有効です。
ゴールデンセットの腐敗: テストデータセットは一度作ったら終わりではありません。プロダクトが進化するにつれ、古いゴールデンアンサーが不適切になります。月次でデータセットをレビューし、本番ログから失敗ケースを継続的に追加する習慣が重要です。
評価の過適合(Gaming the Metric): モデルや評価器を同じプロバイダーのものにすると、評価者が評価対象を過大評価するバイアスが生じます(Self-Serving Bias)。評価用モデルは推論用モデルとは別のプロバイダーから選ぶことを強く推奨します。
実環境との乖離: テストデータが実際のユーザー入力と大きく異なると、良いテスト結果が本番での品質を保証しません。本番ログをサンプリングして定期的にテストセットに加えることが不可欠です。
まとめ
評価フレームワークは「導入してから鍛える」ものです。最初から完璧なシステムを目指さず、promptfoo の設定ファイル 1 枚から始め、本番運用しながらデータセットを充実させていくことが成功の鍵です。Antigravity はコード生成だけでなく、評価戦略の設計や設定ファイルのデバッグにも強力な支援を発揮します。積極的に活用してください。
評価の自動化が定着すれば、LLM のアップデート・プロンプト改善・モデル切り替えすべてが「定量的な根拠のある意思決定」になります。AI 開発の速度と品質を両立するために、本記事の内容をぜひプロジェクトに取り入れてみてください。
LLM 評価と関連して、Sentry 連携による AI 品質自動化やOpenTelemetry を使った可観測性パイプラインも合わせて参考にしてください。また、RAG システムに特化した詳細な実装についてはRAG パイプラインと Vector Search 完全ガイドをご覧ください。
LLM 評価フレームワークをさらに深く