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スプレッドシートで作る

0
Posted at

この記事は、AIエージェントやAI返信下書きを導入する前に、誰が、何を、なぜ判断したかをGoogleスプレッドシートとGASで残すための実装メモです。

AI APIの呼び出しは扱いません。先に、判断待ち、承認、保留、差し戻し、例外対応を記録できる軽い台帳を作ります。

作るもの

Googleフォームや問い合わせログに対して、次のような判断ログを作ります。

  • decision_id: 判断ID
  • source_id: 元の問い合わせやタスクID
  • decision_type: 判断種別
  • status: 判断状態
  • requested_by: 判断を依頼した人
  • owner: 判断する人
  • reason: 判断が必要な理由
  • decision: 決定内容
  • next_action: 次にやること
  • due_at: 期限
  • decided_at: 決定日時

完成形は、AI導入前の小さなDecision Logです。

AIに任せる範囲を増やす前に、人間が迷った判断を残します。

AI導入前の人間確認ゲート

なぜ判断ログが必要なのか

AI導入では、プロンプトや自動化フローを先に作りたくなります。

ただ、現場で止まるのは、文章生成そのものではありません。

  • これはAIに任せてよいのか
  • 誰が承認すればよいのか
  • なぜ保留になったのか
  • 前回はどう判断したのか
  • 似たケースで、判断が変わっていないか
  • 例外対応をFAQや運用ルールに反映したか

これらが残っていないと、AIエージェントを入れても毎回同じ確認が発生します。

判断ログは、AIのためというより、現場の迷いを減らすための記録です。

シート構成

最初は1枚のシートで十分です。

項目 用途
A created_at 2026/06/03 11:15 判断依頼の作成日時
B decision_id DEC-20260603-001 判断を一意に追うID
C source_id INQ-20260603-014 元の問い合わせやタスクID
D source_type inquiry 元データの種別
E decision_type ai_transfer 判断種別
F status pending 判断状態
G requested_by 担当者A 判断を依頼した人
H owner 責任者B 判断する人
I reason 契約条件を含むため 判断が必要な理由
J options approve / hold / reject 選択肢
K decision hold 決定内容
L next_action 顧客へ追加確認 次にやること
M due_at 2026/06/03 15:00 期限
N decided_at 2026/06/03 12:20 決定日時
O note 料金表の更新も必要 補足

ポイントは、判断結果だけでなく、判断が必要になった理由も残すことです。

理由が残ると、後でAIに渡すルール、FAQ、承認フローを改善できます。

decision_typeの例

最初は細かくしすぎず、5種類ほどで始めます。

decision_type 使う場面
ai_transfer AIに情報を渡してよいか
reply_approval 顧客返信を送ってよいか
exception_handling 例外ケースとして人間が見るか
faq_update FAQやテンプレートへ反映するか
crm_stage_change CRM上の状態を進めるか

この分類は、AIエージェントのタスク分類にも使えます。

statusの例

判断ログでは、状態を固定します。

status 意味
pending 判断待ち
approved 承認済み
rejected 却下
hold 保留
returned 差し戻し
done 次アクション完了

「確認中」「対応中」「たぶんOK」のような曖昧な状態を増やすと、AIにも人間にも扱いにくくなります。

GASコード

Apps Scriptを開き、次のコードを貼ります。

const CONFIG = {
  headerRow: 1,
  sheetName: 'DecisionLog',
  columns: {
    createdAt: 1,
    decisionId: 2,
    sourceId: 3,
    sourceType: 4,
    decisionType: 5,
    status: 6,
    requestedBy: 7,
    owner: 8,
    reason: 9,
    options: 10,
    decision: 11,
    nextAction: 12,
    dueAt: 13,
    decidedAt: 14,
    note: 15
  }
};

const DECISION_RULES = [
  {
    type: 'ai_transfer',
    keywords: ['個人情報', '電話番号', '住所', '返金', '解約', '契約', 'クレーム'],
    owner: '責任者',
    reason: 'AIへ渡す前に人間確認が必要',
    options: 'approve / mask / reject / hold'
  },
  {
    type: 'reply_approval',
    keywords: ['送信してよい', '返信案', 'お詫び', '料金', '納期'],
    owner: '担当責任者',
    reason: '顧客返信前の承認が必要',
    options: 'approve / returned / hold'
  },
  {
    type: 'faq_update',
    keywords: ['よくある質問', 'FAQ', '同じ質問', 'テンプレート'],
    owner: 'CS担当',
    reason: 'FAQ反映可否の判断が必要',
    options: 'approve / reject / hold'
  }
];

function createDecisionFromInquiry(source) {
  const sheet = getDecisionSheet_();
  const rule = findDecisionRule_(source.body || '');
  const now = new Date();
  const decisionId = buildDecisionId_(sheet, now);
  const dueAt = new Date(now.getTime() + 4 * 60 * 60 * 1000);

  sheet.appendRow([
    now,
    decisionId,
    source.id || '',
    source.type || 'inquiry',
    rule.type,
    'pending',
    source.requestedBy || '未設定',
    rule.owner,
    rule.reason,
    rule.options,
    '',
    '判断後に次アクションを記入',
    dueAt,
    '',
    source.body || ''
  ]);

  return decisionId;
}

function findDecisionRule_(text) {
  const body = String(text || '');
  const matched = DECISION_RULES.find((rule) =>
    rule.keywords.some((keyword) => body.includes(keyword))
  );

  return matched || {
    type: 'exception_handling',
    owner: '担当者',
    reason: '分類ルールに未登録のため確認',
    options: 'approve / hold / returned'
  };
}

