Contact Form 7の「営業フォーム」をAIで自動ブロックする方法(OpenAI API)

おはようございます!マネジメントオフィスいまむらの今村敦剛です。

WordPressでフォーム経由の営業メールにめちゃめちゃうんざりしてました。そこでContact Form 7(以下CF7)の問い合わせ文をOpenAI APIでチェックして、営業・勧誘と判断したら送信を止める仕組みを作りました。

Loading table of contents...

どんな仕組みなのかをざっくり解説

流れはシンプルです。

  1. WordPressで作成したCF7のフォームで、ユーザーが記入して送信
  2. 送信内容をOpenAIに渡して、「営業」「スパム」「普通の問い合わせ」を判定してもらう
  3. 営業やスパムなら、送信を止めてエラーメッセージを出す
  4. 普通の問い合わせなら、そのまま送信する

これをfunction.phpに追記するだけで実現しようという試みです。

止めたくない「普通の問い合わせ」をどうやって通すか

止めるのは簡単なのですが、もっとも問題なのは、止めたくない「普通の問い合わせ」をどうやって通すかということですね。自社のサービスに興味がある人の問い合わせを止めてしまうと、機会損失になりますからね。

そこで今回の仕組みでは、次のようなルールをAIに教えます。

  • ユーザーが、「自社のサービス」について質問しているなら normal(普通の問い合わせ)に分類
  • ユーザーが「ユーザーのサービス」を売り込んでいるなら sales(営業)に分類
  • 詐欺っぽい・意味不明なら spam(スパム)に分類

そのうえで、AIに渡すプロンプト内で自社のサービスを明示します。そうするとAIは判断に迷いにくくなりますよね。

今回の試した内容では、自社(マネジメントオフィスいまむら)のサービスを次の3つに決めて、プロンプト内に収めるようにしました。

  • ISOマネジメントシステムのコンサル・研修
  • カーボンニュートラルのコンサル・研修
  • AIを使った業務改善(効率化・最適化・整理)のコンサル

こう書いておくと、AIが「それは自社サービスへの問い合わせだから normal」「自社のサービスでない話をしているからsalesだ」と判断しやすくなります。

こういうコードを子テーマのfunction.phpに貼るだけ

これを実現するために作成したコードを以下に紹介します。こういうコードを子テーマのfunction.phpに貼るだけで実現できるのですが、試すときは必ずバックアップをとってからにしてくださいね。(function.phpの修正はかなりのリスクを伴いますので注意が必要です)

<?php
/**
 * Contact Form 7 × OpenAI 判定で営業/スパムをブロック(最小サンプル)
 * 貼り付け先:子テーマ functions.php
 */

define('OPENAI_API_KEY', 'sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'); // 実キーを設定(絶対に公開しない)
define('OPENAI_MODEL', 'gpt-4o-mini');
define('OPENAI_TIMEOUT_SECONDS', 15);

// CF7フィールド名(あなたのフォームに合わせてください)
define('CF7_FIELD_COMPANY', 'your-subject'); // 会社名・屋号
define('CF7_FIELD_MESSAGE', 'your-message');

// API失敗時の動き:まずは fail_open 推奨(誤ブロックを避ける)
define('OPENAI_FAIL_MODE', 'fail_open'); // fail_open / fail_closed

function cf7_ai_classify($content) {
  $endpoint = 'https://api.openai.com/v1/chat/completions';

  $system_prompt = "You are a strict anti-spam filter for a Japanese company's contact form. Output must be JSON only.";

  // ★ここが肝:自社サービス(OUR service/product)を定義して、そこへの問い合わせは normal を強制する
  $user_prompt = <<<EOT
Classify the message into exactly one of: "sales", "spam", "normal".

OUR service/product (this company offers ONLY these):
A) Consulting and training on ISO management systems
B) Consulting and training on carbon neutral / decarbonization
C) Consulting on making business effective/optimized/organized by using AI (AI-based business improvement / DX)

CRITICAL DECISION RULES (must follow):
1) If the sender is asking about OUR service/product (A/B/C) such as pricing, schedule, details, quotation, contract, how to join, training contents, consulting scope, delivery method, support after training, then inquiry_type MUST be "normal" even if words like "seminar" appear.
2) "sales" means the sender is trying to sell/promote THEIR service/product to us (unsolicited marketing, SEO offers, outsourcing proposal, partnership pitch, seminar invitation to THEIR event, etc).
3) "spam" is scams/phishing/malware, nonsense, or auto-generated junk.
4) If uncertain between "normal" and "sales", choose "normal" unless it is clearly promoting something to us.

