0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AI問い合わせ対応の分類ルールをGoogleスプレッドシートとGASで管理する

0
Posted at

この記事は、問い合わせ対応にAI返信下書きやAI要約を入れる前に、問い合わせ分類ルールをGoogleスプレッドシートとGASで管理するための実装メモです。

AI APIは呼びません。まず、問い合わせ本文をA/B/C/Dに分類し、AI下書きへ進めてよいか、人間確認へ戻すべきか、判断理由をログに残せる状態を作ります。

作るもの

Googleスプレッドシートに次の3枚を用意します。

  • inquiry_log: 問い合わせ受付ログ
  • classification_rules: 分類ルール
  • classification_tests: ルールのテストケース

GASで次の処理を作ります。

  • 問い合わせ本文を分類ルールに照合する
  • 一致した rule_id、分類、人間確認要否、理由を inquiry_log に書き込む
  • classification_tests をまとめて実行し、ルール変更の影響を確認する

問い合わせ分類ルールからAI下書き可否を判定する流れ

なぜ分類ルールを先に作るのか

問い合わせ対応をAI化する時、最初に作りたくなるのは返信プロンプトです。

しかし、現場で先に決めるべきなのは、どの問い合わせをAI下書きへ進め、どれを人間確認へ戻すかです。

たとえば、次の問い合わせを同じ扱いにすると危険です。

  • 営業時間を知りたい
  • 予約日を変更したい
  • 返金してほしい
  • 契約内容を確認したい
  • 個人情報を含む相談を送った
  • 強い苦情を書いている

AIが自然な文章を作れても、返金、契約、個人情報、苦情、専門判断を含む問い合わせは、人間が責任を持って確認する必要があります。

この記事では、AIの回答品質ではなく、AIへ渡す前の分類品質を扱います。

分類の考え方

最初は4分類で十分です。

class 意味 AI下書き 人間確認
A 一般FAQで回答できる 任意
B 個別状況の確認が必要 条件付き可 必須
C 金銭、契約、苦情、個人情報などを含む 不可 必須
D 分類不能、情報不足、未定義 不可 必須

大事なのは、分類名だけで終わらせず、判断理由も残すことです。

後から問い合わせログを見返した時に、「なぜAIへ渡したのか」「なぜ止めたのか」が分からないと、ルール改善ができません。

シート構成

inquiry_log

問い合わせを受け付けるログです。フォーム回答シートやCRMエクスポートから取り込む想定です。

inquiry_id received_at source customer_label message detected_rule_id inquiry_class ai_draft_allowed review_required decision_reason final_status
INQ-001 2026/06/25 10:00 form 新規 料金プランを知りたいです faq_basic_info A TRUE FALSE 一般FAQで回答できる可能性が高い classified

customer_label は、個人名やメールアドレスを入れず、運用上の区分だけを書く想定です。

classification_rules

分類ルールを管理します。

rule_id enabled priority class keywords ai_draft_allowed review_required owner reason
refund_contract_complaint TRUE 10 C 返金,解約,契約,クレーム,苦情 FALSE TRUE CS責任者 金銭、契約、苦情を含むため人間確認
sensitive_personal_context TRUE 20 C 住所,電話番号,本人確認,病気,診断 FALSE TRUE CS責任者 個人情報または専門判断に近い可能性
booking_or_availability TRUE 50 B 予約,空き,日程変更,キャンセル TRUE TRUE CS担当 個別状況の確認が必要
faq_basic_info TRUE 100 A 営業時間,所在地,アクセス,料金プラン TRUE FALSE CS担当 一般FAQで回答できる可能性が高い

priority は数字が小さいほど優先です。

たとえば「料金プランを知りたいが、契約変更も相談したい」という文章では、AよりCを優先して人間確認へ戻したいので、リスクの高いルールを上にします。

classification_tests

分類ルールを変えた時に、想定どおり判定されるか確認するシートです。

test_id message expected_rule_id expected_class expected_ai_draft_allowed expected_review_required result
T001 営業時間を教えてください faq_basic_info A TRUE FALSE PASS
T002 返金できますか refund_contract_complaint C FALSE TRUE PASS
T003 予約日を変更したいです booking_or_availability B TRUE TRUE PASS

