1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

LLM分類を本番で使うための6つの実装パターン -- 誤判定を"取り返しのつくもの"にする

1
Posted at

Gemini_Generated_Image_s3d7rqs3d7rqs3d7.png

はじめに

LLMでテキスト分類(スパム判定、問い合わせインテント分類、レビューのネガ/ポジ判定など)を本番に組み込むとき、一番怖いのは 「精度99%です」で済まない側 です。

  • スパム判定で正当な問い合わせを弾く → 機会損失。クレーム。
  • ネガ判定で前向きなレビューを非表示にする → レビューサイトの信頼失墜。

1%の誤判定が、ユーザー1人の1件なら「まあ仕方ない」で済むかもしれません。でも、その1人にとってはその1件が全てです。誤判定を"取り返しのつくもの"にする 設計が、LLM分類を本番で使えるかどうかを分けます。

この記事では、私が FORMLOVA というフォームサービスで営業メール自動検知を本番投入したときに採用した 6つの実装パターン を、具体コードと共にまとめます。スパム分類以外の用途(インテント分類、コンテンツモデレーション、カテゴリ自動付与など)にもそのまま応用できるはずです。

関連記事:


パターン1: 判定手順をステップ化してプロンプトに書く

Gemini_Generated_Image_7ttnuz7ttnuz7ttn.png

問題

「営業ならsales、そうでなければlegitimate」のような2値のゴール記述だけだと、グレーゾーンでモデルの挙動が安定しません。何をもって判定しているかが曖昧で、同じような入力でも結果がブレます。

解法

判定手順そのものをプロンプトに書く。モデルに「まずXをして、次にYを判断し、最後にZの基準でラベルを決めろ」という順序を固定します。

## 判定手順
1. まずフォームのタイトル・説明・フィールド構成から、フォームの目的を把握する
2. 次に回答内容がそのフォームの目的に沿っているかを判断する
3. 以下の分類基準に基づいてラベルを決定する

これは Chain-of-Thought を簡易に強制するアプローチで、モデルが暴走しがちなのを防げます。特に Haiku 系・Flash 系などの軽量モデルを使う場合、判定手順を明示したほうが安定度が大きく変わります。


パターン2: 正例・負例をクラスごとに具体的に書き下す

問題

「営業メール = 自社サービスの売り込み」のような抽象的な定義だと、境界線の判定がブレます。

解法

クラスごとに、実際にありがちな文例を箇条書きで列挙する

### sales(営業・売り込み)
回答者が自社の商品・サービスを売り込んでいる回答:
- 「弊社では〜」「当社の〜」で自社サービスを紹介している
- フォームの目的と無関係な商品・サービスの提案や宣伝
- 「ご提案させてください」「導入実績○社」「無料トライアル」等の営業表現
- SEO対策、広告運用、人材紹介など外部サービスの売り込み
- 回答者が「提供する側」として書いている

ポイント:

  • 抽象語(「営業」)ではなく、実際の言い回し(「ご提案させてください」)を書く
  • リストの数を揃える。今回は legitimate 6パターン、sales 5パターン、suspicious は厳格化して別建て
  • 書き下すこと自体が、自分のドメイン知識の言語化にもなる

パターン3: few-shot examples は"混ざりやすい実例"を選ぶ

問題

few-shot サンプルを3つ入れるのは定番ですが、きれいすぎる例だけを入れると、モデルは境界ケースで迷ったままになります。

解法

実運用で混ざりやすいパターンを例示に選ぶ。正例1つ、明確な負例1つ、そして境界ケース(mix) を1つ入れるのが効果的でした。

## 具体例

回答: 「API連携について教えてください」
→ {"label":"legitimate","score":95,"reason":"サービスへの質問"}

回答: 「弊社はSEO対策の専門会社です。月額5万円から」
→ {"label":"sales","score":98,"reason":"外部SEOサービスの営業"}

回答: 「人材採用でお困りではありませんか?弊社の人材紹介サービスをご検討ください。ただ、御社のサービスにも興味があります」
→ {"label":"suspicious","score":65,"reason":"営業と問い合わせが混在"}

3つ目の suspicious 例が効きます。「営業的表現 + 質問」が混在するケースで、モデルに「こういうのは suspicious に寄せる」と学習させる意図です。


パターン4: 確信度スコア(0-100)を出力させて人間に渡す

Gemini_Generated_Image_2mhpyp2mhpyp2mhp.png

問題

ラベルだけ返されると、「モデルがどれくらい自信を持っているか」が分かりません。グレーゾーンの分類結果に対してユーザーが「本当?」と疑う根拠が作れません。

解法

score を必ず付ける。プロンプトで出力形式に score: 0-100 を含めて、「その分類の確信度です」と書き添えます。

export interface ClassificationResult {
  label: 'sales' | 'suspicious' | 'legitimate';
  score: number; // 0-100
  reason: string; // 20文字以内の判定理由
}

UI 側では以下の使い分けができます:

  • スコア90以上: 自動処理してOK
  • スコア60-90: 視覚的にラベル表示するが、フィルタ上は"確定"扱い
  • スコア60未満: UIで目立たせて、ユーザーに確認を促す

