はじめに
生成AIを業務に組み込むとき、それらしく動くデモは簡単に作れます。難しいのは、使われ始めたあとも信頼できる状態を保つことです。
- 新しいプロンプトは本当に良くなったのか?試した3件でたまたま良かっただけでは?
- 間違えたとき、原因は出力形式なのか、振り分けなのか、値の捏造なのか?
- モデルが更新されて精度が落ちたら、誰か気づけるのか?
アプリに組み込んでも事前のテストが難しく、結局は運用が始まってから、プロンプトを人力でチューニングしていました。
そこで、これに答えるための小さな評価ツールを作って公開しました。Python の標準ライブラリだけで動き、APIキーなしでCIまで回せます。
題材は、ネットショップの問い合わせの振り分けです。お客様からのメッセージを読んで、次の3つを出します。
-
category:注文状況・キャンセル・返品返金・住所変更など7種類のどれか -
order_id:注文番号(ORD-10234の形にそろえる。なければnull) -
sentiment:ポジティブ・ニュートラル・ネガティブ
評価のループ
正解付きのデータは JSON Lines で、1行1件です(40件の架空の問い合わせを同梱)。
{"id": "T002", "input": "Can you tell me when order #10871 will ship?", "expected": {"category": "order_status", "order_id": "ORD-10871", "sentiment": "neutral"}, "tags": ["id_format"]}
tags は自由に付けられ、「あいまいな問い合わせ」「注文番号の書き方が特殊」などの切り口ごとに正答率を出せます。
1. 採点:比べる前に正規化する
AIの出力は、ちょっとした書き方の違いで「不正解」になりがちです。そこで、比べる前にそろえます。
- JSONの前後に説明文やコードブロックの記号が付いていても読み取る
- ラベルは大文字小文字を区別しない
- 注文番号は
#10234・order 10234・ord-10234をすべてORD-10234にそろえる
def normalize_order_id(value) -> str | None:
"""'#10234', 'order 10234', 'ord-10234' -> 'ORD-10234'. Empty -> None."""
if value is None:
return None
text = str(value).strip()
if text.lower() in {"", "null", "none", "n/a"}:
return None
match = _ORDER_DIGITS.search(text)
return f"ORD-{match.group(1)}" if match else text.upper()
そのうえで、期待するすべての項目が一致したときだけ合格にします。
2. 誤答は「1件につき原因を1つ」に分類する
ここがこのツールの肝です。不正解になった理由を、決めた優先順位で1つだけ付けます。
| 誤りの種類 | 意味 | よくある直し方 |
|---|---|---|
invalid_json |
JSONとして読めない | 「JSONだけを返す」と指示する、JSONモードを使う |
missing_field |
必要な項目がない | スキーマ全体を見せる |
invalid_label |
決められたカテゴリ以外 | ラベルを明示的に列挙する |
wrong_category |
正しいラベルだが振り分けが違う | 迷いやすいケースの判断ルールを書く |
wrong_order_id |
注文番号の見落とし・捏造・読み違い | 「なければ null。作らない」と書く |
wrong_sentiment |
感情の判定違い | 何をネガティブとするか定義する |
# 優先順位順。最初に当てはまったものを採用する。
ERROR_TYPES = [
"invalid_json",
"missing_field",
"invalid_label",
"wrong_category",
"wrong_order_id",
"wrong_sentiment",
]
原因を1つに絞ると、件数を足し算できます。「どの直し方をすれば、いちばん多くの誤りが消えるか」がレポートから直接わかるようになります。
3. プロンプトを並べて比べる
同梱の例では、3つのプロンプトを比べています。
-
baseline:指示だけ -
few_shot:例を3つ付ける -
few_shot_schema:例に加えて、スキーマと判断ルールを明記する
few_shot_schema には、迷いやすいケースの判断ルールを書いています。
1. order_id: write any order number as ORD-<digits> ("#10234" or "order 10234" -> "ORD-10234").
If the message contains no order number, use null. Never invent one.
2. category: choose what the customer wants us to DO next.
- Wants to stop an order that has not arrived -> cancel_order (even if they also mention a refund).
- Item received and wants a return, exchange, replacement or refund -> return_refund.
レポートには、プロンプトごとの正答率・項目別の正答率・誤りの種類別件数・タグ別の正答率・カテゴリの混同行列・失敗した全ケース(実際の出力つき)・レイテンシ・トークン数・概算コストが出ます。
同梱の数字はシミュレーションです。 APIキーなしでCIを回すため、記録済みの応答を scripts/simulate_runs.py で生成しています。よくある失敗パターン(JSONの前後に文章が付く、注文番号を捏造する、あいまいな依頼で迷う)を再現したもので、実在のモデルの測定値ではありません。
シミュレーションでのレポートの一部です(見え方の例として)。
| プロンプト | 正答率 | JSONとして有効 | category | order_id | sentiment |
|---|---|---|---|---|---|
baseline |
57.5% | 92.5% | 82.5% | 75.0% | 75.0% |
few_shot |
65.0% | 95.0% | 82.5% | 85.0% | 85.0% |
few_shot_schema |
97.5% | 100.0% | 97.5% | 100.0% | 100.0% |
| 誤りの種類 | baseline |
few_shot |
few_shot_schema |
|---|---|---|---|
| invalid_json | 3 | 2 | 0 |
| missing_field | 1 | 0 | 0 |
| wrong_category | 4 | 5 | 1 |
| wrong_order_id | 6 | 3 | 0 |
| wrong_sentiment | 3 | 4 | 0 |
こう並ぶと、「例を足すだけでは振り分けの誤りは減らない。判断ルールを書くと減る」といった読み方ができます。
4. 品質ゲート:基準を下回ったらCIを落とす
evals.toml に、リリースするプロンプトと最低ラインを書きます。
[gate]
variant = "few_shot_schema"
min_accuracy = 0.85
min_json_valid = 1.0
min_field_accuracy = { order_id = 0.95 }
python -m evalkit check は基準を1つでも下回ると終了コード1を返します。GitHub Actions に組み込んでおけば、プロンプトを変えて精度が落ちたプルリクエストはマージ前に止まります。 レポートはジョブのサマリーにも表示されます。
5. 実際のモデルで測るには
設定にバリアントを1つ足して、APIキーを設定するだけです(OpenAI・Anthropic に対応)。
[[variants]]
name = "few_shot_schema-live"
prompt = "prompts/few_shot_schema.txt"
provider = "anthropic" # または "openai"
model = "your-model-id"
price = { input_per_mtok = 0.0, output_per_mtok = 0.0 }
export ANTHROPIC_API_KEY=...
python -m evalkit run --variant few_shot_schema-live
記録済みの応答を再採点する「replay」と、実際に呼び出す方法を、コードではなく設定で切り替えられるようにしています。普段のCIは無料で決定的に、実測したいときだけ実際のモデルを呼ぶ、という使い分けができます。
設計で意識したこと
-
標準ライブラリだけ:HTTPは
urllib、設定はtomllib。インストールやバージョン管理の手間がない - まずは決定的な採点:正規化したうえでの完全一致は、安く、説明しやすく、結果がぶれない。自由記述の評価にLLMを審査員として使う方法もあるが、その審査員自体も評価してからゲートに使うべき
-
自分のタスクにすぐ置き換えられる:データ(JSON Lines)・プロンプト・
evals.tomlの3つを差し替えるだけ
最近のモデルはかなり賢くなり、ハルシネーションのような誤りはほとんど見かけなくなった気がします。ただ、それを「気がする」で終わらせず、数字で確かめられるようにしておくのが、この仕組みの良さだと思っています。