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返信下書きを送る前にGASで判定ログを残す:送信・差し戻し・保留を分ける

0
Posted at

問い合わせ対応でAIに返信下書きを作らせても、下書きの完成と送信可否は別の状態です。

この記事では、GoogleスプレッドシートとGoogle Apps Script(GAS)で、返信下書きのレビュー結果を記録し、送信前の確認漏れを一覧化する最小構成を紹介します。AI APIの呼び出しや顧客への自動送信は扱いません。AIは下書きと確認候補まで、人間が送信・差し戻し・保留を確定する構成です。

作るもの

AI返信下書き、人間確認、送信または差し戻し、判定ログの流れ

REPLY_REVIEW_LOG シートに、1件の返信レビューを1行で保存します。

列 内容
inquiry_id 問い合わせを戻すための識別子
draft AIが作った返信下書き
source_refs 参照したFAQや手順の識別子
facts_checked 事実を確認したか
recipient_checked 宛先・対象者を確認したか
promise_checked 約束・期限を確認したか
sensitive_data_present 個人情報などが含まれるか
decision send / revise / hold
reason 判定理由
reviewer 人間の確認者
reviewed_at 確認日時
next_action 次に行うこと
status open / done

send は「AIが送信してよいと決めた」という意味にしません。確認項目を人間が埋め、担当者が送信する段階へ進めた、という記録に限定します。

シートを準備する

スプレッドシートに REPLY_REVIEW_LOG というシートを作り、1行目へ見出しを入力します。

inquiry_id,draft,source_refs,facts_checked,recipient_checked,promise_checked,sensitive_data_present,decision,reason,reviewer,reviewed_at,next_action,status

サンプルデータには実際の氏名、メールアドレス、契約番号、問い合わせ本文を入れないでください。動作確認には、次のような匿名化した行を使います。

inq-20260824-001,確認のうえ担当者からご連絡します。,faq-12|service-guide-03,TRUE,TRUE,FALSE,FALSE,revise,返信予定日の根拠がない,担当者A,,返信予定日を確認する,open

GASでレビュー結果を検証する

拡張機能 > Apps Script を開き、次のコードを貼り付けます。

const REVIEW_SHEET = 'REPLY_REVIEW_LOG';
const DECISIONS = new Set(['send', 'revise', 'hold']);
const REQUIRED_HEADERS = [
  'inquiry_id', 'draft', 'source_refs', 'facts_checked',
  'recipient_checked', 'promise_checked', 'sensitive_data_present',
  'decision', 'reason', 'reviewer', 'reviewed_at', 'next_action', 'status'
];

function onOpen() {
  SpreadsheetApp.getUi()
    .createMenu('返信レビュー')
    .addItem('レビュー行を検証', 'validateReviewRows')
    .addItem('要確認一覧を作成', 'createOpenReviewSheet')
    .addToUi();
}

function validateReviewRows() {
  const sheet = SpreadsheetApp.getActive().getSheetByName(REVIEW_SHEET);
  if (!sheet) throw new Error(`${REVIEW_SHEET} シートがありません`);

  const values = sheet.getDataRange().getValues();
  if (values.length < 2) return;
  const header = values[0].map(String);
  const index = Object.fromEntries(header.map((name, i) => [name, i]));
  for (const name of REQUIRED_HEADERS) {
    if (index[name] === undefined) throw new Error(`列がありません: ${name}`);
  }

  const errors = [];
  values.slice(1).forEach((row, offset) => {
    const line = offset + 2;
    if (row.every(value => value === '')) return;
    const get = name => String(row[index[name]] ?? '').trim();
    const decision = get('decision');
    if (!get('inquiry_id')) errors.push(`${line}行目: inquiry_id が空です`);
    if (!get('draft')) errors.push(`${line}行目: draft が空です`);
    if (!DECISIONS.has(decision)) {
      errors.push(`${line}行目: decision は send/revise/hold のいずれかです`);
      return;
    }
    if (decision === 'send' &&
        (!get('facts_checked') || !get('recipient_checked') || !get('promise_checked') ||
         get('sensitive_data_present').toUpperCase() === 'TRUE' || !get('reviewer'))) {
      errors.push(`${line}行目: send には確認項目・機微情報なし・確認者が必要です`);
    }
    if (decision !== 'send' && !get('reason')) {
      errors.push(`${line}行目: revise/hold には reason が必要です`);
    }
    if (decision === 'hold' && !get('next_action')) {
      errors.push(`${line}行目: hold には next_action が必要です`);
    }
  });

  if (errors.length) throw new Error(errors.join('\n'));
  SpreadsheetApp.getUi().alert('レビュー行の検証に成功しました。送信操作は担当者が行ってください。');
}

function createOpenReviewSheet() {
  const source = SpreadsheetApp.getActive().getSheetByName(REVIEW_SHEET);
  if (!source) throw new Error(`${REVIEW_SHEET} シートがありません`);
  const values = source.getDataRange().getValues();
  const header = values[0];
  const statusIndex = header.indexOf('status');
  const rows = values.slice(1).filter(row => String(row[statusIndex]).toLowerCase() !== 'done');
  const book = SpreadsheetApp.getActive();
  const old = book.getSheetByName('OPEN_REPLY_REVIEWS');
  if (old) book.deleteSheet(old);
  const target = book.insertSheet('OPEN_REPLY_REVIEWS');
  target.getRange(1, 1, 1, header.length).setValues([header]);
  if (rows.length) target.getRange(2, 1, rows.length, header.length).setValues(rows);
  target.setFrozenRows(1);
}

このコードは send になった行を自動送信しません。検証、要確認一覧の生成、記録の一貫性確認だけを行います。

判定を分ける

実務では、次の3値を最初から固定すると、担当者ごとの「問題なさそう」が減ります。

  • send: 事実、宛先、約束、機微情報、確認者を確認できた。担当者が送信する
  • revise: 直せる不足や表現の問題がある。下書きを直して再確認する
  • hold: 契約、金額、苦情、個人情報、根拠不足、権限不足がある。送信せず責任者へ戻す

特に hold は失敗ではなく、戻し先を明確にするための状態です。reason と next_action がない保留は、次の担当者を再び迷わせます。

運用前チェックリスト

  • AIに実顧客の機微情報を入力していない
  • FAQや手順の識別子を source_refs に残せる
  • 送信可否をAIの出力だけで決めていない
  • send に確認者と確認日時がある
  • revise と hold に理由がある
  • hold に責任者や次の確認先がある
  • 送信済みになった後、担当者が status を更新する
  • 一定期間後に差し戻し理由をFAQ・フォーム・承認ルールへ戻す

まとめ

AI返信の導入で先に作るべきものは、自動送信の処理ではなく、送信前の判定を戻せるログです。GASで入力を検証し、send・revise・hold を分けるだけでも、確認基準と次の行動を共有できます。

この記事の構成は、AI API呼び出しや社外送信を含まない最小例です。契約、料金、個人情報、苦情などは必ず社内の責任者へ戻してください。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?