LLM分類プロンプトの「誤検知」を自律改善ループで約1/3削減した話
取りこぼしを増やさずにノイズだけ削る、という難題への取り組みです。
TL;DR
- ネガティブニュースの「当事者性判定」(この人物・企業は事件の当事者か?)を行う LLM プロンプトを改善しました。
- 課題は Recall(取りこぼさない率)を保ったまま、ノイズ(誤って「該当」と判定する率)を下げることです。両者は強いトレードオフの関係にあります。
- 人手のプロンプト調整・モデル変更・入力文字量の調整では、トレードオフ曲線の上を移動するだけで壁を越えられませんでした。
- 最終的に 「LLMにプロンプトを改訂させ、別データで採否を判定する自律改善ループ」 が突破口になり、モデルを変えずに ノイズ率を約39%→26%(約1/3削減)/Recallは維持を達成しました。
- 本記事では、評価環境の作り方・突破口・勝ち筋のプロンプト設計・ハマった落とし穴を共有します。
背景と課題
あるコンプライアンスチェックの機能で、「取引先名 × ネガティブワード」でWeb検索し、ヒットした記事ごとに その取引先が事件・不祥事・行政処分の”当事者”かどうか を LLM で判定しています。
- 高(当事者): 本人・関連会社・役員などが逮捕/処分/提訴された等
- 中(非当事者): 同姓同名の別人、他社事例の引用、被害者側、サービスが第三者に悪用されただけ、規約ページ、等
この判定には、性質の異なる2種類の誤りがあります。
- 取りこぼし(false negative): 本当は当事者なのに「中」にしてしまう誤り。コンプラ用途では最も避けたいものです。
- ノイズ(false positive): 本当は非当事者なのに「高」にしてしまう誤り。人間の確認コストを増やします。
そして両者はトレードオフの関係にあります。中に倒す判断を強めればノイズは減りますが取りこぼしが増え、高に倒せばその逆になります。
目標は明確でした。Recall(=100−取りこぼし率)を下限(90%)に保ったまま、ノイズ率をできるだけ下げることです。
現行プロンプトの出発点は、ざっくり Recall 90%台/ノイズ率 約39%(プロンプトで動かせる範囲)でした。ノイズが高い=「高の出しすぎ」の状態で、ここを削るのがミッションです。
まず評価環境を作る(ここが8割)
プロンプト改善は「1本書いて試す」を高速に回せないと始まりません。そこで最初にオフライン評価環境を整えました。設計の勘所は次の通りです。
-
人手ラベル(Ground Truth)を固定のデータセットにする 数千件規模の「人間が正解を付けた判定ログ」をマスター化し、以降はこれを唯一の正解として使います。
-
入力(記事本文)を一度だけ取得してローカルキャッシュ プロンプトを変えても毎回Webフェッチしないよう、本文をキャッシュして同一インプットで反復検証できるようにしました。これで「プロンプトを変えた効果」だけを純粋に測れます。
-
採点は決定論的に行う(LLMを採点に使わない) 「LLMの出力(高/中)」と「人手ラベル」を文字列一致で突き合わせるだけにしました。LLMを評価者に使うと揺らぎが入り、評価器自体の信頼性を別途担保する羽目になります。採点は決定論に限るべきです。
-
本番の入力を忠実に再現する 本文からLLMに渡す抜粋を作る処理を、本番と厳密に一致させました(ここは地味ですが超重要です。後述の落とし穴を参照)。
この「同じ入力・同じ採点で、プロンプトだけ差し替えて比較できる」土台があって初めて、以降の実験が成立しました。
立ちはだかった壁: 何をやっても同じ曲線の上
評価環境ができてから、思いつく限りの手を試しました。
- プロンプトを中寄せ(非当事者の除外ルールを強化)→ ノイズは下がりますが Recall が急落
- プロンプトを高寄せ(迷ったら高を徹底)→ Recall は戻りますがノイズも戻る
- 入力の文字量を変える(キーワード周辺 100〜2000字)→ ほぼ無影響(判断材料は共起箇所の近傍に集約されているため)
- モデルを上位に変更(より賢いモデル)→ ノイズは激減しますが、今度は取りこぼしが激増して Recall が大きく割れる
結果はどれも同じで、「Recall ↔ ノイズ」のトレードオフ曲線の上を移動するだけでした。曲線そのものを内側(同じRecallでノイズがより低い側)へ動かすことができません。
誤検知の中身を分析すると、ノイズの大半は「規約・プライバシーポリシーのページ」「同姓同名の別人」「サービスが第三者に悪用されただけで事業者は加害者でない」「注意喚起・啓発記事」「単なるコメント・言及」といった明確な非当事者でした。つまりラベルは正しく、LLMがこれらと”本物の当事者”を細かく区別しきれないことが本質的な壁だったのです。中に倒す指示を強めると、この区別ができずに本物の当事者まで巻き込んでしまいます。
突破口: 自律プロンプト改善ループ
手で書くのをやめ、LLM自身にプロンプトを改訂させるループを組みました。1サイクル(世代)は次のようになります。
- 現行ベストのプロンプトで評価データを判定し、誤答(ノイズ/取りこぼし)の実例を集めます。
- その実例(記事・人手の判断理由・LLMの誤答理由)を生成役のLLMに渡し、「Recallを保ったままノイズを減らす改訂案」を書かせます。
- 改訂案を評価し、良ければ採用します(山登り)。これを繰り返します。
ただし素朴に回すと、**評価データの誤答例に過剰適合(過学習)**します。実際、最初の実装では「学習に使ったデータでは改善したのに、別データでは元に戻る」という典型的な過学習が起きました。
そこで効いた最大の工夫が、採否の判定を”別データ”で行うことです。
- eval セット: 誤答例を抽出してLLMに見せる(=学習に使う)
- select セット: 改訂を採用するか判定する(eval とは別データ)
- test セット: 最後に一度だけ触る、汎化性能の最終確認用
こうすると、「eval の誤答例に丸暗記で合わせただけ」の改訂は select で改善しないため自動的に弾かれ、別データでも通用する改訂だけが生き残ります。この一手で、ループは初めて意味のある改善を積み始めました。
結果、モデルは一切変えずに、プロンプトだけで ノイズ率 約39% → 約26%(約1/3削減)/Recall はむしろ微増 という、手作業では届かなかった地点に到達しました。
勝ち筋のプロンプト設計: 「安全弁つきの外科的除外」
面白いのは、ループが見つけた勝ちプロンプトの構造です。それは「中に倒す力を強めた」ものではなく、次のような作りでした。
- 現行プロンプトをそのまま温存する(=取りこぼさない緩い基準を壊さない)。
- その末尾に、ノイズ源の類型を狙い撃ちする除外ルールを”追記”する(規約ページ、同姓同名、第三者悪用、被害者、注意喚起、等)。
- 各除外ルールに二重の安全弁をかける。
この安全弁がキモで、これがなければ従来同様に取りこぼしが増えてしまいます。
- 安全弁①(条件付き): 各除外ルールは「かつ、具体的な処分・逮捕・被告などの記述が無い場合のみ中にする」という条件付きにします。曖昧なら高を維持します。
- 安全弁②(最終オーバーライド): どの除外ルールに当てはまっても、記事に「処分/被告/課徴金/逮捕/送検」等の明確な当事者性の語があれば必ず高に戻すようにします。
イメージは次の通りです(※実際の運用プロンプトとは別の、説明用の簡略例)。
# 判定: 対象が事件の「当事者」なら 高、そうでなければ 中。迷えば高。
## 中にしてよい例(下記に該当し、かつ "処分・逮捕・被告" 等の記述が無い場合のみ)
- 規約/プライバシーポリシーのページで具体的な事案が書かれていない
- 同姓同名だが別の職業・所属が明示されている
- サービスが第三者に悪用されただけで、対象自身の加害・処分の記載がない
- なりすまし・偽サイトへの注意喚起で、対象は被害者側
...
## 最終ルール(最優先)
上記のいずれに当てはまっても、記事に「処分・被告・課徴金・逮捕・送検」等の
具体的な当事者性を示す語があれば、必ず「高」とする。
一言でいうと 「安全に落とせるものだけを、逃げ道つきで落とす」 という設計です。本物の当事者は具体語で高に引き戻されるので取りこぼしません。この繊細な線引きは、人間が一発で書き切るのは難しく、ループが誤答例を見ながら少しずつ削り出したものでした。
苦労したこと(落とし穴集)
きれいに進んだわけではありません。むしろ罠だらけでした。
1. 過学習(一番の敵)
前述の通り、採点データと採否データを分けないと、ループは「テストの答えを丸暗記」してしまいます。別データでの採否が必須でした。加えて、eval で良く見えた改訂が全データでは元に戻る、というサンプル起因の”見かけの改善”にも何度も騙されました。小さなサンプルでの良化を信用しないのが鉄則です。
2. 目的関数が反転して自滅する
「Recall 90%以上ならノイズ最小化、未満ならRecall最大化」という目的関数にしていたところ、サンプル誤差でベースラインのRecallがたまたま90%を下回って測定された世代で、目的が「Recall最大化」に切り替わってしまいました。その結果、ノイズを無視してRecallだけを99%まで追う→ノイズ爆発という自滅が起きました。境界付近で目的が不連続に切り替わる設計は危険だと痛感しました。
3. 「測れないものは改善できない」——指標の分解能
Recall は「本物の当事者」の件数でしか測れません。ところがプロンプトで動かせる本物の当事者は数百件しかなく、Recall の測定誤差が ±3%程度ありました。これだと「ノイズを数%下げた」ような小さな改善は誤差に埋もれて検出できません。改善の余地よりも先に、指標の分解能が限界を作るという学びは大きかったです。
4. 生成物が評価器を壊す(エンジニアリングの罠)
- 取得した記事本文に不正なバイト列由来の壊れた文字が混じり、キャッシュ書き込み時にクラッシュしました。→ 事前サニタイズで対処。
- 生成役のLLMが出力したプロンプトに
{...}のような波括弧が含まれ、それを文字列テンプレートに流し込む処理が誤爆してクラッシュしました。→ 既知のプレースホルダだけを安全に置換する方式に変更。 - 長時間バッチが途中で落ちても成果を失わないよう、ベストを逐次保存する仕組みとクラッシュ耐性を入れました。
5. 本番再現の地味な罠
本文からLLMへ渡す抜粋を作る処理で、当初はHTMLの要素間に改行を入れて抽出していましたが、本番は入れていませんでした。この差で文の区切り方が変わり、抽出結果が本番とズレていたのです。“だいたい同じ”では評価がズレます。本番と1文字レベルで揃える必要がありました。
結果と限界
- モデル変更なし・プロンプトのみで、ノイズ率を約1/3削減し、Recallは維持(むしろ微増)できました。実運用相当の見方ではRecallは95%台を確保しています。
- 一方で、ノイズ率は約26%あたりで頭打ちになりました。モデル変更・文字量・2段階化(除去役→救出役の分割)でも越えられず、これが現行モデル+プロンプトの実質的な下限と見ています。
- Recall の確証には正例(本物の当事者)の追加ラベルが要ります。指標の分解能の問題は、最終的には評価データの拡充でしか解けません。
学び
- 評価環境が主役です。決定論的な採点・入力の固定・本番の忠実再現がすべての土台であり、ここに投資した分がそのまま返ってきました。
- プロンプト改善は”自律ループ+過学習対策”で質が変わります。人手の職人芸よりも、誤答例を食わせて別データで採否するループの方が、繊細な線引きを見つけました。
- 勝ち筋は”守りの設計”にありました。「落とす力を強める」のではなく「安全に落とせるものだけ逃げ道つきで落とす」。取りこぼしを増やさない安全弁こそが本質でした。
- 測定の限界を先に知ることが大切です。改善に着手する前に「その改善を検出できるだけの分解能があるか」を問うべきでした。
トレードオフの壁は完全には消えませんが、「壁の上を動く」から「壁を少し内側に押す」へは到達できました。そして何より、その一歩は人手の勘ではなく、評価と自律改善の仕組みが生んだものでした。
Anycloudでは一緒に働くメンバーを募集しています!
Anycloudは、ユーザーの心を動かす体験を届けることを大切にしています。フルリモート・フルフレックスの環境のもと、ライフスタイルに合わせた働き方を実現しながら挑戦したい方を歓迎します。詳細はこちらをご覧ください。