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に任せる場合、プロンプトより先に決めたいのが権限境界です。AIへ渡してよい入力、下書き対象の業務、許可する操作、最終承認者が担当者の記憶だけにあると、ルールを変えた理由も、止めた理由も追えません。

この記事では、Googleスプレッドシートをポリシー台帳、Google Apps Script(GAS)を判定器として使い、問い合わせを AI下書き可 または 人間確認 に振り分ける最小構成を作ります。AI APIとメール送信には接続しません。

この記事で作るもの

処理は次の順序です。

  1. policy_rules シートから有効なルールを読み込む
  2. 入力禁止項目、対象業務、許可操作を順に判定する
  3. 1件でも拒否条件に当たれば、AIへ本文を渡さず人間確認へ戻す
  4. 判定理由とポリシー版を decision_log に記録する
  5. policy_tests の固定ケースで変更前後を検証する

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

この例ではAIに許可するのは分類、要約、返信下書きだけです。外部送信、返金承認、契約変更は常に拒否します。

権限境界を4つに分ける

単一の ai_allowed 列だけでは、何を理由に止めたのか分かりません。次の4層を別々に判定します。

境界 確認すること 失敗時
入力 AIへ渡せない情報を含まないか 本文を渡さず人間確認
業務 AI下書きの対象カテゴリか 担当部署へ戻す
行動 実行しようとする操作が許可済みか 操作を拒否
承認 最終確認者と期限が設定されているか 承認待ち

個人情報を正規表現だけで完全に検出することはできません。ここで作る入力境界は入口のガードであり、個人情報管理や法務判断の代替ではありません。

シートを3枚用意する

policy_rules

policy_version rule_id layer target effect reason priority enabled
2026-07-17 deny_secret input api_key,token,password deny 認証情報の可能性 10 TRUE
2026-07-17 deny_refund case refund,contract,complaint deny 金銭・契約・苦情 20 TRUE
2026-07-17 allow_faq case faq,product_usage allow 一般案内 100 TRUE
2026-07-17 allow_draft action draft_reply allow 下書きのみ許可 100 TRUE
2026-07-17 deny_send action send_reply deny 外部送信は禁止 10 TRUE

priority は小さい数字を先に評価します。拒否ルールを許可ルールより上に置きます。

inquiry_queue

問い合わせ本文そのものをログへ複製しないため、判定対象と結果を分けます。

inquiry_ref category requested_action message reviewer_role status
INQ-001 faq draft_reply 営業時間を教えてください support_lead new

decision_log

decided_at inquiry_ref policy_version result reason_code reviewer_role

本文、メールアドレス、電話番号、認証情報は decision_log に保存しません。参照IDと判断だけを残します。

GASでポリシーを読み込む

const SHEETS = {
  rules: 'policy_rules',
  queue: 'inquiry_queue',
  log: 'decision_log',
  tests: 'policy_tests',
};

function readPolicyRules_(sheet) {
  const values = sheet.getDataRange().getValues();
  const headers = values.shift();
  const col = columnMap_(headers);

  return values
    .filter((row) => row.some((cell) => cell !== ''))
    .filter((row) => toBoolean_(row[col.enabled]))
    .map((row) => ({
      version: String(row[col.policy_version]),
      ruleId: String(row[col.rule_id]),
      layer: String(row[col.layer]),
      targets: String(row[col.target])
        .split(',')
        .map((value) => normalize_(value))
        .filter(Boolean),
      effect: String(row[col.effect]).toLowerCase(),
      reason: String(row[col.reason]),
      priority: Number(row[col.priority] || 999),
    }))
    .sort((a, b) => a.priority - b.priority);
}

実運用では、同時に複数のポリシー版を有効にしない、版を変更する時はレビューを通す、過去版を削除せず無効化する、という運用ルールも必要です。

デフォルト拒否で判定する

許可条件に一致しない問い合わせを自動的に許可しないよう、初期値を拒否にします。

function evaluateInquiry_(input, rules) {
  const version = rules[0]?.version || 'unknown';
  const message = normalize_(input.message);

  const blockedInput = firstMatch_(rules, 'input', message, 'deny');
  if (blockedInput) {
    return deny_(version, blockedInput, 'blocked_input');
  }

  const deniedCase = exactMatch_(rules, 'case', input.category, 'deny');
  if (deniedCase) {
    return deny_(version, deniedCase, 'human_only_case');
  }

  const deniedAction = exactMatch_(rules, 'action', input.action, 'deny');
  if (deniedAction) {
    return deny_(version, deniedAction, 'forbidden_action');
  }

  const allowedCase = exactMatch_(rules, 'case', input.category, 'allow');
  const allowedAction = exactMatch_(rules, 'action', input.action, 'allow');
  if (!allowedCase || !allowedAction) {
    return {
      allowed: false,
      policyVersion: version,
      ruleId: 'default_deny',
      reasonCode: 'no_explicit_allow',
      reason: '明示的な許可ルールがありません',
    };
  }

  if (!String(input.reviewerRole || '').trim()) {
    return {
      allowed: false,
      policyVersion: version,
      ruleId: 'approval_required',
      reasonCode: 'reviewer_missing',
      reason: '最終確認者が設定されていません',
    };
  }

  return {
    allowed: true,
    policyVersion: version,
    ruleId: `${allowedCase.ruleId}+${allowedAction.ruleId}`,
    reasonCode: 'draft_allowed_with_review',
    reason: '下書き作成後に人間確認が必要です',
  };
}