function updateDecision(decisionId, decision, nextAction, note) {
  const sheet = getDecisionSheet_();
  const values = sheet.getDataRange().getValues();
  const idCol = CONFIG.columns.decisionId - 1;

  for (let i = CONFIG.headerRow; i < values.length; i += 1) {
    if (values[i][idCol] === decisionId) {
      const status = normalizeDecisionStatus_(decision);
      const row = i + 1;
      sheet.getRange(row, CONFIG.columns.status).setValue(status);
      sheet.getRange(row, CONFIG.columns.decision).setValue(decision);
      sheet.getRange(row, CONFIG.columns.nextAction).setValue(nextAction || '');
      sheet.getRange(row, CONFIG.columns.decidedAt).setValue(new Date());
      sheet.getRange(row, CONFIG.columns.note).setValue(note || '');
      return { decisionId, status };
    }
  }

  throw new Error('Decision not found: ' + decisionId);
}

function normalizeDecisionStatus_(decision) {
  const value = String(decision || '').toLowerCase();
  if (['approve', 'approved'].includes(value)) return 'approved';
  if (['reject', 'rejected'].includes(value)) return 'rejected';
  if (['returned', 'return'].includes(value)) return 'returned';
  if (['hold', 'pending'].includes(value)) return 'hold';
  return 'pending';
}

function getPendingDecisions() {
  const sheet = getDecisionSheet_();
  const values = sheet.getDataRange().getValues();
  const statusCol = CONFIG.columns.status - 1;
  return values
    .slice(CONFIG.headerRow)
    .filter((row) => ['pending', 'hold', 'returned'].includes(row[statusCol]))
    .map((row) => ({
      decisionId: row[CONFIG.columns.decisionId - 1],
      sourceId: row[CONFIG.columns.sourceId - 1],
      type: row[CONFIG.columns.decisionType - 1],
      status: row[CONFIG.columns.status - 1],
      owner: row[CONFIG.columns.owner - 1],
      reason: row[CONFIG.columns.reason - 1],
      dueAt: row[CONFIG.columns.dueAt - 1]
    }));
}

function buildDecisionId_(sheet, date) {
  const yyyymmdd = Utilities.formatDate(date, 'Asia/Tokyo', 'yyyyMMdd');
  const count = Math.max(0, sheet.getLastRow() - CONFIG.headerRow) + 1;
  return 'DEC-' + yyyymmdd + '-' + String(count).padStart(3, '0');
}

function getDecisionSheet_() {
  const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = spreadsheet.getSheetByName(CONFIG.sheetName) || spreadsheet.insertSheet(CONFIG.sheetName);
  ensureHeader_(sheet);
  return sheet;
}

function ensureHeader_(sheet) {
  const headers = [
    'created_at',
    'decision_id',
    'source_id',
    'source_type',
    'decision_type',
    'status',
    'requested_by',
    'owner',
    'reason',
    'options',
    'decision',
    'next_action',
    'due_at',
    'decided_at',
    'note'
  ];

  const current = sheet.getRange(CONFIG.headerRow, 1, 1, headers.length).getValues()[0];
  if (current.join('') !== headers.join('')) {
    sheet.getRange(CONFIG.headerRow, 1, 1, headers.length).setValues([headers]);
  }
}

function demoCreateDecision() {
  return createDecisionFromInquiry({
    id: 'INQ-20260603-001',
    type: 'inquiry',
    requestedBy: 'CS担当',
    body: '解約と返金について相談がありました。AI返信に進めてよいか確認したいです。'
  });
}

使い方

  1. スプレッドシートに DecisionLog シートを作ります。
  2. Apps Scriptに上記コードを貼ります。
  3. demoCreateDecision() を実行します。
  4. DEC-YYYYMMDD-001 の形式で判断ログが作られることを確認します。
  5. updateDecision('DEC-20260603-001', 'hold', '契約条件を確認する', '料金表と過去契約を確認') のように更新します。

実運用では、問い合わせ受付シートの行から createDecisionFromInquiry() を呼び出します。

AI導入時に使う判断材料

判断ログを残すと、AIエージェント導入時に次の情報が使えます。

  • どの判断が多いか
  • どの判断が保留になりやすいか
  • どの担当者に確認が集中しているか
  • どのルールが曖昧か
  • FAQやテンプレートに反映すべきケースは何か

AIに「判断してください」と任せる前に、人間の判断がどこで詰まっているかを見ます。

運用チェックリスト

  • 判断IDが必ず残る
  • 判断理由が空欄にならない
  • 承認者と依頼者が分かる
  • 保留、差し戻し、却下を正常な状態として扱う
  • 判断後の次アクションが残る
  • AIへ渡す前の判断と、顧客へ送信する前の判断を混ぜない
  • 未分類ケースをルール改善候補として残す

まとめ

AIエージェント導入前に必要なのは、最初から高度な自動化を作ることではありません。

まず、人間が迷った判断を残します。

判断ログがあると、AIに任せる範囲、人間が見る範囲、FAQ化する範囲が見えます。

AI導入は、判断を消すためではなく、判断すべき場所を見えるようにしてから進める方が安定します。

Miraigentでは、AI導入前の業務整理として、問い合わせログ、FAQ候補、CRM状態、判断ログを先に整えることを重視しています。

Miraigentの公開リソース

Miraigentでは、AI導入前に整えるべき問い合わせ対応、FAQ、CRM、人間確認ルールを整理しています。

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?