ルールをシートで編集できるようにすると、変更ミスも起きます。

そのため、最低限のテストケースを持たせます。

GAS実装

次のコードは、classification_rules を読み込み、問い合わせ本文を分類します。

const SHEETS = {
  inquiries: 'inquiry_log',
  rules: 'classification_rules',
  tests: 'classification_tests',
};

const DEFAULT_DECISION = {
  ruleId: 'default_unclassified',
  className: 'D',
  aiDraftAllowed: false,
  reviewRequired: true,
  reason: '分類不能または情報不足',
};

function classifyInquiryLog() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const inquirySheet = ss.getSheetByName(SHEETS.inquiries);
  const rules = readClassificationRules_(ss.getSheetByName(SHEETS.rules));

  const range = inquirySheet.getDataRange();
  const values = range.getValues();
  const headers = values.shift();
  const col = buildColumnMap_(headers);

  const updates = values.map((row) => {
    const message = row[col.message];
    const decision = classifyMessage_(message, rules);
    return [
      decision.ruleId,
      decision.className,
      decision.aiDraftAllowed,
      decision.reviewRequired,
      decision.reason,
      'classified',
    ];
  });

  if (updates.length === 0) return;

  inquirySheet
    .getRange(2, col.detected_rule_id + 1, updates.length, 6)
    .setValues(updates);
}

function readClassificationRules_(sheet) {
  const values = sheet.getDataRange().getValues();
  const headers = values.shift();
  const col = buildColumnMap_(headers);

  return values
    .filter((row) => row.some((cell) => cell !== ''))
    .filter((row) => toBoolean_(row[col.enabled]))
    .map((row) => ({
      ruleId: String(row[col.rule_id]).trim(),
      priority: Number(row[col.priority] || 999),
      className: String(row[col.class]).trim(),
      keywords: String(row[col.keywords])
        .split(',')
        .map((keyword) => keyword.trim())
        .filter(Boolean),
      aiDraftAllowed: toBoolean_(row[col.ai_draft_allowed]),
      reviewRequired: toBoolean_(row[col.review_required]),
      reason: String(row[col.reason]).trim(),
    }))
    .sort((a, b) => a.priority - b.priority);
}

function classifyMessage_(message, rules) {
  const normalized = normalizeText_(message);
  if (!normalized) return DEFAULT_DECISION;

  const matched = rules.find((rule) =>
    rule.keywords.some((keyword) => normalized.includes(normalizeText_(keyword)))
  );

  if (!matched) return DEFAULT_DECISION;

  return {
    ruleId: matched.ruleId,
    className: matched.className,
    aiDraftAllowed: matched.aiDraftAllowed,
    reviewRequired: matched.reviewRequired,
    reason: matched.reason,
  };
}

function normalizeText_(value) {
  return String(value || '')
    .replace(/\\s+/g, '')
    .toLowerCase();
}

function buildColumnMap_(headers) {
  return Object.fromEntries(headers.map((header, index) => [String(header), index]));
}

function toBoolean_(value) {
  return value === true || String(value).toUpperCase() === 'TRUE';
}

ヘッダー名で列を扱う理由

GASでは getRange(row, column) のように列番号で処理できます。

ただ、運用中に列を追加すると、列番号固定のコードは壊れやすくなります。

この記事のコードでは、1行目のヘッダー名から列位置を取得しています。

function buildColumnMap_(headers) {
  return Object.fromEntries(headers.map((header, index) => [String(header), index]));
}

この方法なら、message や detected_rule_id などの列名を維持していれば、列順を少し変えても処理できます。

テストケースを実行する

classification_tests を使って、分類ルールが想定どおり動くか確認します。

