はじめに
社内向けのAI会議支援チャットを開発しており、チャット会議室ごとにAIファシリテーターが同席し、進行役として問いを投げたり、発言の少ない人に話を振ったりします。
このAIファシリテーターの振る舞いはプロンプトで決めており、要望や不具合が出るたびに書き換えてきました。
半年で版は v1.2 から v2.6 まで上がり、プロンプト本文は3万6千字ほどになっています。
運用で重かったのは、プロンプトを直したあとの確認作業で、1箇所を直すと別の場面の振る舞いまで変わることがあり、狙った効果が出たのか、どこかがデグレしていないのかを人が確かめる必要があります。
確認のたびにチャット会議テストを最初から回して発言を読むことになり、修正のたびに同じ時間がかかります。
そこで、AIの発言を別のAIが自動で採点する仕組み を入れました。
LLM-as-a-Judge と呼ばれる方式で、本アプリでは評価対象のプロンプトをそのまま評価器へ渡し、「プロンプトをどれだけ守れているか」を審査させています。
実利用の発言が毎日自動で採点されるようになり、おかしな挙動が出ていないかは画面で追えるようになりました。
今回はこの導入した AIによるプロンプト自動評価 についてまとめます。
なお、プロンプト側で守らせきれない要素をロジックへ引き取った話は、【Genkit】プロンプトの禁止事項は、モデルによって守られ方が変わる に書いています。
対象読者
- LLM-as-a-Judge を自前で回している方
- 評価が高得点ばかりで、改善の手がかりが得られていない方
- プロンプトを直した効果を数字で示したい方
仕組み:評価対象のプロンプトを、そのまま評価器に渡す
評価器に渡しているのは、審査対象であるAIファシリテーターのプロンプトそのものです。
「一般的に良い発言か」ではなく「指示書をどれだけ守れているか」を見る形にしてあり、指示書を書き換えれば評価の基準も自動で追従します。
採点する経路は2つあり、実利用の発言は日次バッチで毎日採点され、プロンプトを直したときの確認には検証用に用意した13の場面を使います。
場面というのは、会議の種類・目的・アジェンダ・そこまでの発言をひとそろいにしたもので、第一声から定刻超過、意見の対立、雑談まで並べてあります。
評価の軸は6つです。
| 軸 | 見ているもの |
|---|---|
| faithfulness | ナレッジに無いことを言っていないか |
| relevancy | 持ち出したナレッジが的外れでないか |
| roleCompliance | 進行役の役割を越えていないか |
| helpfulness | 議論の役に立たない発言になっていないか |
| conciseness | 冗長でないか |
| openItemsHonesty | 未決を決定として書いていないか |
この6軸で採点された点数が毎日ダッシュボードに並び、おかしな挙動が出ていないかを追える状態になりました。
ハマった点①:評価が全件100点で、直すところが見つからなかった
13場面をそれぞれ3回ずつ走らせて採点したところ、結果は下記のとおりでした。
| 場面 | 評価器 |
|---|---|
| 第一声 | 100点 |
| 定刻をむかえた | 100点 |
| 問いかけに返答があった | 100点 |
| 問いに誰も答えていない | 100点 |
| 可否に答えた直後 | 100点 |
| 終了宣言を受けた | 100点 |
| アジェンダに沿う | 100点 |
| 意見が対立している | 100点 |
| 抽象的な賛同が続く | 100点 |
| 新しい案が出た | 100点 |
| 根拠のない断定が出た | 100点 |
| 雑談(おやつを決める) | 100点 |
| 雑談(懇親会の店選び) | 100点 |
| 評価器の平均 | 100.0点 |
満点が並んでいるため、この画面を見るかぎり直すところは見つかりません。
ところが「新しい案が出た」場面では、議論の途中で出た新案を既存の案と並べて整理してほしいのに、その振る舞いが一度も出ていませんでした。
このときの実際の発言は、下記のようになっていました。
[参加者A]: いっそLPを2つ作って、両方の訴求を試すのはどうでしょうか。
[参加者B]: それも手かもしれませんね。
[AIファシリテーター]:
2つ作成して反応を見るという新しいアイデアですね。その場合、制作や運用の
手間・コスト面と、どちらの反応をもって良しとするかの判断基準はどう考えられ
そうでしょうか?
新案の工数は聞いていますが、既存の2案と並べる動作が入っておらず、9回まわしても並べた回は0回でした。
それでも点は100点のままで、ダッシュボード上は何も起きていないように見えていました。
原因は評価軸のほうにあり、先ほどの6軸はどれも「悪いことをしていないか」の検査で、「やってほしいことをやったか」を見る軸が1つもありませんでした。
新案を並べなかったことはどの軸にも違反しないため、減点されず100点になります。
評価器が残したコメントを読むと、事情がはっきりします。
「新しい案が出たら良し悪しを論じる前に手間・コストや判断基準を確認する」
というプロンプトの原則通り、適切な論点(コストと判断基準)を問いかけており、
黒子として簡潔かつ効果的に議論を前進させています。
評価器はこの発言を褒めており、プロンプトに「採否を論じる前に手間とコストを確認する」と書いてある以上、その判断自体は正しいものでした。
審査対象のプロンプトを丸ごと埋め込む方式は、指示書を直せば基準も追従する反面、指示書が望ましくない内容を含んでいれば、その誤りに忠実な発言へ満点を付けてしまいます。
実装:場面だけを凍結して、達成を別に判定する
改善の効果を測るにはプロンプトを変えたときに点が動く必要があり、そのためには何を固定するかが問題になりました。
当初は実際の会議から低得点のログを集めて凍結する形を考えていましたが、凍結したのが「AIが既に言ってしまった発言」だったため、プロンプトを直しても発言のほうは変わりません。
この形でも評価器の正しさは測れますが、改善の効果は永遠に見えないままでした。
そこで凍結するのは会話の場面だけにして、AIには毎回その場で発言させる形に変えています。
export type Scenario = {
id: string;
title: string;
/** ⚠️ この場面が何の実害から来ているか。消さないこと */
why: string;
meetingType: 'PROBLEM_SOLVING' | 'DECISION_MAKING' | 'IDEA_GENERATION';
goal: string;
agenda: string[];
/** 会議の開始が何ミリ秒前か */
startedMsAgo: number;
/** 会議の終了まで何ミリ秒か。⚠️ 負なら定刻超過 */
endsInMs: number;
phase: string;
seed: ScenarioLine[];
// ...
/** 何の能力を見ているか(表示用)。省略すると能力の判定をしない */
capability?: string;
/** できている印。1つでも含まれれば「できた」 */
expect?: RegExp;
/** ⚠️ 出てはいけない印。含まれたら「できていない」 */
antiExpect?: RegExp;
};
場面ごとに「やってほしいこと」を正規表現で書き、達成したかどうかは決定的に判定します。
達成の検査に評価器を使わないのは評価器のほうが揺れるためで、その実測値は後述します。
let capabilityOk: boolean | null = null;
if (s.capability) {
if (text === '') {
// ⚠️ **黙るのが正しい場面がある**(緩い場)。そこでは無音を達成として数える
capabilityOk = s.silenceIsOk === true;
} else {
capabilityOk = (!s.expect || s.expect.test(text))
&& (!s.antiExpect || !s.antiExpect.test(text));
}
}
この形にして同じ13場面を測り直したところ、下記の数字になりました。
評価器の平均: 100.0点
達成: 21/24(87.5%)
評価器は満点のままでしたが、達成のほうに落ち込みが出ており、下がっていたのは「新しい案が出た」場面の 0/3 だけでした。
実装:できていない場面に、プロンプトの直し方を出す
落ち込んでいる場面について、プロンプトのどこをどう直すかを別のAIに出させました。
このとき、下記の指示を1つ入れています。
1. ⚠️ **「書いていないから足す」と即断しないこと。**
まず指示書を読み、**既に該当する記述があるか**を確認してください。
既にあるなら、問題は「書いていない」ことではなく**他の指示に負けている**ことです。
その場合は、何に負けているのかを特定してください。
返ってきたのは、下記のような診断でした。
「論点の構造化」セクションにある「## 新しい案が出たら、良し悪しを論じる前に
確認する」の指示(手間やコスト・担当を確認する)に引っ張られ、新案が出た瞬間に
新案単体のリソースや工数の深掘り質問に飛びついてしまい、既存の案と並べて
選択肢を構造化する動作が阻害されています。
記述が足りなかったのではなく、既にあった記述が別の動作を押し出していた、という指摘でした。
該当箇所を確認すると、下記のように書いてあります。
## 新しい案が出たら、良し悪しを論じる前に確認する
* 議論の途中で新しい案(別案・折衷案・「両方やる」など)が出たら、
**採否を論じる前に次を確認する。**
- 目的(何を解決する案か)
- 手間とコスト(誰がどれだけ動くか)
- 担当(誰がやるか)
- 判断基準(何をもって良しとするか)
提案された書き換え後は下記のとおりです。
## 新しい案が出たら、既存の案と並べて整理する
* 議論の途中で新しい案(別案・折衷案・「両方やる」など)が出たら、いきなり
その案の詳細や実行性を掘り下げず、**まず既存の選択肢と並べて全体を整理する。**
- 「案A・案Bに加えて、第3の案(両方試す)が出た」形として選択肢を並べて示す。
- 並べた上で、比較検討に入るか新案の前提(目的・コスト)の確認へ進める。
提案には副作用の見立ても一緒に出させており、今回は「新案が出た際に即座に実行可能性(工数・担当者)を詰める動きは抑制されます」と返ってきました。
提案は実行IDではなく指示書の版に紐づけており、実行IDで絞っていたときは場面を回し直すたびに提案が消え、画面の主役が空になっていました。
適用そのものは人の作業にしてあり、プロンプトはチームが .md で版管理しているため、自動で書き換えると手元の版とずれてしまうからです。
ハマった点②:評価器・生成側・検査側が、別々に揺れる
ここまでの仕組みを作る過程で、別々に揺れる箇所が3つあることが分かりました。
どれか1つでも見落とすと、出てきた数字を信用できなくなります。
① 評価器が揺れる
きっかけは、リアルタイム評価と日次バッチが同じ発言を両方拾っており、評価2,894件のうち1,407件(48.6%)が重複していたことでした。
不具合ではありますが、その副産物として評価器のブレを測れています。
同じ発言を同じモデル・同じ温度で2回採点したところ、91.4%(1,286件)は同じ点でしたが、残りのうち3.7%(52件)は61点以上ずれていました。
平均のズレは5.0点で、プロンプトを直して平均が5点上がったとしても、このブレと区別が付きません。
そこで入力を凍結して同じものを3回ずつ採点し、毎回まったく同じ判定になる率を条件ごとに比べました。
| 条件 | 標本12件 | 標本24件 | 標本24件(再測) |
|---|---|---|---|
| Flash Lite / temp 0.1 | 50.0% | — | — |
| Flash Lite / temp 0 | 75.0% | 33.3% | 45.8% |
| Flash / temp 0 | 83.3% | 87.5% | 83.3% |
Flash Lite は測定値そのものが 33〜75% で暴れ、同じ標本・同じ温度でも実行ごとに違う結果になりました。
もともと安定していた例まで崩していたため、評価器を Flash / temperature 0 へ変えています。
温度を 0 と 0.1 で比べても Flash では差が出ず、効いたのはモデルのほうでした。
② 生成側が揺れる
評価器を安定させても、測られる側であるAIが毎回ちがう発言をするため、プロンプトを一切変えていない実行を2回比べたところ、改善1件・デグレ1件と表示されました。
同じ条件で9回ずつ測ると、場面によって成功率に幅があることが分かります。
- 抽象的な賛同が続く … 9/9(3回なら 3/3)
- 新しい案が出た … 0/9(3回なら 0/3)
- 意見が対立している … 8/9(3回なら 2/3 か 3/3)
- 雑談(おやつを決める) … 5/9(3回なら 0/3 から 3/3 まで何でも出る)
成功率が 0% や 100% に近い場面は3回で足りますが、56% の場面は3回だと何も言えません。
この成功率をもとに、6場面まわして「1本でも嘘のデグレが出る確率」を計算しました。
| 回数 | 少しでも下がったら | 30ポイント以上 | 50ポイント以上 |
|---|---|---|---|
| 3回 | 65.7% | 65.7% | 21.1% |
| 9回 | 76.7% | 23.2% | 2.6% |
| 15回 | 79.6% | 9.0% | 0.4% |
「少しでも下がったら」は回数をいくら増やしても8割前後から下がらず、判定に幅を持たせることが必須でした。
一方で回数が足りないと幅も効かず、3回だと1回ぶんの差が33ポイントあるため、30ポイントの幅を必ず超えてしまいます。
そこで回数が足りないときは、「デグレ」ではなく「判定できない」と出すようにしています。
/**
* ⚠️ **回数が足りないと、幅を設けても意味がない。**
* 3回なら1回ぶんの差が33ポイントあり、30ポイントの幅を必ず超える。
* ⚠️ **この場合は「デグレ」と呼ばず、判定できないことを明示する。**
*/
const step = 1 / Math.min(x.capTotal || 1, y.capTotal || 1);
const tooFew = x.capTotal > 0 && step > DEGRADE_MARGIN;
③ 検査側が揺れる
正規表現による達成の検査も、あとから書き直す必要が出ました。
プロンプトを直したあとに測ると 5/9 となり、半分しか直っていないように見えたため、発言を読み直しています。
当初の2案に加えて、第3の選択肢として「両方のLPを作成してテストする」という案が
出ましたね。
当初の2案に加えて、「2つ作って両方試す」という第3の案が出た形ですね。
落ちた4件はどれも既存の案と並べており、当たっていなかったのは下記の正規表現のほうでした。
expect: /3つ目|第三|新しい案|選択肢が|加える|並べ|比べ|整理すると|候補は/
「第三」はあるのに「第3」がなく、「加える」はあるのに「加えて」がないという状態で、できていない状態に合わせて書いたため、直ったあとの言い回しを想定できていませんでした。
書き直すときは、保存済みのログを使って両方向から検証しています。
| 検証に使ったログ | 旧の判定 | 新の判定 | 期待 |
|---|---|---|---|
| 直す前(3回) | 0/3 | 0/3 | 1件も通らない |
| 直す前(9回) | 0/9 | 0/9 | 1件も通らない |
| 直した後(9回) | 5/9 | 9/9 | 全部通る |
直す前の失敗が1件も通らないことを確認してから適用しており、この確認を省くと判定を緩めただけで改善したように見えてしまいます。
回してみた結果
プロンプトを v2.6 から v2.7 へ上げ、13場面を回し直しました。
| v2.6 | v2.7 | |
|---|---|---|
| 新しい案が出た | 0/3 | 3/3 |
| 達成(13場面の合計) | 21/24(87.5%) | 23/24(95.8%) |
「新しい案が出た」は3回では幅が効かないため、別途9回でも測り直しており、そちらは 0/9 から 9/9 になりました。
なお、この比較は場面の版が v4 から v5 へ上がっています。正規表現を書き直した結果なので、画面上も「場面そのものが変わっているので差は改善とは言えない」と警告が出る組み合わせです。
比較のときはデグレを先に出すようにしており、上がったところだけを見て適用すると下がったところを見落とすためです。
今回は「問いかけに返答があった」場面で「100点 → 78点」というデグレが出ましたが、9回まわすと減点は0回で、評価器のブレでした。
ただし発言を読み直したところ、別の問題が見つかっています。
佐藤さん、引き継ぎ負担の観点からのご意見ありがとうございます。田中さんの
『週替わり』に対して『2週間交代』の案が出ましたが、引き継ぎ負担を抑える方向
として『2週間』を軸に進めてよろしいでしょうか?
返答を受けた直後に、特定の案へ誘導しています。
これは直す前の版から3回に1回ほど出ており、評価器は100点を付けたり33点を付けたりで安定して検出できていません。
誤検知の調査をしたつもりが、未対応の課題を1つ増やした形になりました。
おわりに
自動評価を入れてから、スコアは毎日出ていましたが、数字が並んでいることと、その数字で判断できることは別でした。
測る側にも揺れがあり、そこを詰めるまでは改善の効果を主張できない状態が続いていました。
対策として有効なのは以下の3点です。
-
違反の検査と達成の検査を分ける
LLM-as-a-Judge は「規則を破っていないか」の判定には向いていますが、「やってほしいことをやったか」は別に持つ必要があります
正規表現のような決定的な判定で十分で、そのほうが評価器のブレを持ち込まずに済みます -
凍結するのは出力ではなく入力にする
AIの発言を凍結すると、プロンプトを直しても点が動きません
会話の場面だけを凍結して毎回その場で生成させると、同じ条件で前後を比べられます -
「何回まわせば差と呼べるか」を先に測る
同じ条件で9回まわして成功率を出し、そこから誤検知率を計算してから判定の幅を決めました
幅を持たせずに比べると、何も変えていなくても6場面中1本は下がって見えます
同じようにAIの出力を自動評価している方の役に立てば幸いです。



