問い合わせ対応にAIを組み込むと、FAQ検索や問い合わせ要約だけでなく、CRM更新や返信案の作成まで一つの処理にまとめたくなります。
しかし、AIに接続するツールを同じ権限で扱うと、確認前の下書きが業務状態を変更したり、外部送信につながったりします。この記事では、GoogleスプレッドシートとGASでツールの権限レーンを管理し、実行前に read / draft / write / deny を検査する最小構成を作ります。
AI APIの呼び出し、CRMの実接続、顧客への自動送信は扱いません。先に「何を読めるか」「何を作れるか」「どこから人間確認へ戻すか」をコードと設定で固定します。
作るもの
次の2枚のシートを用意します。
-
TOOL_PERMISSIONS: ツール名、権限レベル、対象範囲、許可フィールド、承認要否 -
PERMISSION_REVIEW_QUEUE: 権限不足や承認情報不足で止めた実行候補
完成状態は、各ツールに対して「許可された操作」と「止まった時の戻し先」を説明できることです。
問い合わせ
-> 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 へ進めない条件を一つ書くところから始められます。
