AIの回答を承認した理由や、手動対応へ切り替えた理由を残す「判断ログ」は、FAQや承認ルールの改善に使えます。
ただし、記録するだけでは古い判断や不要な情報が溜まります。この記事では、GoogleスプレッドシートとGASで次の仕組みを作ります。
- 保存区分から次回見直し日を計算する
- 期限が来た行をレビューキューへ抽出する
-
keep、revise、deleteを人間が決める - 削除候補を自動削除しない
AI APIは呼び出しません。AIへログを渡す前の、運用とデータ管理に絞ります。
作るもの
判断ログを記録する decision_log と、見直し対象を集める review_queue の2シートを作ります。
decision_log
↓ 保存区分から見直し日を計算
review_queue
↓ 人間が内容と元データを確認
keep / revise / delete
↓
FAQ・承認ルール・改善バックログへ反映
この実装における delete は削除の承認結果です。GASが本文を自動削除する処理は入れません。
シートの列設計
decision_log に次の列を作ります。
| 列名 | 例 | 用途 |
|---|---|---|
| decision_id | DEC-20260710-001 | 一意なID |
| created_at | 2026/07/10 10:00 | 記録日時 |
| decision_type | reply_review | 判断の種類 |
| summary | 返金を含むため手動対応 | 個人情報を除いた要約 |
| source_ref | INQ-1042 | 元データの参照ID |
| retention_class | short | 保存区分 |
| review_due_at | 2026/08/09 | 次回見直し日 |
| review_owner | CS責任者 | 人間の責任者 |
| review_status | pending | 見直し状態 |
| review_action | keep / revise / delete | 人間が決めた結果 |
| reviewed_at | 2026/08/08 | 見直し実施日 |
| action_reason | FAQへ一般化済み | 判断理由 |
問い合わせ原文や顧客情報を summary へコピーしないでください。原文はアクセス制御された正本に残し、判断ログには参照IDと一般化した理由だけを置きます。
保存区分を設定する
今回は3区分に絞ります。
const REVIEW_DAYS = Object.freeze({
short: 30,
standard: 90,
policy: 180,
});
-
short: 一時的な例外や確認待ち -
standard: 繰り返す判断例やFAQ候補 -
policy: 入力禁止、停止条件、承認境界
この日数は法的な保存期間を示すものではありません。契約、会計、人事、医療など固有の保存要件がある記録は別管理にし、社内規程や専門家の判断を優先してください。
初期シートを作る
Apps Scriptへ次のコードを追加します。
const LOG_HEADERS = [
'decision_id', 'created_at', 'decision_type', 'summary', 'source_ref',
'retention_class', 'review_due_at', 'review_owner', 'review_status',
'review_action', 'reviewed_at', 'action_reason',
];
const QUEUE_HEADERS = [
'queued_at', 'decision_id', 'decision_type', 'summary', 'source_ref',
'review_due_at', 'review_owner', 'queue_status',
];
function setupDecisionLogSheets() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
ensureSheet_(ss, 'decision_log', LOG_HEADERS);
ensureSheet_(ss, 'review_queue', QUEUE_HEADERS);
}
function ensureSheet_(ss, name, headers) {
const sheet = ss.getSheetByName(name) || ss.insertSheet(name);
if (sheet.getLastRow() === 0) sheet.appendRow(headers);
return sheet;
}
見直し日を計算する
新しいログを追加する時、保存区分から期限を計算します。
function calculateReviewDueAt_(createdAt, retentionClass) {
const days = REVIEW_DAYS[String(retentionClass || '').toLowerCase()];
if (!days) throw new Error(`Unknown retention_class: ${retentionClass}`);
const due = new Date(createdAt);
if (Number.isNaN(due.getTime())) throw new Error('Invalid created_at');
due.setDate(due.getDate() + days);
return due;
}
function fillMissingReviewDates() {
const sheet = SpreadsheetApp.getActive().getSheetByName('decision_log');
if (!sheet || sheet.getLastRow() < 2) return;
const range = sheet.getDataRange();
const values = range.getValues();
const headers = values[0];
const col = Object.fromEntries(headers.map((name, i) => [name, i]));
values.slice(1).forEach((row, index) => {
if (!row[col.decision_id] || row[col.review_due_at]) return;
const due = calculateReviewDueAt_(
row[col.created_at],
row[col.retention_class],
);
sheet.getRange(index + 2, col.review_due_at + 1).setValue(due);
});
}
retention_class が空欄または想定外ならエラーで止めます。勝手に既定値へ寄せるより、入力ミスを人間へ返す方が安全です。
期限が来たログをレビューキューへ送る
期限超過かつ pending の行だけを抽出します。同じIDを重複登録しないよう、既存キューも確認します。
function enqueueDueReviews() {
const ss = SpreadsheetApp.getActive();
const logSheet = ss.getSheetByName('decision_log');
const queueSheet = ss.getSheetByName('review_queue');
if (!logSheet || !queueSheet) throw new Error('先にsetupDecisionLogSheetsを実行してください');
const logValues = logSheet.getDataRange().getValues();
const headers = logValues.shift();
const col = Object.fromEntries(headers.map((name, i) => [name, i]));
const today = new Date();
const queueValues = queueSheet.getDataRange().getValues();
const queuedIds = new Set(queueValues.slice(1).map((row) => String(row[1])));
logValues.forEach((row) => {
const id = String(row[col.decision_id] || '');
const due = new Date(row[col.review_due_at]);
const status = String(row[col.review_status] || '').toLowerCase();
if (!id || queuedIds.has(id) || status !== 'pending') return;
if (Number.isNaN(due.getTime()) || due > today) return;
queueSheet.appendRow([
new Date(), id, row[col.decision_type], row[col.summary],
row[col.source_ref], due, row[col.review_owner], 'open',
]);
queuedIds.add(id);
});
}
この関数は候補を並べるだけです。保持、更新、削除の判断は行いません。
人間の見直し手順
週に一度、review_queue の open 行を次の順に確認します。
-
source_refから権限のある元データを確認する -
summaryに氏名、連絡先、契約情報が残っていないか見る - 同じ判断が繰り返されていればFAQや承認ルールへ一般化する
- 人間が
keep、revise、deleteを選ぶ -
reviewed_at、review_owner、action_reasonを記録する -
reviseは改善バックログへ渡す -
deleteは別の削除手順へ渡し、実行者と理由だけを監査ログへ残す
保持を選んだ時も無期限にせず、次の review_due_at を設定します。
時間主導トリガー
Apps Scriptの「トリガー」から fillMissingReviewDates と enqueueDueReviews を1日1回実行できます。
ただし、次はトリガーに含めません。
- ログ本文の削除
- 保存期間の延長
- 担当者の変更
- FAQや承認ルールの公開
これらは誤判定時の影響が大きいため、人間確認を残します。
公開前チェックリスト
- 原文と判断ログを別にした
-
summaryに個人情報を複製していない - 全行に保存区分、見直し日、責任者がある
- 想定外の保存区分をエラーにした
- レビューキューの重複を防いだ
- GASは削除を自動実行しない
- 保持・更新・削除の理由を人間が記録する
- 固有の保存義務がある記録を別管理にした
まとめ
判断ログは、件数を増やすだけでは改善資産になりません。retention_class、review_due_at、review_owner を加え、期限が来た時に人間が keep、revise、delete を決める流れが必要です。
まず直近10件へ3列を追加し、期限切れのログを1件だけ手動で見直してください。自動削除を作る前に、何を根拠に残し、何を改善先へ移すかを決めるのが安全です。
MiraigentのAI導入診断でも、ログの有無だけでなく、誰がいつ見直し、どのルールへ戻すかを確認しています。
