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で検査する:read・draft・writeを分離する

0
Posted at

問い合わせ対応にAIを組み込むと、FAQ検索や問い合わせ要約だけでなく、CRM更新や返信案の作成まで一つの処理にまとめたくなります。

しかし、AIに接続するツールを同じ権限で扱うと、確認前の下書きが業務状態を変更したり、外部送信につながったりします。この記事では、GoogleスプレッドシートとGASでツールの権限レーンを管理し、実行前に read / draft / write / deny を検査する最小構成を作ります。

AI APIの呼び出し、CRMの実接続、顧客への自動送信は扱いません。先に「何を読めるか」「何を作れるか」「どこから人間確認へ戻すか」をコードと設定で固定します。

作るもの

次の2枚のシートを用意します。

  • TOOL_PERMISSIONS: ツール名、権限レベル、対象範囲、許可フィールド、承認要否
  • PERMISSION_REVIEW_QUEUE: 権限不足や承認情報不足で止めた実行候補

問い合わせ対応AIのツール権限をread・draft・writeに分け、人間確認を最後に置く構成

完成状態は、各ツールに対して「許可された操作」と「止まった時の戻し先」を説明できることです。

問い合わせ
  -> read: FAQ・対象履歴を参照
  -> draft: 要約・返信案・不足情報候補を作成
  -> 人間確認: 根拠・宛先・約束・対象IDを照合
  -> write: 承認済みの内部記録だけ更新
  -> 外部送信: AIからは実行しない

1. 権限レーンを設定シートで管理する

プロンプトに「安全に処理する」と書くだけでは、ツール追加時の境界が分かりません。設定をシートに分け、未登録ツールは拒否する構成にします。

tool_name level allowed_fields requires_approval stop_reason
lookup_faq read faq_id,version,body false faq_version_unknown
get_inquiry read inquiry_id,category,status false inquiry_id_invalid
create_reply_draft draft draft_id,evidence_refs,missing_fields true evidence_missing
set_review_status write review_status,reviewed_at true approval_missing
send_reply deny true external_send_human_only

シートの初期データ

const TOOL_PERMISSIONS = [
  ['lookup_faq', 'read', 'faq_id,version,body', false, 'faq_version_unknown'],
  ['get_inquiry', 'read', 'inquiry_id,category,status', false, 'inquiry_id_invalid'],
  ['create_reply_draft', 'draft', 'draft_id,evidence_refs,missing_fields', true, 'evidence_missing'],
  ['set_review_status', 'write', 'review_status,reviewed_at', true, 'approval_missing'],
  ['append_judgment_log', 'write', 'decision,reason,reviewer_id', true, 'approval_missing'],
  ['send_reply', 'deny', '', true, 'external_send_human_only'],
];

function setupToolPermissions() {
  const sheet = SpreadsheetApp.getActive().getSheetByName('TOOL_PERMISSIONS')
    || SpreadsheetApp.getActive().insertSheet('TOOL_PERMISSIONS');
  sheet.clearContents();
  sheet.getRange(1, 1, 1, 5).setValues([[
    'tool_name', 'level', 'allowed_fields', 'requires_approval', 'stop_reason',
  ]]);
  sheet.getRange(2, 1, TOOL_PERMISSIONS.length, 5).setValues(TOOL_PERMISSIONS);
}

send_reply のような外部送信は deny として登録します。人間が送信画面で最終確認する工程を残し、AIの write 権限に含めないことが重要です。

2. 設定をオブジェクトへ読み込む

GAS側では、ヘッダー名を使ってシートの行をルールへ変換します。列の追加や並び替えがあっても、位置ではなく列名で扱えます。

function loadToolRules_() {
  const sheet = SpreadsheetApp.getActive().getSheetByName('TOOL_PERMISSIONS');
  if (!sheet) throw new Error('TOOL_PERMISSIONS sheet is required');

  const values = sheet.getDataRange().getValues();
  const headers = values.shift();
  const index = Object.fromEntries(headers.map((name, i) => [name, i]));
  const rules = {};

  values.filter(row => row[index.tool_name]).forEach(row => {
    const level = String(row[index.level] || 'deny');
    if (!['read', 'draft', 'write', 'deny'].includes(level)) {
      throw new Error(`unknown permission level: ${level}`);
    }
    rules[row[index.tool_name]] = {
      level,
      allowedFields: String(row[index.allowed_fields] || '')
        .split(',').map(value => value.trim()).filter(Boolean),
      requiresApproval: row[index.requires_approval] === true
        || String(row[index.requires_approval]).toLowerCase() === 'true',
      stopReason: String(row[index.stop_reason] || 'rule_not_satisfied'),
    };
  });
  return rules;
}

不正なレベルを見つけたら処理を続けず、設定修正へ戻します。未知の設定を都合よく read に変換しないのがポイントです。

3. 実行候補を検査する

次に、ツール実行候補と人間の承認情報を受け取り、許可された操作だけを通します。以下は外部サービスを呼ばずに判定だけを行う例です。

