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?

GASで問い合わせの不足項目を検出し、AI返信の前に確認質問キューへ戻す

0
Posted at

問い合わせ返信の下書きをAIに任せる前に、入力不足を通常コードで止める仕組みを作ります。

この記事では Googleフォームの回答先スプレッドシートを想定し、Google Apps Script(GAS)で次を実装します。

  • 問い合わせ種別ごとの必須項目を検査する
  • 不足があればAIへ渡さず「確認待ち」シートへ追加する
  • 契約、返金、苦情などは必ず人間確認へ回す
  • 追加回答後に同じルールで再判定する

AIに空欄を推測させないことが目的です。顧客への自動送信は実装しません。

作るもの

処理結果を3状態に分けます。

状態 条件 次の処理
READY_FOR_DRAFT 必須項目がそろっている AI下書き候補へ進む
NEEDS_CLARIFICATION 必須項目が不足 確認質問キューへ追加
HUMAN_REVIEW 停止条件に該当 責任者が確認

流れは次の通りです。

フォーム回答
  ├─ 停止条件あり → HUMAN_REVIEW
  ├─ 必須項目不足 → NEEDS_CLARIFICATION
  └─ 必須項目あり → READY_FOR_DRAFT

追加回答を受領 → 元レコードを更新 → 同じルールで再判定

シート構成

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

Inquiries

内容
A inquiry_id
B received_at
C inquiry_type
D product_name
E occurred_at
F symptom
G requested_change
H desired_date
I body
J route_status

ClarificationQueue

内容
A queue_id
B inquiry_id
C missing_fields
D question_template_ids
E owner
F status
G created_at

HumanReview

内容
A review_id
B inquiry_id
C reason
D owner
E status
F created_at

キューへ氏名、電話番号、問い合わせ全文を複製しません。inquiry_id から権限管理された元レコードを参照します。

ルールをコードから分離する

問い合わせ種別と必須項目を設定として定義します。

const ROUTING_RULES = {
  product_issue: {
    requiredFields: ["product_name", "occurred_at", "symptom"],
    questionTemplates: {
      product_name: {
        id: "ASK_PRODUCT_NAME",
        text: "対象の製品・サービス名を教えてください。"
      },
      occurred_at: {
        id: "ASK_OCCURRED_AT",
        text: "問題が発生した日時を教えてください。"
      },
      symptom: {
        id: "ASK_SYMPTOM",
        text: "表示された内容や発生した状況を教えてください。"
      }
    }
  },
  account_change: {
    requiredFields: ["requested_change", "desired_date"],
    forceHumanReview: true
  }
};

const SENSITIVE_PATTERNS = [
  /返金/,
  /解約/,
  /契約変更/,
  /苦情/,
  /法的/
];

設定には実際の顧客情報を入れず、項目名、条件、承認済み質問だけを置きます。

不足項目を決定的に検出する

必須項目の空欄判定は生成AIではなく通常コードで行います。

function isBlank(value) {
  return value === null ||
    value === undefined ||
    String(value).trim() === "";
}

function findMissingFields(record, rule) {
  return rule.requiredFields.filter(
    (field) => isBlank(record[field])
  );
}

function hasSensitiveTopic(body) {
  const text = String(body || "");
  return SENSITIVE_PATTERNS.some((pattern) => pattern.test(text));
}

function routeInquiry(record) {
  const rule = ROUTING_RULES[record.inquiry_type];

  if (!rule) {
    return {
      status: "HUMAN_REVIEW",
      reason: "UNKNOWN_INQUIRY_TYPE"
    };
  }

  if (rule.forceHumanReview || hasSensitiveTopic(record.body)) {
    return {
      status: "HUMAN_REVIEW",
      reason: "SENSITIVE_OR_FORCED_REVIEW"
    };
  }

  const missingFields = findMissingFields(record, rule);
  if (missingFields.length > 0) {
    return {
      status: "NEEDS_CLARIFICATION",
      missingFields
    };
  }

  return {
    status: "READY_FOR_DRAFT",
    missingFields: []
  };
}

未知の問い合わせ種別をAIに分類させて先へ進めず、HUMAN_REVIEW に倒しています。

スプレッドシートの行をオブジェクトへ変換する

const INQUIRY_COLUMNS = [
  "inquiry_id",
  "received_at",
  "inquiry_type",
  "product_name",
  "occurred_at",
  "symptom",
  "requested_change",
  "desired_date",
  "body",
  "route_status"
];

function rowToRecord(row) {
  return INQUIRY_COLUMNS.reduce((record, key, index) => {
    record[key] = row[index];
    return record;
  }, {});
}

列番号を処理の各所へ直書きしないことで、項目追加時の修正漏れを減らします。本番ではヘッダー名から列位置を取得する方式にすると、さらに安全です。

確認質問キューへ追加する