function runClassificationTests() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const testSheet = ss.getSheetByName(SHEETS.tests);
  const rules = readClassificationRules_(ss.getSheetByName(SHEETS.rules));

  const values = testSheet.getDataRange().getValues();
  const headers = values.shift();
  const col = buildColumnMap_(headers);

  const results = values.map((row) => {
    const decision = classifyMessage_(row[col.message], rules);
    const passed =
      decision.ruleId === row[col.expected_rule_id] &&
      decision.className === row[col.expected_class] &&
      decision.aiDraftAllowed === toBoolean_(row[col.expected_ai_draft_allowed]) &&
      decision.reviewRequired === toBoolean_(row[col.expected_review_required]);

    return [passed ? 'PASS' : 'FAIL'];
  });

  if (results.length > 0) {
    testSheet.getRange(2, col.result + 1, results.length, 1).setValues(results);
  }
}

分類ルールを変えたら、先にテストを走らせます。

FAIL が出たら、AI下書きへ進める前にルールを見直します。

AI下書きへ進める条件

この実装では、AI APIへの送信は行いません。

実際にAI返信下書きへつなぐ場合でも、最低限この条件を満たす時だけにします。

  • ai_draft_allowed が TRUE
  • C分類、D分類ではない
  • review_required が TRUE の場合、人間確認なしに送信しない
  • 問い合わせ本文から連絡先、本人確認、契約番号などを除外している
  • 参照するFAQや商品説明が最新である
  • AIが作るのは送信用確定文ではなく下書きである

B分類は、AI下書きを作れても送信前確認を必須にするのが安全です。

日程変更、予約、見積もり、既存契約に関わる問い合わせは、文章だけ自然でも、事実確認が必要だからです。

週次で見る集計

分類ログが残ったら、週次で次の数を見ます。

指標 見る理由
A/B/C/Dの件数 問い合わせの性質を把握する
C分類の件数 AIへ渡さず止めた件数を把握する
D分類の件数 ルール未整備の量を見る
AI下書き可の件数 自動化候補の量を見る
人間確認必須の件数 担当者負荷を見積もる
final_status が need_rule_update の件数 ルール改善候補を拾う

AI導入では「AIが何件処理したか」だけを見がちです。

しかし、安全な運用では、AIへ渡さず止めた件数や、ルール更新が必要な件数も同じくらい重要です。

よくある失敗

リスクの高いルールを下に置く

priority が大きいほど後に評価される設計にしています。

返金、契約、苦情、個人情報のようなC分類は、FAQ系のA分類より先に評価します。

keywords を増やしすぎる

キーワードだけで完全分類しようとすると、誤判定が増えます。

最初は「止めたい条件」を優先します。

AI下書きへ進める条件を広げるより、人間確認へ戻す条件を明確にする方が安全です。

reason を空欄にする

reason がないログは、後から改善できません。

C分類だけではなく、金銭、契約、苦情を含むため人間確認 のように、担当者が読んで分かる理由を残します。

未分類をAIへ渡す

D分類は、分類不能または情報不足です。

ここをAIへ渡すと、曖昧な問い合わせほど自動化に流れてしまいます。

D分類は need_rule_update として見直す方がよいです。

ここでは自動化しないこと

この記事の実装では、次の処理は行いません。

  • AI APIへの送信
  • メールの自動返信
  • CRMへの自動登録
  • 個人情報の完全な検出
  • 法務、税務、医療などの専門判断
  • 返金、契約、クレーム対応の自動判断

これは、AIを使わないという意味ではありません。

AIを使う前に、AIへ渡してよい問い合わせと、止める問い合わせを説明できる状態にするという意味です。

まとめ

問い合わせ対応にAIを入れる前に、返信プロンプトだけを作ると、運用判断が属人化します。

先に分類ルールをスプレッドシートで管理し、GASで判定結果をログに残すと、AI下書きへ進める条件と人間確認へ戻す条件をチームで見直せます。

最初の完成状態は小さくて構いません。

  • A/B/C/D分類を決める
  • classification_rules を作る
  • inquiry_log に判定結果と理由を残す
  • classification_tests でルール変更を確認する
  • C/D分類をAIへ渡さない

AI問い合わせ対応を始める前に確認したいのは、次の一点です。

その問い合わせをAIに渡さない条件は、チーム内で同じ言葉になっていますか。


Miraigentでは、問い合わせ対応、FAQ、CRM、人間確認ルールなど、AI導入前に整える業務設計を整理しています。

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?