9月12日、Towards Data Scienceが「One Capital Letter Was Silently Breaking My AI Support Bot, and It Wasn't in the New Model」と題した記事を公開した。この記事では、AIサポートボットの回帰テストにおいて、モデルの精度スコアが上がっていても出力フォーマットが壊れているという見落としやすいバグをどう検出するかについて詳しく紹介されている。
大文字1文字がサポートボットを静かに壊していた
銀行のサポート受付を想像してほしい。「カードの紛失」「返金リクエスト」「不明な請求」といった問い合わせが次々に届き、それぞれ担当チームに振り分けられる。このルーティングをAIに任せる場合、モデルは以下のようなJSONを返す設計になる。
{"intent": "request_refund", "priority": "high", "needs_human": true, "reply": "..."}
プログラムはこのJSONを機械的にパースして処理を続ける。人間なら「Request_refundでもrequest_refundでも同じ意味だ」と読み飛ばすが、文字列を完全一致で比較するプログラムは静かに処理を落とす。エラーも出ない。ログにも残らない。チケットが消えるだけだ。
記事の著者はこの問題を、47件の実際の銀行問い合わせ(公開データセットBANKING77から抽出)を使ったLLM回帰テストで発見した。テスト対象は3バージョンのOpenAIモデル:古いバージョン、現行の本番モデル、そして導入候補の新モデルだ。
結果は予想外だった。フォーマットを壊していたのは新モデルではなく、現行の本番モデルだった。 しかも返金関連の問い合わせすべてで、request_refundと返すべきところをRequest_refund(Rが大文字)と返し続けていた。精度スコア(正しいカテゴリを選べたか)だけを見ていたら、この問題は永遠に気づかれなかった。
人間が読めば「正しい答え」に見える。でもそれを使うソフトウェアにとっては「壊れた答え」だ。
これが記事全体のテーマである。
なぜ精度スコアだけでは足りないのか
AI企業は新モデルを頻繁にリリースし、採用判断は往々にして「正解率が上がった」という1つの数字で決まる。しかし精度が上がりながら出力の形式が崩れるというケースは十分ありうる。
本番環境への影響で言えば、カテゴリを間違えるよりフォーマットを壊す方が深刻な場合がある。カテゴリ間違いは「誤った担当チームに振られる」だけだが、フォーマット破損は「チケットが存在しないかのように消える」からだ。
テストの設計:厳格なチェッカーとAI審査員の2層構造
著者が構築したテスト基盤は、Weights & BiasesのWeave(実験のトレースを記録・可視化するMLOpsツール)を使って全トレースを記録する設計になっている。Weaveでは@weave.op()デコレータを関数に付けるだけで、入力・出力・処理時間がすべて自動保存される。
評価は2種類のチェッカーを組み合わせて行う。
① 決定論的チェッカー(ルールベース)
- 有効なJSONか
- 必須フィールド(
intent、priority、needs_human、reply)がすべて存在するか priorityはlow/medium/highのいずれかかintentの値は許可リストと完全一致するか- エスカレーション判定(
needs_human)が正しいか
② AI審査員(LLMジャッジ)
- 人間のレビュアーのように応答の質を評価する
- ルールベースでは見落とすニュアンスの問題を補完する
この2層構造がポイントだ。決定論的チェッカーだけでは「意味は正しいが表現が硬い」を見逃す。AI審査員だけでは「大文字/小文字の不一致」を見逃す。両方使うことで、それぞれが補い合う。
Structured Outputsを使えばよかったのでは?
OpenAIにはStructured Outputsという機能があり、JSONスキーマを強制できる。これを使えば今回の問題は防げたのか?
著者はあえてこの機能を使わなかった。理由は明確で、「Structured Outputsが隠してしまうバグを可視化すること」自体がこの実験の目的だからだ。記事では、Structured Outputsが防げる失敗と防げない失敗を正直に整理している。フォーマット破損(JSONではない文字列が混入する、フィールドが欠ける)は防げる。しかし値の意味的な正しさ(正しいカテゴリを選べているか)は防げない。どちらの問題を優先するかによって、機能の使い分けが変わる。
実装の要点:比較の公平性とプロンプトのバージョン管理
テスト対象はtriage_message(text, model, prompt_ref)という1関数に絞った。モデルのバージョンとプロンプトのバージョン以外は何も変えない。これにより比較が公平になる。
プロンプトはv1とv2の2バージョンを用意した。v1ではエスカレーションポリシー(needs_humanをいつtrueにするか)を明示しなかったため、スモークテストで6件中2件しか正解しなかった。v2でポリシーを明記したところ、6件中6件に改善した。プロンプトの変更がどの程度出力を変えるかを追跡できる点が、Weaveによるバージョン管理の実質的な価値だ。
エスカレーション対象として事前定義したインテントは以下だ(コードから抜粋):
ESCALATE_INTENTS = {
"lost_or_stolen_card",
"lost_or_stolen_phone",
"compromised_card",
"card_payment_not_recognised",
"direct_debit_payment_not_recognised",
"cash_withdrawal_not_recognised",
"unable_to_verify_identity",
"verify_source_of_funds",
"pin_blocked",
"transaction_charged_twice",
}
このリストはモデルの出力を見る前に定義した点が重要だ。テスト結果を見てから基準を調整するのでは、テストの意味がない。
まとめ
精度スコアは「モデルが賢いか」を測る。しかし本番で必要なのは「モデルが自分のシステムと正しく噛み合うか」だ。この2つは別物であり、どちらかだけを見ていると今回のような事故が起きる。実際の問い合わせデータ、ルールベースの厳格なチェッカー、AI審査員の3点セットで回帰テストを設計することが、モデル更新時のリスクを下げる現実的な手段である。次にモデルの乗り換えを検討する際、「スコアが上がった」という一点だけで判断していないか、今一度確認してみる価値があるだろう。
詳細はOne Capital Letter Was Silently Breaking My AI Support Bot, and It Wasn't in the New Modelを参照していただきたい。