LLM API 無料で回す問い合わせスパム分類 — ルールベース×レビューキュー×Claude Code
フォーム経由のリードに混ざる営業・スパムを、LLM API を使わず regex ルールベースの3ラベルでさばく設計を扱います。核心は「有効リード判定を先に評価する」順序と、曖昧ケースの人間レビューをルールに昇華する学習ループです。ランニングコスト0円で実運用している一次パターンを動くコードで示します。
by Shin
「問い合わせフォームのスパム分類、LLM に投げるべきか?」——結論から言うと、まず投げない。この問題は決定的な regex ルールベースで大半をさばける構造をしていて、ランニングコストは0円、挙動は1行ずつ説明可能です。LLM の出番は判定そのものではなく、ルールを育てる側にあります。以下では、実運用している3ラベル設計(AUTO_EXCLUDE / NEEDS_REVIEW / VALID)と、人間レビューをルールに昇華する学習ループを、動くコードで書きます。
この記事は実装に寄せた [lab] 記事です。「どの業務に AI 投資するか・しないか」という線引きの判断そのものは スタートアップのCMO候補が知るべきAI活用領域 を参照してください。また本記事は、マーケ運用の自動化に n8n は必要か ― API × Claude Code で組み直す で書いた「LLM を判断ノードに使う」方式と逆の分担を取ります。同じ問い合わせ処理でも、問題の構造が違えば正解の分担が変わる——対で読むと設計判断の幅が見えるはずです。
なぜ LLM を既定にしないか — この問題の構造
問い合わせフォームのリード分類には、他のテキスト分類と決定的に違う性質が一つあります。誤りのコストが極端に非対称であることです。
- 偽陽性(有効リードをスパムと誤判定して捨てる): 商談1件がまるごと消えます。個人や小規模チームの問い合わせ数では、1件の重みが大企業の比ではありません。しかも「捨てたこと」に気づく手段が原理的にほぼない。
- 偽陰性(スパムを有効と誤判定して残す): 人間が一瞬見て消すだけ。コストは数秒です。
この非対称性が要件を規定します。必要なのは「賢い分類器」ではなく、有効リードを絶対に取りこぼさないという保証を、仕組みとして書き下せる分類器です。そして「絶対に〜しない」という保証は、確率的に動く LLM では原理的に書けません。決定的なルールなら書けます——「このパターンにマッチしたら、他に何がマッチしていても除外はしない」という1行の制御構造として。
判断軸は 機械学習を「使うべき場面 / 不要な場面」の見極め で書いた「ルールで書けるならルールで書く」の LLM 版です。この問題でルールベースが既定になる条件を挙げると:
- 入力が短い定型(問い合わせ本文+会社名程度。長文の文脈理解が不要)
- 安全側のラベルが定義できる(迷ったら NEEDS_REVIEW に落とせばよい。機械が白黒つける必要がない)
- 説明可能性・再現性が要る(「なぜこの問い合わせが除外されたか」に1行で答えたい。同じ入力は必ず同じ出力)
- 常時・高頻度で回る(スパムは有効リードの何倍も来る。1件ごとに API コストを払う対象として最悪の部類)
逆に言えば、この4条件が崩れる問題——多様で長い入力・誤りコストが対称・ゆらぎを許せる——なら、冒頭で対比した配管の記事のように軽量 LLM を判断ノードに置くのが正解になります。あちらの実測では分類1件あたり概ね1円未満〜数円。安いですが、スパムの山に対して払う必然性のあるコストではありません。
3ラベル設計 — AUTO_EXCLUDE / NEEDS_REVIEW / VALID
ラベルは3つです。2値(スパム/非スパム)にしないことが、この設計の前提になります。
| ラベル | 意味 | 後段の処理 |
|---|---|---|
VALID | 有効リードの証拠あり | 通常の問い合わせ対応フローへ |
NEEDS_REVIEW | 機械で決めない | レビューキューへ(人間が判定) |
AUTO_EXCLUDE | スパムの証拠あり・有効の証拠なし | 通知を止めて記録のみ |
そして核心は、ルールの中身よりも評価順序にあります。VALID パターンを SPAM パターンより先に評価し、有効リードの証拠が1本でもある限り AUTO_EXCLUDE には到達させない。前節の非対称コストを、そのまま制御構造に翻訳した形です。
// classify.mjs — ルールベース3ラベル分類器(汎用化した例)
// ラベル: VALID / NEEDS_REVIEW / AUTO_EXCLUDE
// VALID: 有効リードの証拠。1本でもヒットしたら AUTO_EXCLUDE には絶対に落とさない
const VALID_PATTERNS = [
{ id: "V01", re: /(相談|依頼|見積|お願いしたい|検討してい(ます|る))/, why: "依頼・相談の意思表示" },
{ id: "V02", re: /(予算|費用感|月額).{0,12}(万|円)/, why: "予算への具体的言及" },
{ id: "V03", re: /(弊社|自社)の.{0,20}(課題|データ|計測|広告)/, why: "自社の具体的文脈" },
{ id: "V04", re: /(打ち合わせ|ミーティング|お話).{0,10}(可能|お願い|したい)/, why: "商談の打診" },
];
// SPAM: 営業・スパムの証拠
const SPAM_PATTERNS = [
{ id: "S01", re: /(上位表示|検索1位).{0,10}(保証|お約束)/, why: "順位保証の営業" },
{ id: "S02", re: /(今だけ|限定価格|無料キャンペーン)/, why: "キャンペーン型の売り込み" },
{ id: "S03", re: /(一斉|リスト).{0,8}(送信|配信)/, why: "一斉送信の自己申告" },
{ id: "S04", re: /https?:\/\/\S+.{0,80}https?:\/\/\S+/s, why: "本文中に複数URL" },
{ id: "S05", re: /(bitcoin|forex|casino|loan)/i, why: "無関係な金融・ギャンブル語彙" },
];
export function classify(message) {
const valid = VALID_PATTERNS.filter((p) => p.re.test(message));
const spam = SPAM_PATTERNS.filter((p) => p.re.test(message));
// 評価順序が核心: VALID の証拠を先に確定させる。
// 偽陽性(有効リードを捨てる)のコスト >> 偽陰性(スパムが残る)なので、
// VALID 証拠がある限り AUTO_EXCLUDE には到達させない。
let label;
if (valid.length > 0 && spam.length === 0) label = "VALID";
else if (valid.length > 0) label = "NEEDS_REVIEW"; // 証拠が衝突 → 機械で決めない
else if (spam.length > 0) label = "AUTO_EXCLUDE";
else label = "NEEDS_REVIEW"; // 証拠ゼロも機械で決めない
return {
label,
matched: [...valid, ...spam].map((p) => `${p.id}:${p.why}`), // 説明可能性
};
}合成データ5件で動かした結果です(実際の問い合わせ文ではなく、動作確認用に作ったものです)。
OK VALID [V01:依頼・相談の意思表示, V02:予算への具体的言及, V03:自社の具体的文脈]
「広告計測の改善を相談したいです。弊社のGA4の計測に課題があり、予算は月20万円程度…」
OK AUTO_EXCLUDE [S01:順位保証の営業, S02:キャンペーン型の売り込み]
「SEO対策で検索1位を保証します!今だけ限定価格でご案内中です。」
OK AUTO_EXCLUDE [S04:本文中に複数URL, S05:無関係な金融・ギャンブル語彙]
「Best forex signals! http://… http://…」
OK NEEDS_REVIEW [] 「はじめまして。サイトを拝見しました。」 ← 証拠ゼロは決めない
OK NEEDS_REVIEW [V01, S02] 「SEOの改善を依頼したいのですが、今だけ社内予算が…」 ← 証拠衝突も決めない
5/5 passedmatched にヒットしたルール ID と理由を残すのが、地味ですが効きます。「なぜ除外されたか」「なぜレビューに回ったか」が常に1行で答えられる——LLM の reason 出力と違って、この説明は判定と同一の実体なので、説明と挙動がずれることがありません。
順序が逆だと何が起きるか
ありがちな実装は「スパムパターンに1本でもヒットしたら早期 return で除外」です。これに「SEO 営業が多いから」と /SEO/i のような広い網を足した瞬間、事故が起きます。
// ありがちな実装: スパムパターン先行・早期return
const NAIVE_SPAM_FIRST = [
/SEO/i, // 「SEO営業が多いから」と広く張ってしまった網
/(上位表示|検索1位).{0,10}(保証|お約束)/,
/(今だけ|限定価格)/,
];
function classifyNaive(message) {
for (const re of NAIVE_SPAM_FIRST) {
if (re.test(message)) return "AUTO_EXCLUDE"; // VALID証拠を見ずに捨てる
}
return "NEEDS_REVIEW";
}
const lead =
"SEOの内製化について相談したいです。弊社のオウンドメディアの計測に課題があり、" +
"予算は月30万円程度で検討しています。"; // 合成データ
console.log(classifyNaive(lead)); // → AUTO_EXCLUDE(正規の商談依頼を無言で捨てる)
console.log(classify(lead).label); // → VALID(VALID先行なら V01/V02/V03 が先に確定する)正規の商談依頼に「SEO」の語が入っているだけで、通知もされず消える——偽陽性の怖さは、この「無言で」の部分です。VALID 先行の設計なら、同じ雑な網を足しても有効リードは VALID か最悪 NEEDS_REVIEW に残ります。ルールを追加する人間のミスに対して安全側に壊れる構造にしておくことが、ルールを育て続ける前提になります。
レビューキュー — 曖昧を機械で決めない
NEEDS_REVIEW は「分類器の失敗」ではなく、設計上の正規の出口です。証拠がない・証拠が衝突している問い合わせを機械で白黒つけないと決めた以上、人間が判定する場所と手順をセットで用意します。
運用の入口は Claude Code のスラッシュコマンドにしています。「キューを一覧→1件ずつ判定→記録」が1コマンドで終わる形です。
<!-- .claude/commands/review-queue.md -->
---
description: 問い合わせの NEEDS_REVIEW キューを一覧し、1件ずつ判定して記録する
---
1. `node scripts/queue.mjs list` を実行し、NEEDS_REVIEW の一覧
(id・受信日時・本文冒頭・ヒットしたルールID)を表で見せる
2. 1件ずつ本文全文を表示し、私に VALID / SPAM の判定を聞く。
**あなた(AI)は判定案を出さない**
3. 私の判定を `node scripts/queue.mjs resolve <id> <VALID|SPAM> --note "<一言>"` で
判定ログ(JSONL)に記録する
4. 全件終わったら、今回と過去の判定ログを読み、共通パターンがあれば
「ルール昇華候補」として理由つきで提示する(採否は私が決める)ここに LLM との線引きを1本引いています。手順2で AI に判定案を出させないのは、精度の問題ではなく、人間の判定ログを次節の学習ループの教師データとして汚したくないからです。AI の案に人間が引っ張られた判定ログは、「人間ならどう判定するか」のログではなくなります。LLM の仕事は判定ではなく、一覧化・全文表示・記録・パターン提示というレビューの摩擦を削ること——レビューを速くするのであって、レビューを代行するのではありません。
学習ループ — レビュー結果をルールに昇華する
このシステムの本体は分類器ではなく、NEEDS_REVIEW 率を下げ続けるループの方です。レビューのたびに判定ログ(本文・人間の判定・一言メモ)が溜まる。定期的に——自分は月次で——このログを Claude Code に渡して、昇華候補を出させます。
判定ログ(review-log.jsonl)を読んで、次を提案してほしい:
1. 人間が繰り返し SPAM と判定しているものの共通パターン(regex 案・誤爆リスク込みで)
2. 人間が繰り返し VALID と判定しているものの共通パターン(同上)
3. 既存ルールのうち、最近ヒットしていない・判定と食い違っているもの
採用するかは私が決める。regex は広く張らず、根拠になったログ行を必ず添えること。ここが LLM の使い所です。判定という個々の意思決定は決定的ルールと人間に置き、パターン発見という帰納を LLM に任せます。問い合わせ配管の記事と分担が逆になっているだけで、「確率的な層を最小限に絞る」という設計原則は同じです。
採用したルールは、必ず過去の判定ログ全件に対する回帰テストを通してから足します。ここでも非対称性を実装します——fail の定義は「人間が VALID と判定した過去ログを、新ルールが AUTO_EXCLUDE に落とすこと」だけです。
// regression.mjs — ルール追加のたびに過去の人間判定ログと突き合わせる
import fs from "node:fs";
import { classify } from "./classify.mjs";
const log = fs
.readFileSync(process.argv[2] ?? "review-log.jsonl", "utf8")
.trimEnd()
.split("\n")
.map((l) => JSON.parse(l));
let regressions = 0;
for (const { message, human } of log) {
const { label } = classify(message);
// 退行の定義は非対称: 人間の VALID 判定を新ルールが AUTO_EXCLUDE に
// 落とすことだけを fail にする
if (human === "VALID" && label === "AUTO_EXCLUDE") {
regressions++;
console.error("REGRESSION:", message.slice(0, 40));
}
}
console.log(regressions === 0 ? `no regression (${log.length} cases)` : `${regressions} regression(s)`);
process.exit(regressions === 0 ? 0 : 1);判定ログがそのまま回帰テストのデータセットになる——レビューという運用コストが、テスト資産として複利で残る構造です。これがあるから、前節の「雑な網を足した人間のミス」も本番前に止まります。
KPI は NEEDS_REVIEW 率(レビューキュー行き÷全問い合わせ)に置いています。立ち上げ直後はルールが薄いので5割前後が NEEDS_REVIEW に落ちる想定から始め、半年で2割まで下げるのを目標ラインにした設計です。この率が下がる=人間のレビュー時間が減る、かつ下げ方は全部ルールとして説明可能に残ります。逆にこの率が下がりきった後に再上昇したら、スパムの型が変わったサインとして扱えます。
効果と限界 — 一次データ
実運用の一次パターンとして言えること(案件特定を避けるため、具体の件数は伏せてレンジと構造で書きます):
- ランニングコストは文字どおり0円です。分類は regex の評価だけで、フォームを受けるサーバー内で完結します。外部 API 呼び出しがないので、コストと同時に遅延と障害点も増えません。
- NEEDS_REVIEW 率は「初期5割→半年2割」を目標レンジとして設計し、この率自体を毎週見る運用です。重要なのは絶対値より傾きで、ループが回っていれば単調に下がり、止まっていれば横ばいになる——運用のサボりが数字に出る KPI として機能します。
- 上の分類器・回帰テストのコードは、本記事用に汎用化した上で合成データで実行し、全ケース通ることを確認しています(実運用のパターンそのものは載せていません。後述)。
限界も構造的に決まっています。
- 新型スパムへの初動は必ず遅れる。ルールベースは定義上、見たことのある型しか弾けません。新型は一度 NEEDS_REVIEW(または受信箱)を通過してからルール化されます。これは欠陥ではなくトレードオフで、「初動の遅れ」を許容する代わりに「有効リードの取りこぼしゼロ」を買っています。
- 日本語と英語でパターンは別物です。語彙も構文も違うので、辞書は言語別に持つ必要があります。英語圏スパムは URL 連打・金融語彙など機械的な型が多く、むしろルール化しやすい部類でした。
- ルールは増えすぎると腐る。月次の昇華と同じサイクルで「最近ヒットしていないルールの棚卸し」を回さないと、誰も意味を説明できない regex の地層ができます。ルールに
idとwhyを持たせているのは、この棚卸しのためでもあります。
まとめ — 「AI を使わない」も AI 活用の設計判断
同じ「問い合わせを分類する」でも、問題の構造で正解の分担が変わります。問い合わせ配管の記事の方式と並べると:
| LLM 判断ノード方式(問い合わせ配管の記事) | ルールベース方式(本稿) | |
|---|---|---|
| 判定の担い手 | 軽量 LLM(確率的) | regex ルール(決定的) |
| 判定コスト | 1件あたり概ね1円未満〜数円 | 0円 |
| 説明可能性 | reason を出力させる(判定とは別物) | ヒットしたルール ID(判定そのもの) |
| 誤りコストへの態度 | スキーマ検証で安全側に倒す | 評価順序で偽陽性を構造的に封じる |
| 変化への追従 | プロンプト・モデルの調整 | レビュー→昇華の学習ループ |
| LLM の役割 | 判定そのもの | レビューの摩擦削減とパターン発見 |
どちらが上という話ではありません。誤りコストが非対称で、安全側ラベルを定義でき、入力が定型で、常時回る——この条件が揃う問題では、判定は決定的に、LLM は改善ループ側に置く。条件が崩れたら判断ノード方式に切り替える。「LLM を使うか」を機能単位でなく問題の構造単位で決めることが、マーケター一人分の予算で AI 活用を回すときの、いちばん効く費用対効果の判断だと考えています。
どの業務を AI に任せ、どの業務は任せないか——その線引きの全体像は、対になるビジネス記事 スタートアップのCMO候補が知るべきAI活用領域 に書いています。