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で4段階に振り分ける:原文を広げず確認キューへ戻す

0
Posted at

AIを使った問い合わせ整理や返信案作成では、判断ログを残すだけでなく、誰に、どの粒度で共有するかを決める必要があります。

原文を全員へ配ると個人情報や契約情報まで広がり、逆に共有を絞りすぎると、次の担当者が停止理由や根拠を確認できません。

この記事では、GoogleスプレッドシートとGoogle Apps Script(GAS)で、匿名化済みメタデータから共有範囲の候補を4段階に振り分けます。本文の転送、ファイル複製、権限変更、外部通知、AI APIの呼び出しは行いません。候補を確認キューへ戻し、人が確定する手前までを実装します。

この記事で作るもの

  • AI_LOG_SHARING シートに共有判断を1ログ1行で記録する
  • WORKER / REVIEWER / OWNER / RESTRICTED の候補を返す
  • 目的・影響度・機密性の未入力を安全側の確認待ちにする
  • 個人名や顧客本文を複製せず、人間確認キューを作る
  • 同じ log_id の二重登録を防ぐテスト用関数

問い合わせ受付から人間確認までのレビューゲート

共有範囲を役職ではなく用途で定義する

役職だけで「管理者には全部見せる」と決めると、実際の判断に不要な情報まで共有されます。最初は、読む人が何をするためにログを見るのかを固定します。

scope 共有する候補 用途
WORKER 状態、次の行動、参照ID 作業を正しい場所へ戻す
REVIEWER 要約、AI候補、確認項目、根拠ID 事実と根拠を照合する
OWNER 影響範囲、判断理由、再開条件 継続・停止・範囲変更を決める
RESTRICTED 権限付き原文参照、機密区分、監査履歴 事故対応や機密性を確認する

ここでの scope はアクセス権を変更する命令ではなく、共有設計の候補です。アクセス権は別の正本で管理し、GASから変更しないようにします。

シートを準備する

スプレッドシートに AI_LOG_SHARING シートを作り、1行目に次のヘッダーを置きます。

列 例 役割
log_id LOG-20260911-001 ログの一意なID
workflow inquiry_reply_draft 対象業務
purpose fact_check 共有する目的
impact customer_facing 下流への影響
sensitivity personal_data_possible 機密性の候補
summary 解約条件の根拠確認待ち 人が読む要約
scope RESTRICTED GASが返す候補
visible_fields summary,reason_code,evidence_ref 見せる項目
redacted_fields customer_name,contact_details 隠す項目
reviewer_role support_reviewer 事実確認の役割
final_decider_role support_owner 最終判断の役割
status REVIEW_REQUIRED 確認状態
evidence_ref FAQ-20260910-01 正本の参照ID
next_review_at 2026-09-11 15:00 見直し期限

ログ本文、氏名、メールアドレス、電話番号、認証情報をこのシートへコピーしません。evidence_ref から権限管理された正本へ戻れる設計にします。

判定ルールをGASに書く

まず、値の揺れを抑えるための定数と正規化関数を定義します。空欄や未知の値を WORKER に落とさないことがポイントです。

const SHARING_SCOPES = [
  'WORKER',
  'REVIEWER',
  'OWNER',
  'RESTRICTED',
];

const HIGH_IMPACT = new Set([
  'customer_facing',
  'contract_or_money',
  'public_statement',
  'complaint_or_incident',
]);

const SENSITIVE = new Set([
  'personal_data_possible',
  'authentication_data',
  'confidential_contract',
]);

function normalize(value) {
  return String(value == null ? '' : value).trim();
}

function normalizeList(value) {
  return normalize(value)
    .split(',')
    .map((item) => item.trim())
    .filter(Boolean);
}

次に、匿名化済みメタデータから候補を返します。候補を返すだけで、共有・送信・権限変更はしません。

function suggestSharingScope(record) {
  const purpose = normalize(record.purpose);
  const impact = normalize(record.impact);
  const sensitivity = normalize(record.sensitivity);

  if (!purpose || !impact || !sensitivity) {
    return {
      scope: 'RESTRICTED',
      status: 'REVIEW_REQUIRED',
      reason: 'sharing_metadata_incomplete',
    };
  }

  if (SENSITIVE.has(sensitivity)) {
    return {
      scope: 'RESTRICTED',
      status: 'REVIEW_REQUIRED',
      reason: 'sensitive_data_possible',
    };
  }

  if (HIGH_IMPACT.has(impact)) {
    return {
      scope: 'OWNER',
      status: 'REVIEW_REQUIRED',
      reason: 'human_decision_required',
    };
  }

  if (purpose === 'fact_check') {
    return {
      scope: 'REVIEWER',
      status: 'REVIEW_REQUIRED',
      reason: 'evidence_review',
    };
  }

  return {
    scope: 'WORKER',
    status: 'REVIEW_REQUIRED',
    reason: 'task_execution_only',
  };
}

すべての戻り値を REVIEW_REQUIRED にしています。REVIEWER が返ったことは、事実確認が完了したことを意味しません。

ヘッダー名から行を読む

列番号をGASへ直接書くと、シートの列を追加したときに別の値を読み込む可能性があります。ヘッダー名からインデックスを作り、必須列を検証します。

function makeHeaderIndex(headers) {
  const index = {};
  headers.forEach((value, position) => {
    const name = normalize(value);
    if (name) index[name] = position;
  });
  return index;
}

function requireHeaders(index, required) {
  const missing = required.filter((name) => index[name] === undefined);
  if (missing.length > 0) {
    throw new Error('Missing headers: ' + missing.join(','));
  }
}

