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

0
Posted at

生成AIで問い合わせの要約や返信下書きを作ると、処理件数や初回返信時間は改善して見えることがあります。

一方で、担当者が確認する時間、差し戻し件数、停止した案件が記録されていないと、「AIを入れた結果、人の仕事が軽くなったのか、確認作業が増えただけなのか」を判断できません。

この記事では、GoogleスプレッドシートとGoogle Apps Script(GAS)で、AI導入後の人間確認負荷を測る最小構成を作ります。AI APIやメール送信には接続せず、確認結果の記録と週次集計だけを実装します。

この記事で作るもの

  1. 1件の確認を、案件IDと集計に必要な最小項目で記録する
  2. 確認時間、差し戻し、停止、再確認を分ける
  3. 週次で業務別の負荷を集計する
  4. 閾値超過を、対象範囲の自動拡大ではなく見直し候補にする
  5. 原因と次の変更を人が確定する

問い合わせ本文、氏名、メールアドレス、契約情報はログへ複製しません。集計に必要なのは、非機密の内部ID、業務分類、状態、時間、理由コードです。

シート構成

review_events

1行を1回の人間確認として記録します。

役割
event_id REV-20260809-001 確認イベントのID
case_id CASE-1042 非機密の案件ID
workflow inquiry_reply 業務分類
reviewer_role support_lead 個人名ではなく役割
started_at / ended_at 09:00 / 09:08 確認時間
decision approve approve / revise / hold
rewrite_count 1 人が書き直した回数
reason_code missing_condition 修正・停止理由
policy_version reply-policy-v4 適用ルール版
source_ref FAQ-12 根拠の参照ID

開始時刻と終了時刻から確認時間を算出します。別作業の中断まで正確に測れるわけではないため、絶対的な生産性ではなく、同じ業務の変化を見る指標として使います。

metric_config

閾値をコードへ埋め込まず、責任者がレビューできる設定にします。

key value
weekly_review_minutes_max 20
rewrite_rate_max 0.30
hold_rate_max 0.15
minimum_events 10
owner_role support_lead

これは「超えたらAIを止める」と決める設定ではありません。見直し候補を出すための合図です。

weekly_metrics

GASが週単位の集計結果を書き込みます。

week_start 2026-08-03
workflow inquiry_reply
event_count 18
review_minutes 126
average_minutes 7
rewrite_rate 0.28
hold_rate 0.11
reason_codes missing_condition,source_unclear
status review_pending

入力を検証する

空欄を0分として扱うと、確認負荷が実際より小さく見えます。欠損はエラーとして残します。

const REVIEW_HEADERS = [
  'event_id', 'case_id', 'workflow', 'reviewer_role',
  'started_at', 'ended_at', 'decision', 'rewrite_count',
  'reason_code', 'policy_version', 'source_ref',
];

const DECISIONS = new Set(['approve', 'revise', 'hold']);

function validateReviewEvent(event) {
  const errors = [];
  const required = [
    'event_id', 'case_id', 'workflow', 'reviewer_role',
    'started_at', 'ended_at', 'decision', 'policy_version',
  ];

  required.forEach((key) => {
    if (!String(event[key] ?? '').trim()) errors.push(key + ':required');
  });

  if (!DECISIONS.has(String(event.decision))) {
    errors.push('decision:unknown');
  }

  const started = new Date(event.started_at);
  const ended = new Date(event.ended_at);
  if (Number.isNaN(started.getTime()) || Number.isNaN(ended.getTime())) {
    errors.push('time:invalid');
  } else if (ended < started) {
    errors.push('time:ended_before_started');
  }

  const rewriteCount = Number(event.rewrite_count ?? 0);
  if (!Number.isInteger(rewriteCount) || rewriteCount < 0) {
    errors.push('rewrite_count:invalid');
  }

  return { ok: errors.length === 0, errors };
}

decisionはAIに決めさせません。AIが作った下書きの候補状態と、人が確定した確認結果を別に保存してください。

ヘッダーから行を読む

列の順番を変えても壊れにくいよう、ヘッダーからインデックスを作ります。

function indexHeaders(headers) {
  const index = Object.fromEntries(
    headers.map((value, position) => [String(value).trim(), position]),
  );
  const missing = REVIEW_HEADERS.filter((key) => !(key in index));
  if (missing.length) throw new Error('Missing headers: ' + missing.join(','));
  return index;
}

function toReviewEvent(row, index) {
  return Object.fromEntries(
    REVIEW_HEADERS.map((key) => [key, row[index[key]]]),
  );
}

source_refは元の正本へ戻るためのIDであり、問い合わせ本文を貼る欄ではありません。シートの共有権限も確認担当者と管理者へ限定します。

確認時間を業務単位で集計する

function minutesBetween(startedAt, endedAt) {
  const started = new Date(startedAt).getTime();
  const ended = new Date(endedAt).getTime();
  if (!Number.isFinite(started) || !Number.isFinite(ended) || ended < started) {
    throw new Error('Invalid review interval');
  }
  return Math.ceil((ended - started) / 60000);
}

