0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

業務AIの「なんとなく良さそう」をやめる:正解データで採点し、誤答を原因別に数え、CIで止める評価ツールを作った

0
Last updated at Posted at 2026-10-13

はじめに

生成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 にそろえる
evalkit/graders.py(抜粋)
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 感情の判定違い 何をネガティブとするか定義する
evalkit/graders.py(抜粋)
# 優先順位順。最初に当てはまったものを採用する。
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 には、迷いやすいケースの判断ルールを書いています。

prompts/few_shot_schema.txt(抜粋)
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 に、リリースするプロンプトと最低ラインを書きます。

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つを差し替えるだけ

最近のモデルはかなり賢くなり、ハルシネーションのような誤りはほとんど見かけなくなった気がします。ただ、それを「気がする」で終わらせず、数字で確かめられるようにしておくのが、この仕組みの良さだと思っています。

リポジトリ:https://github.com/yahayuta/business-ai-evals

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?