問い合わせ返信の下書きを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導入前の無料診断で「必須項目」「停止条件」「人間確認者」「記録の戻し先」が決まっているかを整理しています。自動化率より先に、推測せず確認へ戻れる経路を作ることが重要です。