AIを問い合わせ対応、FAQ候補作成、CRMメモ整理に使い始めると、最初は「何に使ったか」を残すだけでも役に立ちます。
ただ、AI運用ログは残すだけでは改善につながりません。
- 同じ理由で人間確認に戻っている
- CRMの情報不足で毎回止まっている
- FAQにすれば減らせる質問が残っている
- 承認者や確認期限が曖昧なままになっている
この記事では、GoogleスプレッドシートとGASで AI運用ログを週次レビューし、FAQ・CRM・承認ルールへ戻す最小実装 を作ります。
AI APIの呼び出しや顧客への自動送信は扱いません。ログを改善サイクルへ戻すための実務向けテンプレートです。
作るもの
スプレッドシートに3つのシートを用意します。
-
ai_ops_log: AIを使った用途、止めた理由、改善先を残す -
weekly_review: 週次レビューで見る集計を残す -
improvement_backlog: FAQ、CRM、承認ルール、フォーム、テンプレートの改善タスクへ変換する
流れは次の通りです。
AI運用ログ
↓
人間確認理由と改善先を固定値で記録
↓
週次レビューで偏りを見る
↓
改善バックログへ追加
↓
FAQ / CRM / 承認ルール / フォーム / テンプレートを直す
ログには、問い合わせ本文や顧客名をそのまま保存しません。後から改善できる分類だけを残します。
ai_ops_log の列
まず、ai_ops_log シートに次の列を作ります。
| 列名 | 例 | 用途 |
|---|---|---|
| log_id | OPS-20260703-001 | ログID |
| logged_at | 2026/07/03 11:17 | 発生日 |
| task_type | reply_draft | AIを使った用途 |
| source_channel | contact_form | 元の導線 |
| data_level | masked | AIへ渡した情報の扱い |
| human_review_reason | missing_context | 人間確認へ戻した理由 |
| blocked_reason | missing_crm_status | 止まった理由 |
| improvement_target | crm | 改善先 |
| owner | ops_lead | 確認担当 |
| status | review_waiting | 現在状態 |
| review_note | 既存顧客か不明 | 判断メモ |
human_review_reason と improvement_target は自由記述にしすぎない方が集計しやすくなります。
固定値
最初はこのくらいで十分です。
const HUMAN_REVIEW_REASONS = [
'personal_data',
'price_or_contract',
'complaint_or_risk',
'missing_context',
'public_output',
'low_confidence',
'other',
];
const IMPROVEMENT_TARGETS = [
'faq',
'crm',
'approval_rule',
'form',
'template',
'no_action',
];
ログを週次で見る目的は、AIの出力を採点することではありません。
「なぜ人間確認に戻したか」「どの情報が足りなかったか」「次にどこを直すか」を同じ粒度で残すことです。
セットアップ用GAS
スプレッドシートで 拡張機能 -> Apps Script を開き、次のコードを貼り付けます。
const SHEETS = {
log: 'ai_ops_log',
review: 'weekly_review',
backlog: 'improvement_backlog',
};
const LOG_HEADERS = [
'log_id',
'logged_at',
'task_type',
'source_channel',
'data_level',
'human_review_reason',
'blocked_reason',
'improvement_target',
'owner',
'status',
'review_note',
];
const REVIEW_HEADERS = [
'week_start',
'total_count',
'missing_context_count',
'personal_data_count',
'contract_count',
'faq_count',
'crm_count',
'approval_rule_count',
'blank_blocked_reason_count',
'next_action_hint',
];
const BACKLOG_HEADERS = [
'backlog_id',
'created_at',
'source_week',
'target',
'change_summary',
'owner',
'due_date',
'done_definition',
'status',
];
function setupAiOpsLogFeedbackLoop() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
ensureSheet_(ss, SHEETS.log, LOG_HEADERS);
ensureSheet_(ss, SHEETS.review, REVIEW_HEADERS);
ensureSheet_(ss, SHEETS.backlog, BACKLOG_HEADERS);
}
function ensureSheet_(ss, name, headers) {
const sheet = ss.getSheetByName(name) || ss.insertSheet(name);
const current = sheet.getRange(1, 1, 1, headers.length).getValues()[0];
const isEmpty = current.every((value) => value === '');
if (isEmpty) {
sheet.getRange(1, 1, 1, headers.length).setValues([headers]);
sheet.setFrozenRows(1);
}
}
ログを追加する関数
AIを使った後、分類だけをログへ追加します。
function addAiOpsLog(entry) {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName(SHEETS.log);
if (!sheet) throw new Error('setupAiOpsLogFeedbackLoop() を先に実行してください');
const humanReviewReason = normalizeHumanReviewReason_(entry.humanReviewReason);
const improvementTarget = normalizeImprovementTarget_(entry.improvementTarget);
sheet.appendRow([
createLogId_(),
entry.loggedAt || new Date(),
entry.taskType || 'other',
entry.sourceChannel || 'unknown',
entry.dataLevel || 'internal',
humanReviewReason,
entry.blockedReason || '',
improvementTarget,
entry.owner || 'unassigned',
entry.status || 'review_waiting',
entry.reviewNote || '',
]);
}
function normalizeHumanReviewReason_(value) {
const allowed = [
'personal_data',
'price_or_contract',
'complaint_or_risk',
'missing_context',
'public_output',
'low_confidence',
'other',
];
return allowed.includes(value) ? value : 'other';
}
function normalizeImprovementTarget_(value) {
const allowed = ['faq', 'crm', 'approval_rule', 'form', 'template', 'no_action'];
return allowed.includes(value) ? value : 'no_action';
}
function createLogId_() {
const today = Utilities.formatDate(new Date(), Session.getScriptTimeZone(), 'yyyyMMdd');
const random = Math.floor(Math.random() * 900 + 100);
return `OPS-${today}-${random}`;
}
使い方は次のような形です。
function sampleAddAiOpsLog() {
addAiOpsLog({
taskType: 'reply_draft',
sourceChannel: 'contact_form',
dataLevel: 'masked',
humanReviewReason: 'missing_context',
blockedReason: 'missing_crm_status',
improvementTarget: 'crm',
owner: 'ops_lead',
reviewNote: '既存顧客か新規相談か分からず返信下書きを止めた',
});
}
ここでも問い合わせ本文や顧客名は入れません。必要な場合は、元システムの問い合わせIDだけを別途参照します。
週次レビューを作る
週次レビューでは、全ログを読まずに偏りを見ます。
見る条件は次の5つです。
-
missing_contextが多い: CRMや前回対応メモが足りない -
personal_dataが多い: マスキングや入力前チェックが弱い -
price_or_contractが多い: 料金・契約まわりの承認条件が必要 -
faqが多い: FAQ候補にできる質問が残っている -
blocked_reasonが空欄: 止めた理由が残っていない
function runWeeklyAiOpsReview() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const logSheet = ss.getSheetByName(SHEETS.log);
if (!logSheet) throw new Error(`${SHEETS.log} sheet is missing`);
const rows = readObjects_(logSheet);
const now = new Date();
const weekStart = getWeekStart_(now);
const weekEnd = new Date(weekStart);
weekEnd.setDate(weekEnd.getDate() + 7);
const weeklyRows = rows.filter((row) => {
const loggedAt = toDate_(row.logged_at);
return loggedAt && loggedAt >= weekStart && loggedAt < weekEnd;
});
const summary = buildWeeklySummary_(weeklyRows, weekStart);
writeWeeklyReview_(ss, summary);
writeImprovementBacklog_(ss, summary);
}
function buildWeeklySummary_(rows, weekStart) {
const reasonCounts = countBy_(rows, 'human_review_reason');
const targetCounts = countBy_(rows, 'improvement_target');
const blankBlockedReasonCount = rows.filter((row) => !row.blocked_reason).length;
return {
week_start: formatDate_(weekStart),
total_count: rows.length,
missing_context_count: reasonCounts.missing_context || 0,
personal_data_count: reasonCounts.personal_data || 0,
contract_count: reasonCounts.price_or_contract || 0,
faq_count: targetCounts.faq || 0,
crm_count: targetCounts.crm || 0,
approval_rule_count: targetCounts.approval_rule || 0,
blank_blocked_reason_count: blankBlockedReasonCount,
next_action_hint: buildNextActionHint_(reasonCounts, targetCounts, blankBlockedReasonCount),
};
}
function writeWeeklyReview_(ss, summary) {
const sheet = ss.getSheetByName(SHEETS.review);
sheet.appendRow(REVIEW_HEADERS.map((header) => summary[header]));
}
改善バックログへ変換する
レビュー結果から、改善タスクを自動で1件ずつ作ります。
function writeImprovementBacklog_(ss, summary) {
const sheet = ss.getSheetByName(SHEETS.backlog);
const actions = buildBacklogActions_(summary);
actions.forEach((action) => {
sheet.appendRow([
createBacklogId_(),
new Date(),
summary.week_start,
action.target,
action.changeSummary,
'unassigned',
'',
action.doneDefinition,
'todo',
]);
});
}
function buildBacklogActions_(summary) {
const actions = [];
if (summary.faq_count >= 3) {
actions.push({
target: 'faq',
changeSummary: '同じ質問をFAQ候補に追加する',
doneDefinition: '質問、回答骨子、公開/社内区分、確認担当が決まった',
});
}
if (summary.crm_count >= 2 || summary.missing_context_count >= 2) {
actions.push({
target: 'crm',
changeSummary: '顧客状態、前回対応、担当者の不足項目を見直す',
doneDefinition: 'CRM項目名、選択肢、入力タイミングが決まった',
});
}
if (summary.approval_rule_count >= 2 || summary.contract_count >= 1) {
actions.push({
target: 'approval_rule',
changeSummary: '料金、契約、返金、外部送信前の承認条件を明文化する',
doneDefinition: '止める条件、確認担当、返答期限が決まった',
});
}
if (summary.blank_blocked_reason_count >= 1) {
actions.push({
target: 'template',
changeSummary: 'blocked_reasonを必須で残す入力ルールへ直す',
doneDefinition: 'ログ入力時に止めた理由が空欄にならない',
});
}
return actions;
}
function buildNextActionHint_(reasonCounts, targetCounts, blankBlockedReasonCount) {
if ((targetCounts.faq || 0) >= 3) return 'FAQ候補を作る';
if ((targetCounts.crm || 0) >= 2) return 'CRM項目を見直す';
if ((reasonCounts.price_or_contract || 0) >= 1) return '承認ルールを追加する';
if (blankBlockedReasonCount >= 1) return '止めた理由の入力を必須にする';
return '今週は記録を継続する';
}
function createBacklogId_() {
const today = Utilities.formatDate(new Date(), Session.getScriptTimeZone(), 'yyyyMMdd');
const random = Math.floor(Math.random() * 900 + 100);
return `IMP-${today}-${random}`;
}
共通ヘルパー
最後に、読み取りと日付処理の関数を追加します。
function readObjects_(sheet) {
const values = sheet.getDataRange().getValues();
const headers = values.shift();
return values
.filter((row) => row.some((value) => value !== ''))
.map((row) => {
const object = {};
headers.forEach((header, index) => {
object[header] = row[index];
});
return object;
});
}
function countBy_(rows, key) {
return rows.reduce((counts, row) => {
const value = row[key] || 'blank';
counts[value] = (counts[value] || 0) + 1;
return counts;
}, {});
}
function getWeekStart_(date) {
const result = new Date(date);
const day = result.getDay();
const diff = day === 0 ? -6 : 1 - day;
result.setDate(result.getDate() + diff);
result.setHours(0, 0, 0, 0);
return result;
}
function toDate_(value) {
if (value instanceof Date && !Number.isNaN(value.getTime())) return value;
const date = new Date(value);
return Number.isNaN(date.getTime()) ? null : date;
}
function formatDate_(date) {
return Utilities.formatDate(date, Session.getScriptTimeZone(), 'yyyy-MM-dd');
}
トリガー設定
毎週月曜の朝にレビューする場合は、手動で一度だけ次を実行します。
function createWeeklyAiOpsReviewTrigger() {
ScriptApp.newTrigger('runWeeklyAiOpsReview')
.timeBased()
.onWeekDay(ScriptApp.WeekDay.MONDAY)
.atHour(9)
.create();
}
最初は自動トリガーにする前に、sampleAddAiOpsLog() で数件入れてから runWeeklyAiOpsReview() を手動実行する方が確認しやすいです。
AIに任せてよいこと、任せないこと
この実装でAIに任せてよいのは、分類候補やFAQ候補の下書きです。
| AIに任せてよい | 人間が確認する |
|---|---|
| ログ分類の候補 | 最終分類 |
| FAQ候補のたたき台 | 公開してよいか |
| CRM不足項目の候補 | 項目追加の決定 |
| 承認ルール案の整理 | 誰が責任を持つか |
| 週次レビューの要約 | 次に直す優先順位 |
顧客本文、個人情報、認証情報、契約の未確認情報はAIに渡しません。
外部へ送る返信文、公開FAQ、料金・契約・返金に関わる案内は、人間確認を完了条件にします。
まとめ
AI運用ログは、保存するだけでは弱いです。
価値が出るのは、週次レビューで止まった理由を見つけ、FAQ・CRM・承認ルール・フォーム・テンプレートへ戻した時です。
- ログには本文ではなく分類を残す
- 人間確認の理由を固定値にする
- 改善先も固定値にする
- 週次で偏りを見る
- 改善バックログに変換する
まずは1週間だけ、AIを使った用途、止めた理由、改善先の3つを残してみてください。
Miraigentでは、AI導入そのものより前に、人間確認・送らない情報・例外ケース・運用ログの戻し先を整理する支援をしています。
