1
1

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運用ログを週次レビューし、FAQ・CRM・承認ルールへ戻すGAS実装

1
Posted at

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運用ログを改善先へ戻す流れ

流れは次の通りです。

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_reasonimprovement_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導入そのものより前に、人間確認・送らない情報・例外ケース・運用ログの戻し先を整理する支援をしています。

1
1
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
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?