function aggregateWeeklyMetrics(events, weekStart) {
  const byWorkflow = new Map();

  events.forEach((event) => {
    const key = String(event.workflow).trim();
    if (!key) return;
    const bucket = byWorkflow.get(key) ?? {
      workflow: key, eventCount: 0, reviewMinutes: 0,
      reviseCount: 0, holdCount: 0, reasonCodes: new Set(),
    };

    bucket.eventCount += 1;
    bucket.reviewMinutes += minutesBetween(event.started_at, event.ended_at);
    if (event.decision === 'revise') bucket.reviseCount += 1;
    if (event.decision === 'hold') bucket.holdCount += 1;
    if (event.reason_code) bucket.reasonCodes.add(String(event.reason_code));
    byWorkflow.set(key, bucket);
  });

  return [...byWorkflow.values()].map((bucket) => ({
    weekStart,
    workflow: bucket.workflow,
    eventCount: bucket.eventCount,
    reviewMinutes: bucket.reviewMinutes,
    averageMinutes: Math.round(
      (bucket.reviewMinutes / bucket.eventCount) * 10,
    ) / 10,
    rewriteRate: bucket.reviseCount / bucket.eventCount,
    holdRate: bucket.holdCount / bucket.eventCount,
    reasonCodes: [...bucket.reasonCodes].sort().join(','),
    status: 'review_pending',
  }));
}

少数のイベントで割合を出すと、1件の停止だけで大きく見えます。minimum_events未満の業務は「データ不足」と表示し、良し悪しを断定しません。

見直し候補を作る

閾値超過を検出しても、GASからプロンプトや公開範囲を自動変更しません。人間が原因を確認するための候補を作ります。

function findReviewCandidates(metrics, config) {
  return metrics.map((metric) => {
    const blockers = [];
    if (metric.eventCount < Number(config.minimum_events)) {
      blockers.push('insufficient_events');
    } else {
      if (metric.averageMinutes > Number(config.weekly_review_minutes_max)) {
        blockers.push('review_time_high');
      }
      if (metric.rewriteRate > Number(config.rewrite_rate_max)) {
        blockers.push('rewrite_rate_high');
      }
      if (metric.holdRate > Number(config.hold_rate_max)) {
        blockers.push('hold_rate_high');
      }
    }

    return {
      weekStart: metric.weekStart,
      workflow: metric.workflow,
      blockers,
      recommendation: blockers.length
        ? 'human_review_required'
        : 'continue_observation',
    };
  });
}

見直し候補が出たら、次の順に確認します。

  1. 増えた時間がAI下書きの確認なのか、元々あった業務なのかを分ける
  2. reason_codeの上位を確認する
  3. FAQ、フォーム項目、正本、分類ルールのどこが不足しているか決める
  4. AIへ任せる範囲を広げるのではなく、必要なら対象業務を縮める
  5. 変更理由、承認者、見直し日を記録する

指標を一つにしない

  • event_count: 何件を確認したか
  • review_minutes: 確認に使った総時間
  • average_minutes: 1件あたりの平均時間
  • rewrite_rate: 下書きの修正が必要だった割合
  • hold_rate: 担当者だけでは確定せず止めた割合
  • reason_codes: 何が確認を難しくしたか

総時間が増えていても件数が大きく増えたなら、1件あたりの負荷は下がっている可能性があります。平均時間が同じでも停止や差し戻しが増えていれば、対象範囲や根拠を見直します。

人間確認を残す範囲

この計測は、確認作業をなくすためのものではありません。次の判断は自動集計の対象になっても、人が確定します。

  • 契約、価格、返金、納期、保証に関する表現
  • 苦情、紛争、権限変更、個人情報の扱い
  • 正本として採用する資料
  • AIの対象業務を広げる、または停止する判断
  • ログの保存期間、閲覧権限、削除方法

確認時間が短いことを「安全」と解釈しないでください。確認を省略すれば、指標は改善に見えてしまいます。source_refの確認や停止理由は、時間とは別の必須条件として点検します。

導入チェックリスト

  • 顧客本文、氏名、メールアドレス、秘密情報をログへ複製していない
  • 非機密のcase_idとsource_refで元の正本へ戻れる
  • 開始・終了時刻の欠損を0分として扱っていない
  • 承認、修正、停止を別の状態で記録している
  • 確認時間だけでなく差し戻し率と停止率も見ている
  • 少数データを断定せず最低件数を設定している
  • 閾値超過でAIの対象範囲を自動拡大していない
  • 原因、承認者、次の変更を記録している
  • 契約、苦情、個人情報の停止条件を別管理している

まとめ

AI導入の効果を「返信が速くなった」だけで判断すると、人間確認の負荷が見えなくなります。確認時間、修正、停止、理由を同じログで追えば、AIが仕事を減らしたのか、別の確認作業へ移したのかを話し合えるようになります。

まずは直近10件を対象に、workflow、decision、開始・終了時刻、reason_codeから記録してください。数値をもとに自動で運用を変えるのではなく、次の見直しを人が判断できる状態を作ることが最初の完成形です。

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?