問い合わせ対応にAIを使うと、分類や返信下書きは速くなります。しかし、苦情、契約、返金、個人情報などを人間確認へ回した後、担当者と期限が決まっていなければ、対応はそこで止まります。
この記事では、GoogleスプレッドシートとGoogle Apps Script(GAS)で、人間確認が必要な問い合わせへ担当者と確認期限を付け、期限超過を検出する最小構成を作ります。AIは候補分類まで、人間は優先度、回答内容、送信可否を決めます。
この記事で作るもの
処理の流れは次のとおりです。
- フォームやメールから問い合わせを受付台帳へ記録する
- AIまたはルールが人間確認の候補理由を付ける
- GASが理由と優先度から確認期限を計算する
- 期限を過ぎた未確認行を
overdueへ変更する - 人間が原文と正本を確認し、返信または保留を決める
この実装は問い合わせ本文を外部AIへ送信せず、メール返信も自動送信しません。
シートの列を決める
シート名をinquiry_queueとし、次の列を用意します。
| 列 | 例 | 役割 |
|---|---|---|
inquiry_id |
INQ-20260714-001 |
変更しない識別子 |
received_at |
2026-07-14 09:10 |
受付日時 |
category |
refund |
問い合わせ分類 |
risk_reason |
返金条件の判断 |
人間確認へ回す理由 |
priority |
high |
期限計算用の優先度 |
owner |
cs-owner |
確認責任者 |
review_due_at |
2026-07-14 11:10 |
確認期限 |
status |
review_required |
現在の状態 |
decision |
空欄 | 人間の最終判断 |
decided_at |
空欄 | 判断日時 |
原文、氏名、メールアドレス、契約情報は別のアクセス制御された正本に置きます。この台帳へ必要以上に複製しないでください。
優先度別の期限を設定する
最初は設定をコードから分離します。
const CONFIG = {
SHEET_NAME: 'inquiry_queue',
REVIEW_HOURS: {
critical: 1,
high: 2,
normal: 8,
low: 24,
},
};
これは一般的なSLA(対応期限)の基準ではありません。営業時間、担当人数、顧客との約束に合わせ、人間が決める初期値です。夜間を除外する場合は、単純な時間加算ではなく営業日カレンダーを実装してください。
確認期限を付ける
review_requiredで期限が空の行だけを更新します。
function assignReviewDeadlines() {
const sheet = getQueueSheet();
const values = sheet.getDataRange().getValues();
if (values.length < 2) return;
const col = indexColumns(values[0]);
values.slice(1).forEach((row, index) => {
const status = String(row[col.status]).trim();
const currentDue = row[col.review_due_at];
if (status !== 'review_required' || currentDue) return;
const receivedAt = toDate(row[col.received_at]);
const priority = String(row[col.priority]).trim();
const hours = CONFIG.REVIEW_HOURS[priority];
if (!receivedAt || !hours) return;
const dueAt = new Date(receivedAt.getTime() + hours * 60 * 60 * 1000);
sheet.getRange(index + 2, col.review_due_at + 1).setValue(dueAt);
});
}
補助関数を追加します。
function getQueueSheet() {
const sheet = SpreadsheetApp.getActive().getSheetByName(CONFIG.SHEET_NAME);
if (!sheet) throw new Error(`Sheet not found: ${CONFIG.SHEET_NAME}`);
return sheet;
}
function indexColumns(header) {
const index = Object.fromEntries(
header.map((name, position) => [String(name).trim(), position]),
);
const required = [
'inquiry_id', 'received_at', 'priority', 'owner',
'review_due_at', 'status', 'decision', 'decided_at',
];
required.forEach((name) => {
if (!(name in index)) throw new Error(`Missing column: ${name}`);
});
return index;
}
function toDate(value) {
if (value instanceof Date && !Number.isNaN(value.getTime())) return value;
const parsed = new Date(value);
return Number.isNaN(parsed.getTime()) ? null : parsed;
}
期限超過を検出する
期限を過ぎた行をoverdueへ変更します。問い合わせ本文はログへ出しません。
function markOverdueReviews() {
const sheet = getQueueSheet();
const values = sheet.getDataRange().getValues();
if (values.length < 2) return;
const col = indexColumns(values[0]);
const now = new Date();
const overdueIds = [];
values.slice(1).forEach((row, index) => {
const status = String(row[col.status]).trim();
const dueAt = toDate(row[col.review_due_at]);
if (status !== 'review_required' || !dueAt || dueAt >= now) return;
sheet.getRange(index + 2, col.status + 1).setValue('overdue');
overdueIds.push(String(row[col.inquiry_id]));
});
console.log(JSON.stringify({ checkedAt: now.toISOString(), overdueIds }));
}
overdueは自動返信の許可ではなく、確認が遅れている合図です。重要案件ほど無確認の自動返信を止め、人間へ通知します。
完了時の整合性を検査する
resolvedなのに判断者や判断日時がない状態を検出します。
function validateResolvedRows() {
const sheet = getQueueSheet();
const values = sheet.getDataRange().getValues();
if (values.length < 2) return;
const col = indexColumns(values[0]);
const errors = [];
values.slice(1).forEach((row, index) => {
if (String(row[col.status]).trim() !== 'resolved') return;
if (!String(row[col.decision]).trim() || !toDate(row[col.decided_at])) {
errors.push({ row: index + 2, inquiryId: row[col.inquiry_id] });
}
});
if (errors.length) throw new Error(`Incomplete decisions: ${JSON.stringify(errors)}`);
}
判断内容には「返信した」「追加情報を依頼」「対応対象外」「責任者へ移管」など、後から追える短い値を使います。自由記述へ個人情報を貼り付けないでください。
トリガーと通知を設定する
時間主導トリガーでassignReviewDeadlinesとmarkOverdueReviewsを15分〜1時間ごとに実行します。実行間隔は実際の期限より十分短くします。
通知には次だけを含めます。
- 期限超過件数
inquiry_id- 担当者
- 台帳へのアクセス制御されたリンク
問い合わせ原文、メールアドレス、AIの要約全文をチャット通知へ転記しません。
テストケース
公開運用前に、ダミーデータで次を確認します。
-
highで期限が空: 受付から2時間後が設定される -
review_requiredで期限が過去:overdueになる -
resolvedで期限が過去: 状態は変わらない -
resolvedで判断日時が空: 検査が失敗する - 不明な優先度: 勝手に期限を設定せず、設定不足として残る
- タイムゾーン境界: シートとApps Scriptの設定が一致する
AIと人間の境界
AIやルールでできるのは、分類候補、優先度候補、要約候補、期限超過の検出までです。次は人間が行います。
- 苦情、契約、返金、金額、個人情報を含む案件の判断
- 問い合わせの正本と顧客履歴の確認
- 担当者と期限の最終決定
- 返信内容と送信先の確認
-
resolvedへの変更と判断記録
導入チェックリスト
- 人間確認へ回す条件を文章化した
- 優先度ごとの期限と営業時間の扱いを決めた
- すべての確認待ちに担当者がいる
- 期限超過が自動送信ではなく人間通知につながる
- 原文と個人情報を台帳やログへ複製していない
- 完了時に判断と判断日時を残す
- タイムゾーンと境界ケースをテストした
- 通知失敗時の確認手順を決めた
最初は過去20件を分類し、「人間確認へ回した後、誰がいつまでに見るか説明できるか」を確認してください。説明できなければ、返信生成を高度化する前にowner、review_due_at、statusの3列を整える方が効果的です。
