0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AI判断ログの見直し期限を管理するGoogleスプレッドシート+GAS

0
Posted at

AIの回答を承認した理由や、手動対応へ切り替えた理由を残す「判断ログ」は、FAQや承認ルールの改善に使えます。

ただし、記録するだけでは古い判断や不要な情報が溜まります。この記事では、GoogleスプレッドシートとGASで次の仕組みを作ります。

  • 保存区分から次回見直し日を計算する
  • 期限が来た行をレビューキューへ抽出する
  • keeprevisedelete を人間が決める
  • 削除候補を自動削除しない

AI APIは呼び出しません。AIへログを渡す前の、運用とデータ管理に絞ります。

作るもの

判断ログを記録する decision_log と、見直し対象を集める review_queue の2シートを作ります。

問い合わせフォームから受付ログへ集約し、FAQ候補・CRM項目・AI利用可否を同じ行で判断する流れ

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_queueopen 行を次の順に確認します。

  1. source_ref から権限のある元データを確認する
  2. summary に氏名、連絡先、契約情報が残っていないか見る
  3. 同じ判断が繰り返されていればFAQや承認ルールへ一般化する
  4. 人間が keeprevisedelete を選ぶ
  5. reviewed_atreview_owneraction_reason を記録する
  6. revise は改善バックログへ渡す
  7. delete は別の削除手順へ渡し、実行者と理由だけを監査ログへ残す

保持を選んだ時も無期限にせず、次の review_due_at を設定します。

時間主導トリガー

Apps Scriptの「トリガー」から fillMissingReviewDatesenqueueDueReviews を1日1回実行できます。

ただし、次はトリガーに含めません。

  • ログ本文の削除
  • 保存期間の延長
  • 担当者の変更
  • FAQや承認ルールの公開

これらは誤判定時の影響が大きいため、人間確認を残します。

公開前チェックリスト

  • 原文と判断ログを別にした
  • summary に個人情報を複製していない
  • 全行に保存区分、見直し日、責任者がある
  • 想定外の保存区分をエラーにした
  • レビューキューの重複を防いだ
  • GASは削除を自動実行しない
  • 保持・更新・削除の理由を人間が記録する
  • 固有の保存義務がある記録を別管理にした

まとめ

判断ログは、件数を増やすだけでは改善資産になりません。retention_classreview_due_atreview_owner を加え、期限が来た時に人間が keeprevisedelete を決める流れが必要です。

まず直近10件へ3列を追加し、期限切れのログを1件だけ手動で見直してください。自動削除を作る前に、何を根拠に残し、何を改善先へ移すかを決めるのが安全です。

MiraigentのAI導入診断でも、ログの有無だけでなく、誰がいつ見直し、どのルールへ戻すかを確認しています。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?