生成AIで問い合わせの要約や返信下書きを作ると、処理件数や初回返信時間は改善して見えることがあります。
一方で、担当者が確認する時間、差し戻し件数、停止した案件が記録されていないと、「AIを入れた結果、人の仕事が軽くなったのか、確認作業が増えただけなのか」を判断できません。
この記事では、GoogleスプレッドシートとGoogle Apps Script(GAS)で、AI導入後の人間確認負荷を測る最小構成を作ります。AI APIやメール送信には接続せず、確認結果の記録と週次集計だけを実装します。
この記事で作るもの
- 1件の確認を、案件IDと集計に必要な最小項目で記録する
- 確認時間、差し戻し、停止、再確認を分ける
- 週次で業務別の負荷を集計する
- 閾値超過を、対象範囲の自動拡大ではなく見直し候補にする
- 原因と次の変更を人が確定する
問い合わせ本文、氏名、メールアドレス、契約情報はログへ複製しません。集計に必要なのは、非機密の内部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',
};
});
}
見直し候補が出たら、次の順に確認します。
- 増えた時間がAI下書きの確認なのか、元々あった業務なのかを分ける
- reason_codeの上位を確認する
- FAQ、フォーム項目、正本、分類ルールのどこが不足しているか決める
- AIへ任せる範囲を広げるのではなく、必要なら対象業務を縮める
- 変更理由、承認者、見直し日を記録する
指標を一つにしない
- 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から記録してください。数値をもとに自動で運用を変えるのではなく、次の見直しを人が判断できる状態を作ることが最初の完成形です。