AIに問い合わせ返信の下書きを作らせると、レビュー担当者からの差し戻しが増えることがあります。差し戻しを担当者の修正で終わらせると、同じ不足が次の案件でも繰り返されます。
この記事では、GoogleスプレッドシートとGoogle Apps Script(GAS)で、差し戻し理由を FAQ・FORM・APPROVAL・SOURCE・SCOPE の改善キューへ分類します。AI APIの呼び出し、顧客への自動送信、FAQの自動公開は扱いません。
この記事で作るもの
- 返信レビューを1行で記録する
ReplyReviewシート - 差し戻し理由から改善先を決めるGAS
- 同じ案件を何度もキューへ積まない重複防止
- 改善担当者が確認できる
ReplyImprovementsシート
レビュー結果は人間が確定します。GASは記録の検証と改善候補の整理だけを行い、送信や設定変更は行いません。
まず差し戻し理由を5種類に固定する
自由記述だけでは、週次レビューで似た問題をまとめられません。最初は次の5種類に絞ります。
| reason_code | 典型例 | 戻し先 |
|---|---|---|
FAQ_GAP |
正しい回答の根拠がFAQにない | FAQの追加・更新候補 |
FORM_GAP |
受付時に必要な条件が取れていない | フォーム項目候補 |
APPROVAL_GAP |
誰が決めるかが記録されていない | 承認ルール候補 |
SOURCE_STALE |
参照したFAQや規程が古い | 正本・更新期限の確認 |
SCOPE_RISK |
契約・返金・苦情など対象外の内容 | 人間確認・対象外ルール |
reason_code は原因の候補であり、AIが決める最終結論ではありません。レビュー担当者が根拠を確認して選択します。
シートを準備する
ReplyReview の1行目に次の見出しを置きます。
review_id,inquiry_id,decision,reason_code,reason_note,source_refs,reviewer,reviewed_at,improvement_status
サンプルには実際の氏名、メールアドレス、契約番号、問い合わせ本文を入れません。
RV-001,INQ-001,revise,FORM_GAP,利用開始希望日の入力欄がない,FAQ-12,role_support,2026-09-13T09:30:00+09:00,open
decision は send・revise・hold の3値にします。send は担当者が送信前確認を終えた記録であり、自動送信の許可ではありません。
GASでレビュー行を検証する
const REVIEW_SHEET = 'ReplyReview';
const IMPROVEMENT_SHEET = 'ReplyImprovements';
const DECISIONS = new Set(['send', 'revise', 'hold']);
const REASONS = new Set([
'FAQ_GAP', 'FORM_GAP', 'APPROVAL_GAP', 'SOURCE_STALE', 'SCOPE_RISK',
]);
const REQUIRED_HEADERS = [
'review_id', 'inquiry_id', 'decision', 'reason_code', 'reason_note',
'source_refs', 'reviewer', 'reviewed_at', 'improvement_status',
];
function headerIndex_(headers) {
return Object.fromEntries(headers.map((name, index) => [String(name).trim(), index]));
}
function validateReviewRows() {
const sheet = SpreadsheetApp.getActive().getSheetByName(REVIEW_SHEET);
if (!sheet) throw new Error(`${REVIEW_SHEET} シートがありません`);
const values = sheet.getDataRange().getValues();
if (values.length < 2) return { checked: 0, errors: [] };
const index = headerIndex_(values[0]);
REQUIRED_HEADERS.forEach((name) => {
if (index[name] === undefined) throw new Error(`列がありません: ${name}`);
});
const errors = [];
values.slice(1).forEach((row, offset) => {
const line = offset + 2;
const get = (name) => String(row[index[name]] ?? '').trim();
if (!get('review_id') && !get('inquiry_id')) return;
if (!get('review_id')) errors.push(`${line}行目: review_id が空です`);
if (!get('inquiry_id')) errors.push(`${line}行目: inquiry_id が空です`);
if (!DECISIONS.has(get('decision'))) {
errors.push(`${line}行目: decision は send/revise/hold のいずれかです`);
}
const reason = get('reason_code');
if (get('decision') !== 'send' && !REASONS.has(reason)) {
errors.push(`${line}行目: 差し戻し・保留には有効な reason_code が必要です`);
}
if (get('decision') !== 'send' && !get('reason_note')) {
errors.push(`${line}行目: 差し戻し・保留には reason_note が必要です`);
}
if (!get('reviewer') || !get('reviewed_at')) {
errors.push(`${line}行目: reviewer と reviewed_at が必要です`);
}
});
return { checked: values.length - 1, errors };
}
validateReviewRows() の戻り値にエラーがあれば、改善キューを作る前に人が修正します。空欄を OTHER で埋めると、改善先が分からなくなるためです。
改善キューへ重複なく戻す
同じ inquiry_id について同じ reason_code が何度も記録されても、改善キューは1件にまとめます。レビュー履歴そのものは ReplyReview に残します。
const DESTINATION = {
FAQ_GAP: 'FAQ',
FORM_GAP: 'FORM',
APPROVAL_GAP: 'APPROVAL',
SOURCE_STALE: 'SOURCE',
SCOPE_RISK: 'SCOPE',
};
function createImprovementQueue() {
const result = validateReviewRows();
if (result.errors.length) throw new Error(result.errors.join('\n'));
const book = SpreadsheetApp.getActive();
const source = book.getSheetByName(REVIEW_SHEET);
const target = book.getSheetByName(IMPROVEMENT_SHEET)
|| book.insertSheet(IMPROVEMENT_SHEET);
const values = source.getDataRange().getValues();
const index = headerIndex_(values[0]);
const existing = target.getLastRow() < 2 ? []
: target.getRange(2, 1, target.getLastRow() - 1, 3).getValues();
const keys = new Set(existing.map((row) => `${row[1]}:${row[2]}`));
const additions = [];
values.slice(1).forEach((row) => {
const decision = String(row[index.decision] || '').trim();
const reason = String(row[index.reason_code] || '').trim();
if (decision === 'send' || !DESTINATION[reason]) return;
const inquiryId = String(row[index.inquiry_id] || '').trim();
const key = `${inquiryId}:${reason}`;
if (!inquiryId || keys.has(key)) return;
keys.add(key);
additions.push([
`IMP-${inquiryId}-${reason}`,
inquiryId,
reason,
DESTINATION[reason],
String(row[index.reason_note] || '').trim(),
'open',
new Date(),
]);
});
if (target.getLastRow() === 0) {
target.appendRow([
'improvement_id', 'inquiry_id', 'reason_code', 'destination',
'reason_note', 'status', 'created_at',
]);
}
if (additions.length) {
target.getRange(target.getLastRow() + 1, 1, additions.length, additions[0].length)
.setValues(additions);
}
return { added: additions.length };
}
改善キューの destination は作業先の候補です。FAQの公開、フォームの変更、承認ルールの変更は、それぞれの責任者が根拠と影響を確認してから行います。
運用で分ける3つの修正
- その場の修正: 今回の返信だけを直す。根拠があり、再発条件がない場合。
- 正本の修正: FAQや規程が古い、または説明不足の場合。正本の所有者が更新する。
- 入口の修正: フォームや受付項目が足りず、毎回同じ確認が必要な場合。入力項目と必須条件を見直す。
承認ルールや対象外範囲に問題がある場合は、AIのプロンプトを先に変えません。責任者・停止条件・記録先を決めてから、必要なら設定を更新します。
チェックリスト
- 差し戻し理由を自由記述だけで終わらせていない
-
source_refsから現行のFAQや規程へ戻れる - FAQ、フォーム、承認ルールの改善先を分けている
- 同じ問い合わせ・理由を改善キューへ重複登録しない
- 改善の承認者と見直し日を別途決めている
- 実顧客の本文や個人情報を改善ログへ複製していない
- AIに最終承認やFAQ公開をさせていない
まとめ
AI返信のレビューを一件ごとの修正で終わらせず、差し戻し理由を次の改善先へ戻すと、FAQ不足・フォーム不足・承認ルール不足を分けて扱えます。GASは候補の整理までに留め、正本の更新と社外送信は人が確定します。
Miraigentで業務を整理するときも、確認担当者へ「同じ差し戻しが起きたら、どの正本へ戻しますか」と尋ねるところから始めます。