score definition:
- score is confidence that the message is "sales" OR "spam" (0=safe normal, 100=definitely sales/spam).

Return JSON only:
{
  "score": 0-100,
  "inquiry_type": "sales" or "spam" or "normal",
  "reason": "Explanation in Japanese"
}

Content:
{$content}
EOT;

  $payload = [
    'model' => OPENAI_MODEL,
    'temperature' => 0,
    'messages' => [
      ['role' => 'system', 'content' => $system_prompt],
      ['role' => 'user',   'content' => $user_prompt],
    ],
    'response_format' => ['type' => 'json_object'],
  ];

  $resp = wp_remote_post($endpoint, [
    'headers' => [
      'Authorization' => 'Bearer ' . OPENAI_API_KEY,
      'Content-Type'  => 'application/json',
    ],
    'body'    => wp_json_encode($payload),
    'timeout' => OPENAI_TIMEOUT_SECONDS,
  ]);

  if (is_wp_error($resp)) return ['ok'=>false, 'error'=>$resp->get_error_message()];
  $code = (int) wp_remote_retrieve_response_code($resp);
  $raw  = (string) wp_remote_retrieve_body($resp);
  if ($code !== 200) return ['ok'=>false, 'error'=>"HTTP {$code}", 'raw'=>$raw];

  $decoded = json_decode($raw, true);
  $content_json = $decoded['choices'][0]['message']['content'] ?? '';
  $json = json_decode($content_json, true);
  if (!is_array($json)) return ['ok'=>false, 'error'=>'Invalid JSON', 'raw'=>$content_json];

  return [
    'ok' => true,
    'inquiry_type' => $json['inquiry_type'] ?? 'normal',
    'score' => (int)($json['score'] ?? 0),
    'reason' => $json['reason'] ?? '',
  ];
}

function cf7_ai_should_block($judge) {
  // OpenAIが成功したら、typeを最優先で判断する
  if (!empty($judge['ok'])) {
    $type = (string)($judge['inquiry_type'] ?? 'normal');
    if ($type === 'sales' || $type === 'spam') return true;
    return false; // normal は通す(scoreで基本ブロックしない)
  }

  // API失敗時:まずは通す(fail_open)
  if (OPENAI_FAIL_MODE === 'fail_open') return false;

  // fail_closed にしたい場合は、ここでフォールバック判定を入れる
  return false;
}

// CF7のvalidateで送信を止める
add_filter('wpcf7_validate', function($result, $tags){
  if (!class_exists('WPCF7_Submission')) return $result;
  $submission = WPCF7_Submission::get_instance();
  if (!$submission) return $result;

  $data = $submission->get_posted_data();
  $company = trim((string)($data[CF7_FIELD_COMPANY] ?? ''));
  $message = trim((string)($data[CF7_FIELD_MESSAGE] ?? ''));

  $content = "Company: {$company}\nMessage:\n{$message}\n";
  $judge = cf7_ai_classify($content);

  if (cf7_ai_should_block($judge)) {
    $result->invalidate(CF7_FIELD_MESSAGE, '営業・勧誘またはスパムの可能性が高いため送信できませんでした。');
  }
  return $result;
}, 20, 2);

使ってみて分かった注意点

この仕組みを入れて、過去に実際にきた普通の問い合あわせ(normal)と営業メール(sales)をフォームに入力し、正しく仕訳ができるかを確認しました。体感としてはかなりの精度で仕訳してくれますが、「完璧にゼロ」にはなりませんね。まあ、完璧を求めると機会損失になるリスクも高まるので、「まあまあ減ったな」程度で押さえておくほうがいいのかもしれません。

またコード作成で気をつけた点は、APIが落ちたときにどうするかです。その時はfail_open(APIがダメなら通す)で運用して、誤ブロックの事故が起きないようにしておくのが安全かな?と思います。

というわけで、OpenAI APIを使うと、「この文章は売り込みか?それとも本当の問い合わせか?」を文章の意味で見分けられるようになりました。そして精度を上げるコツは、「自社サービス(OUR service/product)をはっきり定義しておく」ことだ、ということですね。

この記事を書いた人
代表取締役 今村 敦剛

中小企業診断士/審査員(ISO9001, 14001, 45001)/日本心理学会認定心理士