function rowToRecord(row, index) {
  return {
    logId: normalize(row[index.log_id]),
    purpose: normalize(row[index.purpose]),
    impact: normalize(row[index.impact]),
    sensitivity: normalize(row[index.sensitivity]),
    summary: normalize(row[index.summary]),
    evidenceRef: normalize(row[index.evidence_ref]),
  };
}

既存ログを更新し、確認キューを作る

log_id をキーに既存行を検索し、未確認の行だけ候補を更新します。確認キューには要約と参照IDだけを出し、顧客本文は複製しません。

function refreshSharingSuggestions() {
  const book = SpreadsheetApp.getActiveSpreadsheet();
  const source = book.getSheetByName('AI_LOG_SHARING');
  const queue = book.getSheetByName('AI_LOG_SHARING_REVIEW_QUEUE')
    || book.insertSheet('AI_LOG_SHARING_REVIEW_QUEUE');
  if (!source) throw new Error('AI_LOG_SHARING is missing');

  const values = source.getDataRange().getValues();
  if (values.length < 2) return { updated: 0, reviewRequired: 0 };

  const index = makeHeaderIndex(values[0]);
  requireHeaders(index, [
    'log_id', 'purpose', 'impact', 'sensitivity', 'summary',
    'scope', 'status', 'evidence_ref',
  ]);

  const queueRows = [[
    'log_id', 'summary', 'scope', 'reason',
    'evidence_ref', 'queue_status',
  ]];
  let updated = 0;
  let reviewRequired = 0;

  values.slice(1).forEach((row, offset) => {
    const record = rowToRecord(row, index);
    if (!record.logId) return;

    const suggestion = suggestSharingScope(record);
    source.getRange(offset + 2, index.scope + 1)
      .setValue(suggestion.scope);
    source.getRange(offset + 2, index.status + 1)
      .setValue(suggestion.status);

    queueRows.push([
      record.logId,
      record.summary,
      suggestion.scope,
      suggestion.reason,
      record.evidenceRef,
      'WAITING_FOR_HUMAN',
    ]);
    updated += 1;
    reviewRequired += 1;
  });

  queue.clearContents();
  queue.getRange(1, 1, queueRows.length, queueRows[0].length)
    .setValues(queueRows);

  return { updated, reviewRequired };
}

この関数は共有先への通知やアクセス権の変更を行いません。また、VERIFIED や CLOSED へ自動遷移させません。人が正本を確認した後に、別の手順で状態を確定します。

判定関数をテストする

Apps Scriptの実行ログで確認できる、外部通信をしない簡易テストです。機密性が未確認のケースが安全側へ戻ることを先に確認します。

function assertEqual(actual, expected, label) {
  if (JSON.stringify(actual) !== JSON.stringify(expected)) {
    throw new Error(label + ': expected ' + JSON.stringify(expected)
      + ', got ' + JSON.stringify(actual));
  }
}

function testSuggestSharingScope() {
  assertEqual(
    suggestSharingScope({
      purpose: 'fact_check',
      impact: 'internal_only',
      sensitivity: 'none',
    }).scope,
    'REVIEWER',
    'fact check',
  );

  assertEqual(
    suggestSharingScope({
      purpose: 'status_check',
      impact: 'customer_facing',
      sensitivity: 'none',
    }).scope,
    'OWNER',
    'customer impact',
  );

  assertEqual(
    suggestSharingScope({
      purpose: 'status_check',
      impact: 'internal_only',
      sensitivity: 'personal_data_possible',
    }).scope,
    'RESTRICTED',
    'sensitive data',
  );

  assertEqual(
    suggestSharingScope({
      purpose: '',
      impact: 'internal_only',
      sensitivity: 'none',
    }).status,
    'REVIEW_REQUIRED',
    'missing purpose',
  );
}

運用時の確認ポイント

GASの候補判定を時間主導トリガーで実行する場合も、次の境界は人が確認します。

  • 顧客、従業員、契約、料金、苦情、公開情報への影響
  • 氏名、連絡先、認証情報、個別契約の有無
  • 要約が原文の意味を変えていないか
  • evidence_ref が最新版の正本を指しているか
  • REVIEW_REQUIRED を承認済みや再開可能と扱っていないか
  • 共有範囲を変更する場合の責任者、日時、記録先

特に、認証情報やセッション情報は要約にも含めません。共有が必要なときは、限定された担当者が権限管理された正本を確認し、他の担当者には判断に必要な状態と参照IDだけを渡します。

15分で始めるチェックリスト

  • 直近の判断ログを3件だけ選んだ
  • 共有目的を一行で書いた
  • 顧客・契約・料金・苦情・公開情報への影響を確認した
  • 個人情報・認証情報・契約情報の有無を確認した
  • 4つの scope の意味を担当者と確認した
  • 見せる項目と隠す項目を分けた
  • 正本の参照IDと版を記録した
  • 事実確認者と最終判断者を役割で指定した
  • 確認前のログを共有済み・承認済みと混同しないと決めた
  • 次回見直し日と保持・削除の確認担当を残した

まとめ

AI判断ログは、全員へ同じ原文を配るほど役立つものではありません。共有目的、影響度、機密性を先に記録し、GASには候補判定と確認キュー作成までを任せます。

  • 作業担当者には状態と次の行動を候補として渡す
  • 事実確認者には根拠と確認項目を候補として渡す
  • 業務オーナーには影響範囲と再開条件を候補として渡す
  • 機密性が高い原文は限定担当者へ戻す
  • 共有済み、確認済み、再開可能を別の状態で扱う

Miraigentの業務診断でも、ログを増やす前に「誰が何を判断するために読むのか」と「共有しない情報は何か」を確認します。まずは一件だけ、purpose、sensitivity、evidence_ref を埋め、空欄が残るなら共有範囲を広げず人間確認へ戻してください。

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?