はじめに~Mastra Evalsで評価機能を実装する
ここまで3回にわたって、メール返信エージェントを段階的に構築してきました。第2回でAgent/
しかし、ここで一歩立ち止まって考えてみましょう。
この問いに、現状では答えることができません。メモリが効いているように
この問いに答えるための一般的な概念・
AIエージェントの評価(Evals)とは
なぜ AI エージェントの評価は難しいのか
通常のソフトウェアであれば、テストは
- 出力が確率的:同じ入力でも毎回違う応答が返る[1]
- 正解が一つではない:
「丁寧な返信」 「相手の要望を拾った返信」 のように、評価軸自体が文脈依存 - 挙動がプロンプトやモデルに強く左右される:一文書き換えただけ、あるいはLLMのモデルやバージョンを変えただけで挙動が変わる
このため、LLM/
評価関数(Scorer/Evaluator)という基本道具
このアプローチの中心にあるのが評価関数です。フレームワークによってScorer・
たとえば
評価関数の2つの方式:コードベースvsLLMベース
評価関数の中身の作り方には、大きく2つの方式があります。Mastraをはじめ主要なEvalsツールで共通して採用されている分類です。
- コードベース
(Code-based scorer) :厳密なマッチングによる決定論的な評価 - LLMベース
(LLM-based scorer) :AIを用いた意味的(セマンティック) な評価
実プロジェクトではどちらか一方を選ぶのではなく、低コストなコードベースで広く網を張り、判定が難しい観点だけLLMベースで補うという組み合わせ運用が定石です。
ここまでが
Mastraでの実装:スコアラー
Mastraでは評価関数をスコアラー
本連載のメール返信エージェントでは、この組み合わせの中から次の3種類を使い分けています。手軽な
| 種類 | スコアラー名 | 評価する観点 |
|---|---|---|
| 組み込み・ |
completenessScorer | 入力情報を応答に網羅できているか |
| カスタム・ |
replyLengthScorer | 返信の長さが指定した文字数に収まっているか |
| カスタム・ |
requirementCoverageScorer | 相手の具体要望を拾えているか |
組み込みスコアラーから始める
最も手軽な選択肢が、Mastraが標準で提供する組み込みスコアラーです。@mastra/
import { createCompletenessScorer } from "@mastra/evals/scorers/prebuilt";
export const completenessScorer = createCompletenessScorer();
createCompletenessScorerは内部で compromise というNLPライブラリで名詞・
カスタムスコアラー:コードベース
組み込みにない観点
export const replyLengthScorer = createScorer({
id: "reply-length-scorer",
name: "Reply Length",
type: "agent",
}).generateScore(({ run }) => {
// 返信本文の文字数を取得
const length = getReplyLength(run);
// 目標レンジ(80〜800字)に収まっていれば満点
if (length >= 80 && length <= 800) return 1;
// レンジ外は外れ具合に応じて 0〜1 のスコアを返す
return calcLengthScore(length);
});
文字数をカウントしているだけなのでLLMを呼ばず、決定論的かつ低コストで、CIで高速に回せる実装になっています。
カスタムスコアラー:LLMベース
コードでは評価しづらい
export const requirementCoverageScorer = createScorer({
id: "requirement-coverage-scorer",
type: "agent",
// ジャッジに使う LLM と判定基準を渡す
judge: { model: "google/gemini-2.5-flash-lite", instructions: "..." },
})
// 1) 入出力テキストを取り出す
.preprocess(({ run }) => ({
userText: getUserText(run),
assistantText: getAssistantText(run),
}))
// 2) LLM に「要望の総数」と「拾えた数」を判定させる
.analyze({
outputSchema: requirementCountSchema,
createPrompt: ({ results }) => buildJudgePrompt(results),
})
// 3) 拾えた数 ÷ 要望の総数 で 0〜1 に正規化
.generateScore(({ results }) => calcCoverage(results));
同じ3ステップ構成で、相手が指定した言語
評価のタイミング
スコアラーの作り方がわかったので、次は
一般概念:3つの評価タイミング
LLMアプリケーションの評価は、運用ライフサイクルのどこで実行するかによって3つのタイミングに分けて考えるのが定石です。
| タイミング | 目的 | 性質 |
|---|---|---|
| ①ライブ評価 |
運用中のエージェントを継続監視し、デグレを検知する | 本番トラフィックに同居・ |
| ②CI 評価 |
プロンプトやモデル変更 PR が品質を下げていないかをマージ前にゲートする | 少数のテストケースで高速判定 |
| ③バッチ評価 |
改善前後を腰を据えて比較し、施策の効果を定量確認する | 多数のケースを一括実行・ |
これら3つは目的が異なるため、実務では評価関数を3つのタイミングで使い分けるのが良いでしょう。
Mastraでの実装:3つのAPI
Mastra Evalsでも、この3タイミングそれぞれに対応するAPIが用意されています。①ライブ評価はAgentのscorersフィールド、②CI評価はrunEvals、③バッチ評価はdatasets+startExperimentを使います。順に見ていきましょう。
①ライブ評価―サンプリングと運用監視
エージェント定義のscorersフィールドにスコアラーとサンプリング率を登録すると、本番/
export const mailReplyAgent = new Agent({
/* ... */
scorers: {
replyLength: { scorer: replyLengthScorer, sampling: { type: "ratio", rate: 1 } }, // 全件
requirementCoverage: { scorer: requirementCoverageScorer, sampling: { type: "ratio", rate: 0.1 } }, // 10%
},
});
サンプリング率はコストに応じて調整します。
たとえば、変更直後で挙動を細かく見たい場合や低コストなコードベース・
②CI評価―PRの品質ゲート
プロンプトを変更したPRで
const result = await runEvals({
data: [
{ input: "見積書の送付をお願いしたいのですが" },
{ input: "先日のお見積もりの件、検討の結果お願いしたいです" },
],
target: mailReplyAgent,
scorers: [replyLengthScorer, completenessScorer],
});
// スコアが閾値を超えているかを判定
expect(result.scores["reply-length-scorer"]).toBeGreaterThan(0.8);
runEvalsはデータセット登録なしで、インラインのケース配列で完結します。expectで閾値判定するので、スコアが下がったPRはCIが落ち、マージ前に気づけます。
③Datasets+Experiments―腰を据えた改善サイクル
「プロンプト改善の前後でスコアがどう変わったか」
- Datasets:複数のテストケースをMastraのストレージに保存する仕組み。アイテムの追加・
更新のたびにデータセットのバージョンが自動付与され、過去のデータセットバージョンに対しても再実験できる - Experiments:データセットに対してテストを走らせ、結果をまとめて保存する仕組み。Studioで一覧・
個別に確認できる
- 補足:なぜDatasetsを使うのか?
- TypeScriptの配列で書いてGit管理するだけでもrunEvalsで同じことができます
(前節がそれです)。Datasets を使う主なメリットとして、Studioの管理画面から非エンジニアでもプロンプトやテストケースを手軽に調整できること、実装コードを書き換えずに、複数のデータに対して調整結果を一括で確認できること、アイテムの変更履歴が自動でバージョン管理されること、実験結果と紐付けて永続化・ 比較できる ことなどが挙げられます。これら以外にもメリットがあるため、CI品質ゲートのように手軽さ重視の場面ではrunEvals、腰を据えた改善サイクルや非エンジニアとの協業ではDatasets、と用途に合わせて使い分けると良いでしょう。
データセットの定義やExperimentsの実行は、コードからも、Mastra Studioの管理画面からも行えます。実際の利用例は次節で紹介します。
改善サイクルの実践
ここまで揃ったスコアラーと評価タイミングを使って、実際に改善サイクルを1周回してみましょう。本連載リポジトリには7件のテストケースを持つデータセットmail-reply-casesが登録されているので、これを使ってbaseline計測→プロンプト調整→再計測→結果比較の流れを順に進めていきます。
Step 1:baseline の計測
まずは現状のエージェントに同じデータセットを通し、改善前のスコアを取ります。Studioから
EXP_NAME=baseline pnpm experiment
Step 2:分析
実行が終わると、各テストケースのスコアが並びます。
items: 7/7 ok, 0 failed
[12462f43...] Reply Length=0.67
[3cd38397...] Reply Length=0.67
[19d9aef6...] Reply Length=0.81
[c15fe082...] Reply Length=0.91
...
スコアが低いアイテムから
Step 3:プロンプトを調整
分析を踏まえて、エージェントの指示文に返信の長さルールを追加してみます。
// 改善前
instructions: `あなたはメール返信の担当者です。
受信メールの内容を正確に理解し、適切な返信を作成してください。`;
// 改善後(返信ルールを追加)
instructions: `あなたはメール返信の担当者です。
受信メールの内容を正確に理解し、適切な返信を作成してください。
【返信のルール】
- 返信本文は200~400字前後に収めること`;
Step 4:改善版で再計測
同じデータセットでもう一度実験を走らせます。
EXP_NAME=improved-v1 pnpm experiment
実行後はStudioのExperimentsタブにbaselineとimproved-v1が並んで保存されます。
Step 5:結果の比較
2つの実験のスコアを並べると、改善効果が一目で確認できます。
| スコアラー | baseline | improved-v1 | 差分 |
|---|---|---|---|
| reply-length-scorer | 0. |
1. |
+0. |
ベースラインとなる改善前の平均スコアは0.
改善後は7件すべてが目標レンジに収まり、平均スコアは1.
連載のまとめ
全4回を通じて、メール返信エージェントを段階的に構築しながらAIエージェント開発の主要要素を学んできました。第1回ではAIとAIエージェントの違いを整理し、教材としてMastraを選んだ理由と完成形のデモを紹介。第2回ではAgent/
本連載で扱ったAgent/
AIエージェントは
