AIを使った問い合わせ分類や返信下書きのルールを変更した後は、通常ケースよりも例外ケースの扱いが難しくなります。
たとえば入力欄が増えたのに旧フォームからの問い合わせが残っている、確認者が変わったのに引き継ぎ前の担当者が設定されている、といった状態です。条件が足りない案件を通常処理へ流すと、AIの性能以前に「誰が判断を戻すのか」が分からなくなります。
この記事では、GoogleスプレッドシートとGoogle Apps Script(GAS)だけで、変更後の例外を記録し、`HOLD`(保留)・`MANUAL`(手動対応)・`REOPEN`(条件を満たして再開)の順に扱う最小構成を作ります。AI APIや顧客への自動送信は扱いません。
この記事で作るもの
一つの例外について、次の6項目をそろえた行を作ります。
- 変更後に何が不足・不一致だったか
- 例外の影響範囲
- 例外理由
- 手動で確認する担当者
- 手動対応へ戻す場所
- 再開条件と次回確認日
状態は次の3つだけに固定します。
| 状態 | 意味 | 次の処理 |
|---|---|---|
| `HOLD` | 情報が足りず、まだ判断できない | 不足情報を補い、担当者へ戻す |
| `MANUAL` | 例外として人が処理する | 正本を確認して人が対応する |
| `REOPEN` | 再開条件を確認できた | 変更後ルールで小さく再開する |
`REOPEN` はAIやGASが自動送信してよいという意味ではありません。再開後も、送信・契約・料金・苦情・個人情報に関わる判断は人が行います。
シートの準備
スプレッドシートに `ExceptionQueue` というシートを作り、1行目に次の見出しを置きます。
```text
exceptionId,createdAt,sourceId,changeId,impact,reason,owner,manualDestination,reopenCondition,status,nextReviewAt,reviewNote
```
例外の本文や顧客名をそのまま保存するのではなく、`sourceId` と社内の正本への参照を使います。記事のサンプルでは実データを使いません。
1. 変更IDと例外IDを分ける
変更が同じでも、例外は複数発生します。変更IDだけで管理すると、どの案件を確認したか追えなくなるため、行ごとに例外IDを発行します。
```javascript
const QUEUE_SHEET = 'ExceptionQueue';
const HEADERS = [
'exceptionId', 'createdAt', 'sourceId', 'changeId', 'impact', 'reason',
'owner', 'manualDestination', 'reopenCondition', 'status',
'nextReviewAt', 'reviewNote'
];
function makeId_(prefix) {
return prefix + '-' + Utilities.getUuid().slice(0, 8);
}
function ensureQueueSheet_() {
const book = SpreadsheetApp.getActive();
const sheet = book.getSheetByName(QUEUE_SHEET) || book.insertSheet(QUEUE_SHEET);
if (sheet.getLastRow() === 0) sheet.appendRow(HEADERS);
return sheet;
}
```
2. 例外を登録する
受付フォームや既存のCRMから渡す値は、例外を判断するための最小項目に限定します。`impact` は `LOW`、`MEDIUM`、`HIGH` のいずれかにします。
```javascript
function addException(input) {
const required = [
'sourceId', 'changeId', 'impact', 'reason',
'owner', 'manualDestination', 'reopenCondition', 'nextReviewAt'
];
const missing = required.filter((key) => !String(input[key] || '').trim());
if (missing.length > 0) {
throw new Error('必須項目が不足しています: ' + missing.join(', '));
}
if (!['LOW', 'MEDIUM', 'HIGH'].includes(input.impact)) {
throw new Error('impact は LOW / MEDIUM / HIGH のいずれかです');
}
const sheet = ensureQueueSheet_();
const row = [
makeId_('EX'), new Date(), String(input.sourceId), String(input.changeId),
String(input.impact), String(input.reason), String(input.owner),
String(input.manualDestination), String(input.reopenCondition),
'HOLD', String(input.nextReviewAt), ''
];
sheet.appendRow(row);
return row[0];
}
```
ここで `HOLD` にするのは、安全性を自動判定できるからではありません。必要な情報がそろうまで通常ルートへ流さないための、保守的な初期状態です。
3. 例外を3状態へ分類する
分類は、影響度と確認状態を別々に扱います。高影響の案件を、理由が書かれているだけで再開してはいけません。
```javascript
function decideStatus_(record) {
const impact = String(record.impact || '');
const reason = String(record.reason || '').trim();
const destination = String(record.manualDestination || '').trim();
const condition = String(record.reopenCondition || '').trim();
if (impact === 'HIGH' || !reason || !destination) return 'MANUAL';
if (!condition) return 'HOLD';
return 'REOPEN';
}
function reviewException(exceptionId) {
const sheet = ensureQueueSheet_();
const values = sheet.getDataRange().getValues();
const idCol = HEADERS.indexOf('exceptionId');
const statusCol = HEADERS.indexOf('status');
const noteCol = HEADERS.indexOf('reviewNote');
for (let i = 1; i < values.length; i += 1) {
if (values[i][idCol] !== exceptionId) continue;
const record = Object.fromEntries(HEADERS.map((key, index) => [key, values[i][index]]));
const status = decideStatus_(record);
sheet.getRange(i + 1, statusCol + 1).setValue(status);
sheet.getRange(i + 1, noteCol + 1).setValue(
'人間確認: ' + new Date().toISOString() + ' / changeId=' + record.changeId
);
return status;
}
throw new Error('exceptionId が見つかりません: ' + exceptionId);
}
```
実運用では、`reviewException` を時間トリガーで自動実行して状態を確定させないでください。GASは必須項目の検査とキュー化までにとどめ、状態の確定は確認者がシート上で行う構成を推奨します。上の関数は、確認者が対象行を選んで使う想定です。
4. 手動の戻し先を必ず記録する
「担当者へ戻す」だけでは、担当者が不在の時に止まります。`manualDestination` には、次に開く場所を具体的に書きます。
- `問い合わせ確認キュー / 変更ID-2026-10`
- `CRMの要確認ビュー / exceptionId`
- `運用責任者の確認フォーム / 受付番号`
個人のチャット名や口頭の依頼だけを戻し先にしないことが重要です。手動処理が終わった後、どの正本を見たかを `reviewNote` に残せる場所を選びます。
5. 再開条件を「確認できる文」にする
再開条件は「問題が解消した」ではなく、第三者が確認できる文にします。
悪い例:
```text
入力が直ったら再開
```
良い例:
```text
変更ID CHANGE-2026-10-03 の新フォーム回答で、必須3項目と確認者欄が埋まり、
確認者が正本FAQ v3.2を参照したことを記録できたら、テスト1件だけ再開する
```
それでも、条件を満たしたことの確認と、顧客へ送る最終判断は別です。再開は「変更後のルールで処理を試す」状態であり、「人間確認を省略する」状態ではありません。
6. 確認結果を運用へ戻す
毎週または変更後の決めた時刻に、次の順番で6項目を見直します。
- `sourceId` が重複していないか確認する
- 変更IDが現行の正本に存在するか確認する
- 影響度が `HIGH` のまま残っていないか確認する
- 手動の戻し先が実際に閲覧できるか確認する
- 再開条件を一つずつ照合する
- 対応結果と次回確認日を記録する
確認の結果、条件を満たさないものは `HOLD` に戻します。担当者が判断できないものを無理に `REOPEN` へ進めないことが、変更直後の事故を小さくします。
テスト用データで確認する
実際の顧客問い合わせや契約情報はテストに使わず、次のようなダミー行を作ります。
```javascript
function seedExample() {
return addException({
sourceId: 'sample-inquiry-001',
changeId: 'CHANGE-2026-10-03',
impact: 'MEDIUM',
reason: '変更後の必須項目が旧受付経路には存在しない',
owner: '業務確認担当',
manualDestination: '問い合わせ確認キュー / CHANGE-2026-10-03',
reopenCondition: '新受付経路の必須項目3つを確認者が照合する',
nextReviewAt: '2026-10-04'
});
}
```
最低限、次の結果を確認します。
- 必須項目が空なら行が追加されない
- 追加直後の状態が `HOLD` になる
- 高影響または戻し先なしは `MANUAL` になる
- 再開条件を人が確認したテスト行だけ `REOPEN` にできる
- 顧客本文、個人情報、認証情報がシートへ入っていない
自動化しない範囲
この構成で自動化するのは、例外の識別、必須項目の検査、確認キューへの記録です。次の処理は人へ戻します。
- 顧客への返信・送信
- 料金、契約、返金、苦情に関する判断
- 個人情報や機密情報をAIへ送る判断
- 変更後ルールの最終承認
- `REOPEN` 後の本番処理の拡大
GASのステータスは、判断の代わりではなく「どこで人が再確認するか」を残すための記録です。Miraigentでも、AIの導入範囲を広げる前に、例外が起きた時に人が戻れる場所を先に設計します。
まとめ
AI導入の変更後に例外が出たら、通常ルートへ押し戻すのではなく、次の6項目を一行で残します。
- 影響範囲
- 例外理由
- 確認者
- 手動の戻し先
- 記録先
- 再開条件
最初の状態を `HOLD` にし、条件が足りないものを `MANUAL` へ戻し、確認できたものだけ小さく `REOPEN` する。この順番なら、変更後に起きた一件を、次の担当者が使える運用記録へ変えられます。