function enqueueClarification(record, result) {
  const sheet = SpreadsheetApp
    .getActiveSpreadsheet()
    .getSheetByName("ClarificationQueue");

  const rule = ROUTING_RULES[record.inquiry_type];
  const templateIds = result.missingFields.map(
    (field) => rule.questionTemplates[field].id
  );

  sheet.appendRow([
    Utilities.getUuid(),
    record.inquiry_id,
    result.missingFields.join(","),
    templateIds.join(","),
    "support_team",
    "OPEN",
    new Date()
  ]);
}

質問文そのものではなくテンプレートIDを残すと、後から文面を変更しても「どの承認済みルールを使ったか」を追跡できます。

人間確認キューへ追加する

function enqueueHumanReview(record, result) {
  const sheet = SpreadsheetApp
    .getActiveSpreadsheet()
    .getSheetByName("HumanReview");

  sheet.appendRow([
    Utilities.getUuid(),
    record.inquiry_id,
    result.reason,
    "review_owner",
    "OPEN",
    new Date()
  ]);
}

契約、返金、苦情、分類不能などは、AI下書きを作る前に止めます。

フォーム送信時にルーティングする

function onFormSubmit(e) {
  const sheet = e.range.getSheet();
  const rowNumber = e.range.getRow();
  const row = sheet
    .getRange(rowNumber, 1, 1, INQUIRY_COLUMNS.length)
    .getValues()[0];

  const record = rowToRecord(row);
  const result = routeInquiry(record);

  if (result.status === "NEEDS_CLARIFICATION") {
    enqueueClarification(record, result);
  } else if (result.status === "HUMAN_REVIEW") {
    enqueueHumanReview(record, result);
  }

  const statusColumn = INQUIRY_COLUMNS.indexOf("route_status") + 1;
  sheet.getRange(rowNumber, statusColumn).setValue(result.status);
}

Apps Script の「フォーム送信時」インストール型トリガーへ onFormSubmit を登録します。トリガー作成者の権限で動くため、対象スプレッドシートとキューの閲覧権限を事前に確認してください。

AIへ渡すデータを最小化する

READY_FOR_DRAFT になっても、行全体をそのままAIへ送りません。用途に必要な項目だけを許可リストで選びます。

const AI_ALLOWED_FIELDS = [
  "inquiry_type",
  "product_name",
  "symptom"
];

function buildAiPayload(record) {
  return AI_ALLOWED_FIELDS.reduce((payload, field) => {
    payload[field] = record[field];
    return payload;
  }, {});
}

氏名、メールアドレス、電話番号、契約番号などは、返信下書きに不要なら送らない設計にします。利用するAIサービスの規約、保存設定、社内ルールも別途確認が必要です。

最小テスト

Apps Script エディタから次を実行し、ログを確認します。

function testRouteInquiry() {
  const cases = [
    {
      name: "不足なし",
      input: {
        inquiry_type: "product_issue",
        product_name: "Sample",
        occurred_at: "2026-07-24 09:00",
        symptom: "画面を開けない",
        body: "操作方法を確認したい"
      },
      expected: "READY_FOR_DRAFT"
    },
    {
      name: "発生日時が不足",
      input: {
        inquiry_type: "product_issue",
        product_name: "Sample",
        occurred_at: "",
        symptom: "画面を開けない",
        body: "操作方法を確認したい"
      },
      expected: "NEEDS_CLARIFICATION"
    },
    {
      name: "返金を含む",
      input: {
        inquiry_type: "product_issue",
        product_name: "Sample",
        occurred_at: "2026-07-24 09:00",
        symptom: "利用できない",
        body: "返金について確認したい"
      },
      expected: "HUMAN_REVIEW"
    }
  ];

  cases.forEach((testCase) => {
    const actual = routeInquiry(testCase.input).status;
    console.log(
      `${testCase.name}: ${actual === testCase.expected ? "PASS" : "FAIL"}`
    );
  });
}

最低でも、正常、不足、停止条件、未知種別、空白だけの値をテストします。

運用開始前チェックリスト

  • 必須項目を問い合わせ種別ごとに担当者が承認した
  • 分類不能時は HUMAN_REVIEW へ戻る
  • AIは不足情報を推測しない
  • 顧客への自動送信を行わない
  • キューへ不要な個人情報を複製しない
  • AIへ渡す項目を許可リストで限定した
  • トリガー実行者と閲覧権限を確認した
  • 追加回答後に同じルールで再判定する
  • 停止条件と質問テンプレートの見直し責任者を決めた

改善時に見る数字

確認質問キューは、止めるためだけではなく入口を直す材料にもなります。

  • 不足項目率: フォームで説明や選択肢を直すべき項目
  • 再質問率: 質問テンプレートが伝わりにくい可能性
  • 人間確認移行率: 停止条件が広すぎないか、狭すぎないか
  • 推測による修正件数: AIが事実を補っていないか
  • 確認完了までの時間: キューが滞留していないか

まず実際の問い合わせ10件で不足項目を数え、最も多い1項目だけを検出対象へ加えると、小さく試せます。

Miraigentでは、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?