スコアはモデルの自己評価なので絶対の指標ではありませんが、ユーザーが違和感を持つための手がかりとして機能します。


パターン5: 手動修正を自動分類で上書きしない(source フラグ)

Gemini_Generated_Image_e827p7e827p7e827 (1).png

問題

ユーザーが誤判定を手動で直しても、翌日の再分類バッチで上書きされて元に戻る -- これが起きるとユーザーの信頼が一気に失われます。

解法

ラベルの出所を保持するフラグを持って、自動分類は手動行を絶対に上書きしないロジックにします。

DB スキーマ:

ALTER TABLE responses
  ADD COLUMN spam_label text,
  ADD COLUMN spam_score smallint,
  ADD COLUMN spam_label_source text CHECK (spam_label_source IN ('auto','manual')),
  ADD COLUMN spam_classified_at timestamptz;

実装:

// 自動分類の適用
await supabase
  .from('responses')
  .update({
    spam_label: result.label,
    spam_score: result.score,
    spam_label_source: 'auto',
    spam_classified_at: new Date().toISOString(),
  })
  .eq('id', responseId)
  .or('spam_label_source.is.null,spam_label_source.eq.auto'); // manual は対象外

// 手動修正
await supabase
  .from('responses')
  .update({
    spam_label: newLabel,
    spam_label_source: 'manual',
  })
  .eq('id', responseId);

ポイント:

  • 自動分類の UPDATE 側で spam_label_source != 'manual' を条件に入れる。これで手動行は永久に守られる
  • 手動 UPDATE 側では spam_label_source = 'manual' に切り替える
  • 監査ログ(別テーブル)にラベル変更履歴を残すと、チーム運用で誰がいつ何を直したか追える

ユーザーの体感としては「直したら直したまま」。これが本番の信頼を作ります。


パターン6: 「迷ったらX」デフォルトの置き方

問題

LLM は「分からないとき」にランダムに寄ります。false positive が致命的なユースケース(スパム判定、コンテンツモデレーション)で、このランダム性は許容できません。

解法

プロンプトに「迷ったらX」ルールを最後に明示する。X の選び方はユースケースで決まります。

  • スパム判定: 迷ったら legitimate(正当側)。false positive が致命的なので、確信が持てなければ通す。
  • コンテンツモデレーション: 迷ったら flag(人間レビューへ)。false negative が致命的なので、確信が持てなければ上げる。
  • インテント分類: 迷ったら other(その他)。無理に特定カテゴリに割り当てない。

プロンプトでの書き方:

## 重要ルール
- 迷ったら legitimate を選ぶ。正当な問い合わせの誤分類は営業の見逃しより有害
- 「問い合わせフォーム」等のフォームでは、サービスについての質問は当然 legitimate

「なぜそのデフォルトなのか」の理由まで書いておくとモデルが従いやすいです。「正当な問い合わせの誤分類は営業の見逃しより有害」という短い一文が、境界ケースでの判断を安定させます。


番外: 非同期実行でメインフロー(フォーム送信等)を絶対に壊さない

6パターンに入れるか迷いましたが、実装上これがないと本番で使えないので書いておきます。

LLM API はタイムアウト・レート制限・500系・ネットワーク失敗などいくらでも起きます。分類失敗がメインフローを壊してはいけない

Next.js 16 なら after() で簡単に組めます。

import { after } from 'next/server';

// ... ユーザーへのレスポンスを返す処理(分類は待たない)...

after(async () => {
  try {
    const { classifyResponse } = await import('@/lib/classification/engine');
    const result = await classifyResponse(context);
    if (result) {
      await saveLabel(responseId, result);
    }
  } catch (err) {
    console.error('classification failed:', err);
    // DB には何も書かない。label は null のまま保管。
  }
});
  • レスポンスを即返す(ユーザー体験に影響なし)
  • 例外は絶対に漏らさない
  • 分類失敗時は DB に何も書かない。label が NULL のまま保管される

ユーザー側から見ると「ラベルが付いていない回答もたまにある」状態になりますが、ラベルが付いていないこと自体がシグナルなので、フィルタで「未分類のみ」を出せば人間がキャッチアップできます。


まとめ -- 6パターンと、それぞれが解く課題

# パターン 解く課題
1 判定手順のステップ化 グレーゾーンで挙動が安定しない
2 正例・負例の具体化 抽象定義だと境界判定がブレる
3 few-shot に境界ケースを入れる きれいな例だけだと実運用で迷う
4 スコア出力の併記 ユーザーが違和感を持つ根拠がない
5 手動上書き保護(source フラグ) 再分類で手動補正が消える
6 「迷ったらX」デフォルトの明示 失敗の方向を制御できない

この6つを揃えると、LLM 分類は「精度99%ですって言いたいだけの機能」から「誤判定が出ても運用を壊さず、人間が直せば直ったままの機能」に変わります。

精度を上げることと、誤判定を許容可能にすることは、別の仕事です。本番投入するなら両方やってください。


参考

FORMLOVA は MCP クライアント(Claude/ChatGPT/Gemini 等)から操作できるフォームサービスです。この記事のパターンを実装に落として、全プラン無料で営業メール検知を提供しています。

1
2
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
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?