function firstMatch_(rules, layer, value, effect) {
  return rules.find((rule) =>
    rule.layer === layer &&
    rule.effect === effect &&
    rule.targets.some((target) => value.includes(target))
  );
}

function exactMatch_(rules, layer, value, effect) {
  const normalized = normalize_(value);
  return rules.find((rule) =>
    rule.layer === layer &&
    rule.effect === effect &&
    rule.targets.includes(normalized)
  );
}

function deny_(version, rule, reasonCode) {
  return {
    allowed: false,
    policyVersion: version,
    ruleId: rule.ruleId,
    reasonCode,
    reason: rule.reason,
  };
}

function normalize_(value) {
  return String(value || '').trim().toLowerCase();
}

function columnMap_(headers) {
  return Object.fromEntries(
    headers.map((header, index) => [String(header).trim(), index])
  );
}

function toBoolean_(value) {
  return value === true || String(value).toUpperCase() === 'TRUE';
}

このコードは password などの文字列を見つけたら、本文をAIへ送る前に止めます。ただし、秘密情報の全形式を網羅するものではありません。フォーム側で入力項目を制限し、必要に応じて専用の検出・マスキング処理を組み合わせます。

キューを判定してログへ残す

function evaluateInquiryQueue() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const queueSheet = ss.getSheetByName(SHEETS.queue);
  const logSheet = ss.getSheetByName(SHEETS.log);
  const rules = readPolicyRules_(ss.getSheetByName(SHEETS.rules));

  const values = queueSheet.getDataRange().getValues();
  const headers = values.shift();
  const col = columnMap_(headers);

  values.forEach((row, index) => {
    if (String(row[col.status]) !== 'new') return;

    const decision = evaluateInquiry_({
      category: row[col.category],
      action: row[col.requested_action],
      message: row[col.message],
      reviewerRole: row[col.reviewer_role],
    }, rules);

    logSheet.appendRow([
      new Date(),
      row[col.inquiry_ref],
      decision.policyVersion,
      decision.allowed ? 'review_queue' : 'human_only',
      decision.reasonCode,
      row[col.reviewer_role],
    ]);

    queueSheet.getRange(index + 2, col.status + 1)
      .setValue(decision.allowed ? 'review_queue' : 'human_only');
  });
}

review_queue は送信許可ではありません。AI下書きを人が確認する待ち行列です。送信処理は別関数・別権限に分け、この記事のスクリプトから呼べない構成にします。

ポリシー変更を固定ケースでテストする

policy_tests に最低限、次のケースを用意します。

test_id category action message reviewer_role expected_allowed
T001 faq draft_reply 営業時間を教えてください support_lead TRUE
T002 refund draft_reply 返金を相談したい support_lead FALSE
T003 faq send_reply 営業時間を案内 support_lead FALSE
T004 faq draft_reply api_key=example support_lead FALSE
T005 unknown draft_reply 質問があります support_lead FALSE
T006 faq draft_reply 営業時間を教えてください 空欄 FALSE
function runPolicyTests() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const rules = readPolicyRules_(ss.getSheetByName(SHEETS.rules));
  const sheet = ss.getSheetByName(SHEETS.tests);
  const values = sheet.getDataRange().getValues();
  const headers = values.shift();
  const col = columnMap_(headers);

  const results = values.map((row) => {
    const actual = evaluateInquiry_({
      category: row[col.category],
      action: row[col.action],
      message: row[col.message],
      reviewerRole: row[col.reviewer_role],
    }, rules);
    const expected = toBoolean_(row[col.expected_allowed]);
    return [actual.allowed === expected ? 'PASS' : 'FAIL'];
  });

  if (results.length) {
    sheet.getRange(2, col.result + 1, results.length, 1).setValues(results);
  }
}

ポリシーを変更したら、全テストが PASS になるまで本番用の版を切り替えません。許可ケースだけでなく、拒否ケースと未定義ケースを必ず含めます。

公開前・運用開始前チェックリスト

  • AIへ許可する操作を列挙した
  • send_reply、返金、契約変更などの禁止操作を明記した
  • 未定義ケースをデフォルト拒否にした
  • 問い合わせ本文を判断ログへ複製していない
  • ポリシー版、ルールID、理由コードを記録している
  • AI下書きと外部送信を別権限にした
  • 承認者が空欄なら止まる
  • 許可、拒否、未定義、機密らしき入力のテストがある
  • ポリシー変更者と承認者を分けた
  • 障害時に draft_only または停止へ切り替える手順がある

この構成で自動化しないこと

この実装は、AIに渡す前の権限判定と判断記録だけを扱います。次は自動化しません。

  • 顧客への外部送信
  • 返金、契約、法務、苦情の最終判断
  • 個人情報・機密情報の完全検出
  • ポリシーの自動承認
  • 障害後の自動再開

まず下書きだけに限定し、拒否理由と人間判断を記録できる状態を作ると、AIの精度改善と運用責任を混同しにくくなります。

Miraigentでは、AI導入前の無料診断で、入力情報、対象業務、許可操作、人間確認、判断ログの境界を整理しています。ツール導入前に「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?