01TL;DR
- エージェントの評価指標は「タスク達成」「応答品質」「効率」「コスト」の4カテゴリに整理できます。
- 主観評価は「ルーブリック」で採点基準を明文化することで、再現性のある定量スコアに変換できます。
- 評価の自動化は「正解比較型」と「LLMジャッジ型」の2系統から始め、段階的に拡張します。
- 指標の選択はユースケースに依存するため、「全部測る」より「捨てる指標を意識して選ぶ」方が実践的です。
- サンプルコードはTypeScript(Node.js v20.x)で記述しています。
02はじめに
この記事の対象読者
エージェント開発に携わっているものの、「デモでは動くが、本番品質をどう担保するか」で悩んでいる方を想定しています。 具体的には、次のような状況にある方に向けて書きます。
- プロトタイプは作ったが、品質の良し悪しを説明できる言語化ができていない
- 評価の仕組みを作りたいが、何をどう測ればよいか迷っている
- 主観的なフィードバックを数値に変換する方法を探している
前提知識
LLMを使ったエージェントの基本的な動作(プロンプト・ツール呼び出し・マルチターン会話)を理解していることを前提とします。 特定のフレームワークの知識は必要ありません。
この記事で得られること
- 評価指標の4カテゴリとその具体例
- 指標の選び方の考え方
- ルーブリックによる主観評価の定量化手順
- 評価を自動化するための最初のコード
- 評価カテゴリ全体のロードマップ(関連記事への導線)
03エージェント評価が難しい理由
エージェントの評価が難しい理由は、「正解が一意に定まらない」ことにあります。
通常のソフトウェアテストは、「この入力に対してこの出力が返れば合格」という形で書けます。 ところがエージェントの応答には、表現の揺れ・迂回経路・判断の深さなど、正解への経路が複数存在します。 「別の言い回しだが内容は正しい」応答を誤りと判定するのは意味がなく、かといって「なんとなく良さそう」という直感だけでは改善につながりません。
この問題に対処するために、評価を「何を測るか」「どう採点するか」「どう自動化するか」の3段階で考えます。
04評価指標の4カテゴリ
エージェントの品質を測る指標は、大きく4つのカテゴリに整理できます。
┌─────────────────────────────────────────────────────┐
│ 評価指標の全体像 │
├──────────────────┬──────────────────────────────────┤
│ カテゴリ │ 主な指標 │
├──────────────────┼──────────────────────────────────┤
│ タスク達成 │ 完了率、正確性、ゴール達成率 │
├──────────────────┼──────────────────────────────────┤
│ 応答品質 │ 関連性、誠実性、有害性スコア │
├──────────────────┼──────────────────────────────────┤
│ 効率 │ ターン数、レイテンシ、ツール呼び出し数 │
├──────────────────┼──────────────────────────────────┤
│ コスト │ トークン使用量、API呼び出し回数 │
└──────────────────┴──────────────────────────────────┘
それぞれのカテゴリを順に見ていきます。
タスク達成:「できたか」を測る
タスク達成カテゴリは、エージェントが期待された仕事をこなせたかを測ります。
完了率(Task Completion Rate)
ユーザーが依頼したタスクを、エージェントが最終的に完了できた割合です。 「完了」の定義はユースケースごとに明確にする必要があります。 例えば予約エージェントであれば「予約確定メッセージが返った」を完了とし、検索エージェントであれば「結果リストが返った」を完了と定義するといった具合です。
正確性(Accuracy)
応答の内容が事実として正しいか、あるいは期待する情報を含んでいるかを測ります。 正解データがある場合は完全一致・部分一致・ベクトル類似度などで自動計測できます。 正解データがない場合はルーブリック評価(後述)や人手評価を組み合わせます。
ゴール達成率(Goal Achievement Rate)
タスクの完了とは別に、ユーザーの「本来の目的」が達成されたかを測る指標です。 「手順を教えてほしい」という依頼に対して、手順を返すだけでなく、ユーザーが実際に問題を解決できたかを追跡する場合に使います。 会話後のフォローアップアンケートやセッション続行率から間接的に推定することが多いです。
応答品質:「どれだけ良い回答か」を測る
タスクが完了したとしても、応答の品質が低ければユーザー体験は損なわれます。
関連性(Relevance)
ユーザーの質問や要求に対して、応答が的外れでないかを測ります。 的外れな応答は、たとえ内容が正確でもユーザーを混乱させます。
誠実性・根拠の明示(Faithfulness / Groundedness)
RAGを使うエージェントでは特に重要な指標です。 応答が参照ドキュメントの内容に基づいているか、根拠なく情報を生成していないかを測ります。
安全性(Safety / Harmlessness)
有害なコンテンツ、プライバシー侵害、誤った医療・法律情報などを返していないかを測ります。 ビジネス用途ではブランドリスクとも直結するため、最低基準として設定することを筆者たちは推奨しています。
効率:「どれだけ無駄がないか」を測る
効率カテゴリは、同じ結果を得るためのリソース消費を測ります。
対話ターン数(Turn Count)
タスクを完了するまでに必要な往復回数です。 ターン数が多い場合、プロンプト設計の問題(情報不足・曖昧な質問)や、エラーリカバリの非効率さが隠れていることがあります。
レイテンシ(Latency)
ユーザーが入力を送信してから応答が返るまでの時間です。 p50(中央値)だけでなくp95・p99も測ることで、「ほとんどは速いが一部が極端に遅い」という問題を発見できます。
ツール呼び出し数(Tool Call Count)
マルチツールエージェントでは、1タスクあたりのツール呼び出し回数を追跡します。 不必要に同じツールを何度も呼ぶパターンは、プロンプト設計の問題や状態管理の漏れを示すことがあります。
コスト:「どれだけ費用がかかるか」を測る
トークン使用量(Token Usage)
入力トークン・出力トークンの消費量です。 コスト削減の目的だけでなく、コンテキストウィンドウの逼迫(入力トークンの増加)を早期に検知する手段としても機能します。
API呼び出し回数
LLM APIの呼び出し回数は、コストと直結します。 リトライやフォールバック処理の多発は、呼び出し回数の急増として現れます。
05指標の選び方
4カテゴリ全部を一度に測ろうとすると、計測基盤の構築だけで膨大なコストがかかります。 筆者たちの経験では、最初から「全部測る」より「今のフェーズで最も重要な1〜2指標を選ぶ」方が実践的です。
フェーズ別の優先指標
| フェーズ | 優先すべき指標 | 理由 |
|---|---|---|
| プロトタイプ | 完了率・正確性 | まず「動くか」の確認が必要 |
| クローズドβ | ターン数・有害性 | ユーザー体験の粗を早期発見 |
| 本番稼働後 | コスト・レイテンシ | スケール時のリソース設計に直結 |
| 改善サイクル | 全カテゴリから1〜2本 | 改善仮説ごとに指標を切り替える |
「捨てる」指標を意識する
すべての指標が常に重要というわけではありません。 例えば社内向けの業務エージェントであれば、ターン数が多少多くても許容されることがあります。 一方で医療・法律情報を扱うエージェントでは、安全性スコアは非交渉の最低基準です。
「なぜこの指標を測るのか」「この指標が悪化したときに何をするのか」を事前に言語化しておくことで、評価が改善サイクルに直結します。
06主観評価をルーブリックで定量化する
ルーブリックとは
ルーブリック(Rubric)とは、評価基準を複数のレベルに分けて明文化した採点表です。 教育分野で広く使われてきた手法ですが、LLM評価においても「判断基準を明文化することで評価の一貫性を高める」目的で有効です。
主観評価の問題は「人によって判断が変わる」ことにあります。 ルーブリックを使うと、評価者(人間またはLLM)が同じ基準で採点できるため、スコアの再現性が上がります。
ルーブリックの設計手順
ルーブリックの設計は次の4ステップで進めます。
ステップ1:評価軸を決める
まず「何の品質を測りたいか」を1軸に絞ります。 1つのルーブリックで複数の軸を測ると基準が曖昧になるため、軸ごとにルーブリックを分けます。
よく使われる評価軸の例は次の通りです。
- 関連性(ユーザーの質問に答えているか)
- 完全性(必要な情報が漏れなく含まれているか)
- 正確性(事実として誤りがないか)
- 簡潔さ(不要な情報が含まれていないか)
- 誠実さ(根拠なく断言していないか)
ステップ2:スコアレベルを定義する
3〜5段階のスコアレベルを設定し、各レベルの「状態」を具体的に記述します。 曖昧な表現(「良い」「普通」)を避け、観察可能な行動・状態で書きます。
関連性の4段階ルーブリックの例は次の通りです。
スコア4(完全に関連している):
ユーザーの質問のすべての要素に答えており、余分な情報を含まない。
スコア3(ほぼ関連している):
ユーザーの質問の主要部分に答えているが、1〜2点の補足情報が欠けている。
スコア2(部分的に関連している):
ユーザーの質問に関連する情報を含むが、中心的な問いには答えていない。
スコア1(関連していない):
ユーザーの質問とほとんど無関係な内容を返している。
ステップ3:境界ケースの例を追加する
スコア3と4の違いなど、境界で迷いやすいケースのサンプル応答を添えると、採点の一貫性がさらに上がります。
ステップ4:評価者間信頼性を確認する
複数の評価者(または複数回のLLMジャッジ)に同じサンプルを採点させ、スコアのばらつきを確認します。 Cohen's κ(カッパ係数)が0.6以上であれば、まずまず一致していると判断できます(目安として)。 ばらつきが大きい場合はレベルの定義を見直します。
07TypeScriptで評価基盤を作る
評価データの型定義
まずは評価に必要なデータ構造を定義します。
// evaluation/types.ts
export interface EvaluationCase {
id: string;
input: string;
expectedOutput?: string; // 正解データがある場合
actualOutput: string;
metadata?: Record<string, unknown>;
}
export interface RubricLevel {
score: number;
label: string;
description: string;
}
export interface Rubric {
dimension: string;
levels: RubricLevel[];
}
export interface EvaluationResult {
caseId: string;
dimension: string;
score: number;
rationale: string;
evaluatedAt: string;
}
完了率の計測
完了率は、最もシンプルに自動計測できる指標です。 「完了」の判定ロジックをユースケースごとに差し替えられるよう、関数として分離します。
// evaluation/metrics/completion-rate.ts
import type { EvaluationCase, EvaluationResult } from "../types.js";
type CompletionJudge = (actualOutput: string) => boolean;
export function measureCompletionRate(
cases: EvaluationCase[],
judge: CompletionJudge
): { rate: number; results: Array<{ id: string; completed: boolean }> } {
const results = cases.map((c) => ({
id: c.id,
completed: judge(c.actualOutput),
}));
const completedCount = results.filter((r) => r.completed).length;
const rate = cases.length > 0 ? completedCount / cases.length : 0;
return { rate, results };
}
// 使用例
import { measureCompletionRate } from "./evaluation/metrics/completion-rate.js";
const cases = [
{
id: "case-001",
input: "東京の明日の天気を教えてください",
actualOutput: "明日の東京は晴れのち曇り、最高気温は28度の見込みです。",
},
{
id: "case-002",
input: "最新のニュースを3件まとめてください",
actualOutput: "申し訳ありませんが、現在の情報にアクセスできません。",
},
];
const { rate, results } = measureCompletionRate(
cases,
// 「申し訳ありません」「できません」を含む場合は未完了と判定する例
(output) => !output.includes("申し訳ありません") && !output.includes("できません")
);
console.log(`完了率: ${(rate * 100).toFixed(1)}%`);
// => 完了率: 50.0%
console.log(results);
// => [
// { id: 'case-001', completed: true },
// { id: 'case-002', completed: false }
// ]
正確性の計測(文字列一致・類似度)
正解データがある場合の正確性計測です。 完全一致と正規化一致(大文字小文字・空白を除いた比較)を組み合わせます。
// evaluation/metrics/accuracy.ts
export interface AccuracyResult {
caseId: string;
exactMatch: boolean;
normalizedMatch: boolean;
}
function normalize(text: string): string {
return text.toLowerCase().replace(/\s+/g, " ").trim();
}
export function measureAccuracy(
cases: Array<{ id: string; expectedOutput: string; actualOutput: string }>
): { exactMatchRate: number; normalizedMatchRate: number; results: AccuracyResult[] } {
const results: AccuracyResult[] = cases.map((c) => ({
caseId: c.id,
exactMatch: c.expectedOutput === c.actualOutput,
normalizedMatch: normalize(c.expectedOutput) === normalize(c.actualOutput),
}));
const total = cases.length;
return {
exactMatchRate: total > 0
? results.filter((r) => r.exactMatch).length / total
: 0,
normalizedMatchRate: total > 0
? results.filter((r) => r.normalizedMatch).length / total
: 0,
results,
};
}
// 使用例
import { measureAccuracy } from "./evaluation/metrics/accuracy.js";
const cases = [
{
id: "case-001",
expectedOutput: "東京",
actualOutput: "東京",
},
{
id: "case-002",
expectedOutput: "大阪",
actualOutput: "大阪府",
},
{
id: "case-003",
expectedOutput: "Kyoto",
actualOutput: "kyoto",
},
];
const { exactMatchRate, normalizedMatchRate } = measureAccuracy(cases);
console.log(`完全一致率: ${(exactMatchRate * 100).toFixed(1)}%`);
// => 完全一致率: 33.3%
console.log(`正規化一致率: ${(normalizedMatchRate * 100).toFixed(1)}%`);
// => 正規化一致率: 66.7%
対話ターン数の計測
ターン数はセッションログから計測します。
// evaluation/metrics/turn-count.ts
export interface ConversationTurn {
role: "user" | "assistant" | "tool";
content: string;
timestamp: string;
}
export interface TurnCountResult {
sessionId: string;
userTurns: number;
assistantTurns: number;
toolCalls: number;
totalTurns: number;
}
export function measureTurnCount(
sessionId: string,
turns: ConversationTurn[]
): TurnCountResult {
return {
sessionId,
userTurns: turns.filter((t) => t.role === "user").length,
assistantTurns: turns.filter((t) => t.role === "assistant").length,
toolCalls: turns.filter((t) => t.role === "tool").length,
totalTurns: turns.length,
};
}
export function aggregateTurnCounts(
results: TurnCountResult[]
): {
avgUserTurns: number;
avgAssistantTurns: number;
avgToolCalls: number;
p50TotalTurns: number;
p95TotalTurns: number;
} {
if (results.length === 0) {
return { avgUserTurns: 0, avgAssistantTurns: 0, avgToolCalls: 0, p50TotalTurns: 0, p95TotalTurns: 0 };
}
const sorted = [...results].sort((a, b) => a.totalTurns - b.totalTurns);
const p50Index = Math.floor(sorted.length * 0.5);
const p95Index = Math.floor(sorted.length * 0.95);
const avg = (arr: number[]) => arr.reduce((s, v) => s + v, 0) / arr.length;
return {
avgUserTurns: avg(results.map((r) => r.userTurns)),
avgAssistantTurns: avg(results.map((r) => r.assistantTurns)),
avgToolCalls: avg(results.map((r) => r.toolCalls)),
p50TotalTurns: sorted[p50Index].totalTurns,
p95TotalTurns: sorted[p95Index].totalTurns,
};
}
// 使用例
import { measureTurnCount, aggregateTurnCounts } from "./evaluation/metrics/turn-count.js";
const sessions = [
{
id: "session-001",
turns: [
{ role: "user" as const, content: "...", timestamp: "2026-06-11T09:00:00Z" },
{ role: "assistant" as const, content: "...", timestamp: "2026-06-11T09:00:01Z" },
{ role: "tool" as const, content: "...", timestamp: "2026-06-11T09:00:02Z" },
{ role: "assistant" as const, content: "...", timestamp: "2026-06-11T09:00:03Z" },
],
},
{
id: "session-002",
turns: [
{ role: "user" as const, content: "...", timestamp: "2026-06-11T09:01:00Z" },
{ role: "assistant" as const, content: "...", timestamp: "2026-06-11T09:01:01Z" },
],
},
];
const results = sessions.map((s) => measureTurnCount(s.id, s.turns));
const agg = aggregateTurnCounts(results);
console.log(agg);
// => {
// avgUserTurns: 1,
// avgAssistantTurns: 1.5,
// avgToolCalls: 0.5,
// p50TotalTurns: 4,
// p95TotalTurns: 4
// }
ルーブリック評価をコードで実装する
ルーブリックを使った評価を、LLMに採点させる形で実装します。 ここでは、評価プロンプトの構築と結果のパースに焦点を当てます。
// evaluation/rubric-evaluator.ts
import type { Rubric, EvaluationCase, EvaluationResult } from "./types.js";
export function buildRubricPrompt(
rubric: Rubric,
evalCase: EvaluationCase
): string {
const levelsText = rubric.levels
.map((l) => `スコア${l.score}(${l.label}):${l.description}`)
.join("\n");
return `
あなたは公平な評価者です。以下の採点基準に従って、エージェントの応答を評価してください。
[評価軸]
${rubric.dimension}
[採点基準]
${levelsText}
[ユーザーの入力]
${evalCase.input}
[エージェントの応答]
${evalCase.actualOutput}
[指示]
上記の採点基準に従い、応答に最も近いスコアを1つ選択してください。
必ず以下のJSON形式のみで出力してください。説明文や余分なテキストは含めないでください。
{"score": <数値>, "rationale": "<採点理由を1〜2文で>"}
`.trim();
}
export function parseRubricResponse(
rawResponse: string,
caseId: string,
dimension: string
): EvaluationResult | null {
try {
// JSONブロックを抽出する(前後に余分なテキストがある場合に対応)
const jsonMatch = rawResponse.match(/\{[^}]+\}/);
if (!jsonMatch) return null;
const parsed = JSON.parse(jsonMatch[0]) as { score: unknown; rationale: unknown };
if (typeof parsed.score !== "number" || typeof parsed.rationale !== "string") {
return null;
}
return {
caseId,
dimension,
score: parsed.score,
rationale: parsed.rationale,
evaluatedAt: new Date().toISOString(),
};
} catch {
return null;
}
}
// 使用例(LLM呼び出し部分は疑似コードで示します)
import { buildRubricPrompt, parseRubricResponse } from "./evaluation/rubric-evaluator.js";
import type { Rubric, EvaluationCase } from "./evaluation/types.js";
const relevanceRubric: Rubric = {
dimension: "関連性",
levels: [
{
score: 4,
label: "完全に関連している",
description: "ユーザーの質問のすべての要素に答えており、余分な情報を含まない。",
},
{
score: 3,
label: "ほぼ関連している",
description: "ユーザーの質問の主要部分に答えているが、1〜2点の補足情報が欠けている。",
},
{
score: 2,
label: "部分的に関連している",
description: "ユーザーの質問に関連する情報を含むが、中心的な問いには答えていない。",
},
{
score: 1,
label: "関連していない",
description: "ユーザーの質問とほとんど無関係な内容を返している。",
},
],
};
const evalCase: EvaluationCase = {
id: "case-001",
input: "TypeScriptでオブジェクトの深いコピーを作る方法を教えてください",
actualOutput:
"structuredClone()関数を使うと、ネストされたオブジェクトも含めて完全なコピーが作れます。Node.js v17以降で使用可能です。",
};
const prompt = buildRubricPrompt(relevanceRubric, evalCase);
// ここでLLMを呼び出します(実際の実装ではSDKを使います)
// const rawResponse = await callLLM(prompt);
// 以下は返り値の例です(実測値ではありません)
const rawResponse = `{"score": 4, "rationale": "質問に正確に答えており、Node.jsのバージョン情報も含まれていて適切です。"}`;
const result = parseRubricResponse(rawResponse, evalCase.id, relevanceRubric.dimension);
console.log(result);
// => {
// caseId: 'case-001',
// dimension: '関連性',
// score: 4,
// rationale: '質問に正確に答えており、Node.jsのバージョン情報も含まれていて適切です。',
// evaluatedAt: '2026-06-11T...'
// }
08評価結果の集計と可視化
個別のスコアを集めたら、次は集計です。 以下のコードは、複数の評価ケースと複数の評価軸をまとめてサマリーに変換します。
// evaluation/aggregator.ts
import type { EvaluationResult } from "./types.js";
export interface DimensionSummary {
dimension: string;
sampleCount: number;
avgScore: number;
minScore: number;
maxScore: number;
distribution: Record<number, number>;
}
export function aggregateByDimension(
results: EvaluationResult[]
): DimensionSummary[] {
const grouped = new Map<string, EvaluationResult[]>();
for (const result of results) {
const existing = grouped.get(result.dimension) ?? [];
grouped.set(result.dimension, [...existing, result]);
}
return Array.from(grouped.entries()).map(([dimension, items]) => {
const scores = items.map((i) => i.score);
const distribution: Record<number, number> = {};
for (const score of scores) {
distribution[score] = (distribution[score] ?? 0) + 1;
}
return {
dimension,
sampleCount: items.length,
avgScore: scores.reduce((s, v) => s + v, 0) / scores.length,
minScore: Math.min(...scores),
maxScore: Math.max(...scores),
distribution,
};
});
}
export function printSummary(summaries: DimensionSummary[]): void {
for (const s of summaries) {
console.log(`\n[${s.dimension}]`);
console.log(` サンプル数: ${s.sampleCount}`);
console.log(` 平均スコア: ${s.avgScore.toFixed(2)}`);
console.log(` 最小/最大: ${s.minScore} / ${s.maxScore}`);
console.log(` 分布: ${JSON.stringify(s.distribution)}`);
}
}
// 実行例
import { aggregateByDimension, printSummary } from "./evaluation/aggregator.js";
import type { EvaluationResult } from "./evaluation/types.js";
const results: EvaluationResult[] = [
{ caseId: "c-001", dimension: "関連性", score: 4, rationale: "...", evaluatedAt: "2026-06-11T09:00:00Z" },
{ caseId: "c-002", dimension: "関連性", score: 3, rationale: "...", evaluatedAt: "2026-06-11T09:00:00Z" },
{ caseId: "c-003", dimension: "関連性", score: 4, rationale: "...", evaluatedAt: "2026-06-11T09:00:00Z" },
{ caseId: "c-001", dimension: "完全性", score: 3, rationale: "...", evaluatedAt: "2026-06-11T09:00:00Z" },
{ caseId: "c-002", dimension: "完全性", score: 2, rationale: "...", evaluatedAt: "2026-06-11T09:00:00Z" },
{ caseId: "c-003", dimension: "完全性", score: 4, rationale: "...", evaluatedAt: "2026-06-11T09:00:00Z" },
];
const summaries = aggregateByDimension(results);
printSummary(summaries);
実行結果(出力例):
[関連性]
サンプル数: 3
平均スコア: 3.67
最小/最大: 3 / 4
分布: {"3":1,"4":2}
[完全性]
サンプル数: 3
平均スコア: 3.00
最小/最大: 2 / 4
分布: {"2":1,"3":1,"4":1}
09評価自動化への足がかり
手動評価からテストスクリプトへ、段階的に自動化を進める考え方を整理します。
2系統の自動評価
自動評価は大きく2系統に分けられます。
正解比較型(Deterministic Evaluation)
正解データと照合する形の評価です。 完全一致・正規化一致・正規表現マッチ・JSON構造の比較などがこれに当たります。 実行が速く、再現性が高いため、まず最初に整備する価値があります。
flowchart LR
A["入力"] --> B["エージェント応答"] --> C["正解データと比較"] --> D["Pass / Fail"]
LLMジャッジ型(Model-based Evaluation)
評価者として別のLLMを使う形です。 先述のルーブリック評価プロンプトを自動実行します。 正解データが用意しにくい「文体の自然さ」「回答の誠実さ」などの評価に向いています。 ただし、LLMジャッジ自体にコストがかかること、ジャッジの安定性を別途検証する必要があることに留意が必要です。
flowchart LR
A["入力"] --> B["エージェント応答"] --> C["ルーブリックプロンプト"] --> D["ジャッジLLM"] --> E["スコア"]
評価セットの育て方
評価の自動化を始めるにあたって、最初から大量のテストケースは必要ありません。 筆者たちが試してきた経験では、次の順序が実践的です。
- 代表的なユーザー入力を10〜20件選び、手動で期待する応答を書く
- 正解比較型の評価スクリプトを書き、CI(継続的インテグレーション)に組み込む
- ルーブリックを1軸設計し、LLMジャッジ型を追加する
- 本番ログから「悪い応答」サンプルを収集し、テストセットを拡充する
- 回帰テストとして定期実行する
この順序で進めると、「評価セットを作るコスト」と「評価の恩恵」のバランスが取りやすいです。
10評価設計でよくある落とし穴
スコアが「改善の何を示すか」が不明になる
平均スコアが上がっても、「どのケースが改善したのか」「何が変わったのか」が追えなければ、スコアは飾りになります。 評価結果は必ずケースIDに紐付けて保存し、バージョン間の差分を追えるようにしておくことを推奨します。
ルーブリックの粒度が粗すぎる
「1=悪い、5=良い」のような定義では、評価者によってスコアが大きく変わります。 前述のように「スコア3と4の違い」を具体的な行動・状態で記述することが、採点の一貫性につながります。
評価セットが配布データに偏る
最初に用意したテストケースが「うまくいくケース」ばかりになりがちです。 意図的にエッジケース(曖昧な入力・複数解釈が可能な質問・エラーケース)を混ぜることで、評価の網羅性が上がります。
LLMジャッジのバイアスを見落とす
LLMは長い応答を短い応答より高く評価する傾向、自身が生成したスタイルを好む傾向などが報告されています。 LLMジャッジを使う場合は、人間の評価と一定件数で照合し、系統的なズレがないかを確認することを推奨します。
11BizPlanでの評価設計の考え方
私たちが開発するBizPlan(事業計画エージェント)では、事業計画という性質上「正解が一意に定まらない」タスクが多くを占めます。 そのため、正解比較型だけでは品質の全体像を捉えにくいという課題がありました。
この課題に対して、私たちはルーブリックを「論理的一貫性」「根拠の明示」「業界文脈への適合性」の3軸で設計し、それぞれをLLMジャッジ型で自動評価する形を検討しています。 ただし、これはまだ設計段階であり、実測値や確定した手法として紹介できる段階ではありません。 評価の実践知見がたまり次第、別記事で詳しく報告する予定です。
12まとめ
エージェントの品質評価は「何を測るか」の設計から始まります。 指標はタスク達成・応答品質・効率・コストの4カテゴリに整理でき、最初から全部測ろうとするより、フェーズに応じて優先指標を絞ることが実践的です。
主観評価は、ルーブリックで採点基準を明文化することで定量スコアに変換できます。 評価の自動化は正解比較型とLLMジャッジ型の2系統から始め、テストケースを育てながら段階的に拡充する方法を取ると、コストと恩恵のバランスが取りやすいです。
13このカテゴリの関連記事
本記事は「評価と改善」カテゴリの入口記事として、指標の全体像を示しました。 各トピックの詳細は以下の記事で掘り下げています。
- 評価データセットの作り方:テストケースの設計・収集・管理手法
- LLM-as-a-Judge:LLMを評価者として使う:ジャッジ型評価の設計とバイアス対策の詳細
- 回帰テストでエージェントの品質を守る:CI組み込みと差分追跡の実践
- フィードバックループの設計:本番ログから評価セットを継続的に改善する仕組み
14参考文献
- Evaluating LLM-based Applications - RAGAS Documentation(2026年6月時点)
- Evals - OpenAI Platform Documentation(2026年6月時点)
- LLM Evaluation Frameworks Survey - arXiv(参考:LLM評価の分類整理として)
- Rubric-based Assessment in Education - Wikipedia(ルーブリック手法の背景)