function inspectToolExecution(execution, rules, approvedInquiryIds) {
  const rule = rules[execution.tool];
  if (!rule) return hold_(execution, 'tool_not_registered');
  if (rule.level === 'deny') return hold_(execution, rule.stopReason);

  if (rule.level === 'write') {
    if (rule.requiresApproval && !execution.approvalId) {
      return hold_(execution, 'approval_missing');
    }
    if (!approvedInquiryIds.has(execution.inquiryId)) {
      return hold_(execution, 'inquiry_not_approved');
    }
  }

  const fields = Object.keys(execution.fields || {});
  const allowed = new Set(rule.allowedFields);
  const unexpected = fields.filter(field => !allowed.has(field));
  if (unexpected.length > 0) {
    return hold_(execution, `field_not_allowed:${unexpected.join(',')}`);
  }

  return { status: 'ALLOW', tool: execution.tool, level: rule.level };
}

function hold_(execution, reason) {
  return {
    status: 'HOLD',
    tool: execution.tool,
    inquiryId: execution.inquiryId,
    reason,
    nextAction: 'human_review',
  };
}

この関数は認証・認可基盤の代わりではありません。実運用では、承認IDの発行元、承認の有効期限、対象レコードの所有権、監査ログへの記録成功も確認してください。

4. 止めた候補を確認キューへ記録する

HOLD を返すだけでは、誰も再確認しない可能性があります。確認キューには本文全体をコピーせず、内部ID、ツール、理由、戻し先を記録します。

function appendPermissionReview_(result) {
  const sheet = SpreadsheetApp.getActive().getSheetByName('PERMISSION_REVIEW_QUEUE')
    || SpreadsheetApp.getActive().insertSheet('PERMISSION_REVIEW_QUEUE');
  if (sheet.getLastRow() === 0) {
    sheet.appendRow([
      'created_at', 'inquiry_id', 'tool_name', 'reason', 'status', 'next_action',
    ]);
  }
  sheet.appendRow([
    new Date(),
    result.inquiryId || '',
    result.tool || '',
    result.reason || '',
    'OPEN',
    result.nextAction || 'human_review',
  ]);
}

問い合わせ本文、氏名、メールアドレス、契約・決済情報、認証情報はこのキューへ複製しません。必要な情報は正本の内部IDから権限内で参照します。

5. テストケースを先に作る

本番接続前に、少なくとも次のケースを実データなしでテストします。

function testPermissionLanes() {
  const rules = {
    lookup_faq: { level: 'read', allowedFields: ['faq_id'], requiresApproval: false },
    create_reply_draft: {
      level: 'draft', allowedFields: ['draft_id'], requiresApproval: true,
    },
    set_review_status: {
      level: 'write', allowedFields: ['review_status'], requiresApproval: true,
    },
    send_reply: { level: 'deny', allowedFields: [], requiresApproval: true,
    },
  };
  const approved = new Set(['sample-inquiry']);
  const cases = [
    ['read is allowed', 'lookup_faq', { faq_id: 'sample' }, undefined, 'ALLOW'],
    ['draft is not a write', 'create_reply_draft', { draft_id: 'draft-1' }, undefined, 'ALLOW'],
    ['write needs approval', 'set_review_status', { review_status: 'approved' }, undefined, 'HOLD'],
    ['write with approval is allowed', 'set_review_status', { review_status: 'approved' }, 'approval-1', 'ALLOW'],
    ['external send is denied', 'send_reply', {}, 'approval-1', 'HOLD'],
    ['unknown tool is held', 'unknown_tool', {}, undefined, 'HOLD'],
  ];
  cases.forEach(([name, tool, fields, approvalId, expected]) => {
    const actual = inspectToolExecution(
      { tool, inquiryId: 'sample-inquiry', fields, approvalId }, rules, approved,
    ).status;
    if (actual !== expected) throw new Error(`${name}: expected ${expected}, got ${actual}`);
  });
}

ここで write with approval is allowed となるのは、内部の限定フィールドを更新する候補だけです。顧客への送信や契約・料金の確定が ALLOW になるテストは作らないでください。

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

  • 対象業務と参照する正本が決まっている
  • すべてのツールに read / draft / write / deny のいずれかが付いている
  • 未登録ツールは HOLD になる
  • write の許可フィールドが限定されている
  • write に承認IDと対象問い合わせIDの照合がある
  • 下書き生成と業務状態の変更が別操作になっている
  • 外部送信、削除、契約・料金の確定はAIから実行できない
  • 情報不足、版ずれ、承認者不在の戻し先がある
  • 確認キューへ実顧客本文を複製していない
  • 設定変更、差し戻し理由、確認者を記録できる
  • 実データなしのテストケースを通している

まとめ

AIツールの安全性を「AIが賢く判断すること」に寄せると、ツール追加や担当者交代のたびに境界が曖昧になります。まずは、読む、下書きを作る、限定された内部記録を書く、外部へ送らない、を別レーンに分けてください。

MiraigentのAI導入相談でも、最初に確認するのは接続する製品の数ではなく、承認前に状態を変えてしまう操作がないかです。直近の問い合わせ一件を使い、draft から write へ進めない条件を一つ書くところから始められます。

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?