AIを業務に組み込んだ後に起きやすい事故は、モデルが突然壊れることだけではありません。担当者や入力項目が変わったのに、以前の判断ログを現在の手本として使い続けることも危険です。
本記事では、Googleスプレッドシートに保存したAI判断ログへ、更新が必要になる6つの変更イベントを記録し、期限が来た行をGASで抽出する最小構成を作ります。GASは候補の検出までに限定し、ログを現行ルールとして採用するか、保持・更新・停止のどれにするかは人間が決めます。
対象読者と完成状態
- AIで問い合わせ分類や返信案作成を試している管理者
- 判断ログをSheetsに残しているが、見直しの合図が決まっていない担当者
- GASで定期確認の候補を作りたい人
完成状態では、JUDGMENT_LOGS に記録した変更イベントから、見直し期限を過ぎたログを一覧化できます。AIが「このログを更新すべき」と確定する処理や、古いログの削除処理は作りません。
1. シートを2つに分ける
観測した事実と、人間が確定した運用判断を同じ行で上書きしないようにします。
JUDGMENT_LOGS
log_id, task_name, rule_version, decision, source_ref,
observed_at, review_due_at, change_event, status, owner
REVIEW_DECISIONS
review_id, log_id, action, reason, reviewer, reviewed_at, next_review_at
action は retain、update、retire の3値に固定します。decision はAIの出力ではなく、その時点で人間が採用した業務上の判断です。顧客本文、氏名、メールアドレス、認証情報はログへ複製せず、必要なら社内の正本を指す参照IDだけを保存します。
2. 更新を検討する6つのイベント
| change_event | 例 | まず確認するもの |
|---|---|---|
owner_changed |
担当者・確認者が交代した | 役割と代理確認 |
input_changed |
入力フォームや必須項目が変わった | 入力と出力の対応 |
scope_changed |
対象業務や対象顧客を広げた | 対象外と停止条件 |
ai_changed |
モデル、プロンプト、設定を変えた | 比較用の再検証 |
exception_changed |
契約・苦情などの扱いを変えた | 人間確認への戻し先 |
reaction_changed |
差し戻しや問い合わせ傾向が変わった | FAQ・ルールの改善先 |
6つは自動更新の命令ではありません。イベントが起きたら、該当ログを人間のレビューキューへ入れるためのトリガーです。
3. 変更イベントを登録する設定
イベント名を自由記述にすると、input-change と input_changed のような揺れで集計できなくなります。設定値をコード側で固定します。
const LOG_SHEET = 'JUDGMENT_LOGS';
const REVIEW_SHEET = 'REVIEW_DECISIONS';
const EVENT_TYPES = [
'owner_changed',
'input_changed',
'scope_changed',
'ai_changed',
'exception_changed',
'reaction_changed',
];
const ACTIONS = ['retain', 'update', 'retire'];
function assertOneOf_(value, values, label) {
if (!values.includes(value)) {
throw new Error(`${label} must be one of: ${values.join(', ')}`);
}
}
function registerChangeEvent(logId, eventType, sourceRef, owner) {
assertOneOf_(eventType, EVENT_TYPES, 'eventType');
if (!String(logId).trim() || !String(sourceRef).trim()) {
throw new Error('logId and sourceRef are required');
}
const sheet = SpreadsheetApp.getActive().getSheetByName(LOG_SHEET);
if (!sheet) throw new Error(`Sheet not found: ${LOG_SHEET}`);
sheet.appendRow([
Utilities.getUuid(),
logId,
'',
'',
sourceRef,
new Date(),
new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
eventType,
'review_required',
owner,
]);
}
sourceRef は変更を確認できる社内の文書IDやチケット番号を想定しています。顧客の本文をセルへ貼り付けるための引数ではありません。実際の運用では、フォーム送信や変更管理表からこの関数を呼び出し、入力値を検証してから登録します。
4. 見直し期限を過ぎたログを抽出する
列番号を直接参照せず、ヘッダーから列を作ります。シートに列を追加しても、別の値を誤って読むリスクを下げられます。
function headerMap_(headers) {
return Object.fromEntries(
headers.map((value, index) => [String(value).trim(), index])
);
}
function findReviewDueLogs(now = new Date()) {
const sheet = SpreadsheetApp.getActive().getSheetByName(LOG_SHEET);
if (!sheet) throw new Error(`Sheet not found: ${LOG_SHEET}`);
const values = sheet.getDataRange().getValues();
if (values.length < 2) return [];
const map = headerMap_(values.shift());
const required = ['log_id', 'review_due_at', 'change_event', 'status', 'owner'];
const missing = required.filter((key) => map[key] === undefined);
if (missing.length) throw new Error(`Missing columns: ${missing.join(', ')}`);
return values
.map((row, index) => ({row, rowNumber: index + 2}))
.filter(({row}) => {
const dueAt = row[map.review_due_at];
return row[map.status] === 'review_required'
&& dueAt instanceof Date
&& dueAt <= now
&& EVENT_TYPES.includes(String(row[map.change_event]));
})
.map(({row, rowNumber}) => ({
rowNumber,
logId: String(row[map.log_id]),
eventType: String(row[map.change_event]),
owner: String(row[map.owner]),
}));
}
この関数の返り値をChat通知やメール送信へ直結させる場合も、通知は「レビュー候補がある」という事実にとどめます。update や retire を自動入力したり、顧客への返信を送ったりはしません。
5. 人間のレビュー結果を保存する
レビュー担当者が根拠と次回確認日を残してから、対象ログの状態を更新します。
function saveReviewDecision(logId, action, reason, reviewer, nextReviewAt) {
assertOneOf_(action, ACTIONS, 'action');
if (![logId, reason, reviewer, nextReviewAt].every((value) => String(value).trim())) {
throw new Error('logId, reason, reviewer and nextReviewAt are required');
}
const sheet = SpreadsheetApp.getActive().getSheetByName(REVIEW_SHEET);
if (!sheet) throw new Error(`Sheet not found: ${REVIEW_SHEET}`);
sheet.appendRow([
Utilities.getUuid(), logId, action, reason, reviewer, new Date(), nextReviewAt,
]);
}
reason には「入力項目が変わったため、旧ログは保持するが現行手順の根拠にはしない」のように、次の担当者が判断を再現できる短い説明を残します。料金、契約、返金、苦情、個人情報の扱いをAIやGASに決めさせないことも明記します。
運用チェックリスト
- 変更イベントを6つの固定値から選べる
- ログの正本と判断結果の保存先を分けている
- 変更の根拠を参照IDで追える
- 見直し期限と担当者が空欄のままになっていない
-
retain / update / retireを人間が確定する - 個人情報、認証情報、顧客本文をログへ複製していない
- 更新後のFAQ、フォーム、CRM、手順書の戻し先を記録した
- 停止や手作業への復旧条件を確認した
まとめ
AI判断ログは、保存して終わりではありません。担当者、入力、対象範囲、AI設定、例外、現場の反応が変わった時に、古いログをそのまま手本にしないための見直しが必要です。
まずは直近10件のログに6つのイベントを付け、期限切れの候補を人間が retain / update / retire に分けてください。Miraigentでは、AIを使うことだけでなく、使わない・止める・更新する判断を次の担当者へ渡せる状態から業務設計を始めます。
