問い合わせをAIで要約すると、引き継ぎやCRM入力は速くなります。一方、フォーム本文をそのまま外部AIへ送る設計では、メールアドレス、電話番号、注文番号、認証情報など、本来不要な情報まで送信しやすくなります。
この記事では、Google SheetsとGoogle Apps Script(GAS)で、問い合わせを送信前に検査・匿名化し、危険な入力を人間確認へ止める最小構成を作ります。特定のAI APIへ接続する部分ではなく、その前段の安全ゲートに焦点を当てます。
この記事で作るもの
- AIへ送る項目を許可リストで限定
- メールアドレス、電話番号、URL内トークンらしき文字列を置換
- 認証情報や決済情報の疑いがある入力を自動送信しない
- 送信用本文のハッシュと判定理由を監査ログへ保存
- AIの要約結果をCRM登録前に人間が確認
完成状態は「安全と判定した文章だけを送信候補にし、要約結果も自動確定しない」ことです。
シートを3つに分ける
InquiryQueue は受付データ、SummaryQueue は送信候補、AuditLog は判定記録です。
InquiryQueue:
inquiry_id,received_at,category,subject,body,contact_ref,status
SummaryQueue:
inquiry_id,safe_subject,safe_body,risk_status,reason_codes,payload_hash,review_status,summary
AuditLog:
checked_at,inquiry_id,risk_status,reason_codes,payload_hash,reviewer,reviewed_at
連絡先の原文は contact_ref の参照先へ分離し、送信用キューへコピーしません。
許可する項目を先に決める
「禁止語を消せば安全」ではなく、AIへ渡してよい項目だけでペイロードを組み立てます。
const ALLOWED_CATEGORIES = new Set([
"product_question",
"support",
"estimate_request"
]);
function buildAllowedPayload(row) {
return {
inquiryId: String(row.inquiryId),
category: ALLOWED_CATEGORIES.has(row.category)
? row.category
: "other",
subject: String(row.subject || "").slice(0, 200),
body: String(row.body || "").slice(0, 4000)
};
}
氏名、会社名、メール、電話、住所、添付ファイルは、要約に必要と確認できるまで含めません。
決定的な置換ルールを作る
置換後の文章だけを送信候補にします。正規表現は完全な個人情報検出器ではなく、明らかな混入を減らす第一段階です。
function redactText(value) {
return String(value || "")
.normalize("NFKC")
.replace(
/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi,
"[EMAIL]"
)
.replace(
/(?:\+?81[- ]?)?0\d{1,4}[- ]?\d{1,4}[- ]?\d{3,4}/g,
"[PHONE]"
)
.replace(
/(https?:\/\/[^\s?]+)\?[^\s]+/gi,
"$1?[QUERY_REMOVED]"
)
.replace(
/\b(?:sk|pk|api)[-_][A-Za-z0-9_-]{16,}\b/g,
"[POSSIBLE_SECRET]"
);
}
日本語の氏名、住所、自由形式の顧客番号は正規表現だけでは安定して検出できません。入力フォーム側で項目を分け、本文へ書かない案内を置く方が安全です。
危険な入力をHOLDへ止める
認証情報、決済情報、事故、医療・健康、法的紛争などを示す語が含まれる場合は、置換後であっても自動送信しません。
const HOLD_RULES = [
{ code: "credential", pattern: /パスワード|暗証番号|秘密鍵|認証コード/i },
{ code: "payment", pattern: /カード番号|口座番号|CVV|セキュリティコード/i },
{ code: "incident", pattern: /情報漏えい|不正アクセス|事故|人身/i },
{ code: "sensitive_case", pattern: /診断結果|病歴|訴訟|弁護士/i }
];
function inspectRisk(text) {
const reasonCodes = HOLD_RULES
.filter(rule => rule.pattern.test(text))
.map(rule => rule.code);
return {
status: reasonCodes.length ? "HOLD" : "READY_FOR_REVIEW",
reasonCodes
};
}
READY_FOR_REVIEW は「AIへ送信済み」ではありません。担当者が匿名化結果を確認できる状態です。
送信候補と監査用ハッシュを作る
監査ログには本文そのものではなく、送信候補が後から同一か確認できるSHA-256を保存します。
function sha256Hex(value) {
return Utilities.computeDigest(
Utilities.DigestAlgorithm.SHA_256,
value,
Utilities.Charset.UTF_8
).map(byte => (byte + 256).toString(16).slice(-2)).join("");
}
function prepareSummaryCandidate(raw) {
const allowed = buildAllowedPayload(raw);
const combined = `${allowed.subject}\n${allowed.body}`;
const risk = inspectRisk(combined);
const safeSubject = redactText(allowed.subject);
const safeBody = redactText(allowed.body);
const serialized = JSON.stringify({
category: allowed.category,
subject: safeSubject,
body: safeBody
});
return {
inquiryId: allowed.inquiryId,
safeSubject,
safeBody,
riskStatus: risk.status,
reasonCodes: risk.reasonCodes.join(","),
payloadHash: sha256Hex(serialized),
reviewStatus: "PENDING"
};
}
原文をハッシュ化しただけでは匿名化になりません。外部送信に使うのは必ず safeSubject と safeBody から作ったペイロードです。
AI送信の直前にも確認する
送信関数は APPROVED の行だけを対象にします。サンプルでは誤送信を避けるため、実際のAPI呼び出しは実装しません。
function listApprovedPayloads(rows) {
return rows
.filter(row =>
row.riskStatus === "READY_FOR_REVIEW" &&
row.reviewStatus === "APPROVED"
)
.map(row => ({
inquiryId: row.inquiryId,
category: row.category,
subject: row.safeSubject,
body: row.safeBody
}));
}
APIキーはスプレッドシートやコードへ書かず、実行環境の秘密情報ストアで管理します。ログにもAuthorizationヘッダーや送信本文を出しません。
要約結果を自動確定しない
AIの出力は次の項目に分けると確認しやすくなります。
{
"summary": "相談内容を3文以内で記載",
"requested_action": "相手が求める次の行動",
"missing_information": ["確認が必要な項目"],
"confidence": "low | medium | high"
}
担当者は、原文への参照権限を持つ画面で以下を確認します。
- 意図や否定表現が反転していない
- 金額、期限、契約条件を作っていない
- 個人情報を要約へ復元していない
-
missing_informationを事実のように書いていない - 苦情、解約、事故、権限変更は担当部署へ引き継いだ
最低限のテスト
function testPrepareSummaryCandidate() {
const tests = [
{
name: "メールと電話を置換",
body: "連絡先は test@example.com、03-1234-5678 です",
expectedStatus: "READY_FOR_REVIEW",
mustNotContain: ["test@example.com", "03-1234-5678"]
},
{
name: "認証情報の記載を停止",
body: "認証コードを送ります",
expectedStatus: "HOLD",
mustNotContain: []
}
];
tests.forEach((test, index) => {
const result = prepareSummaryCandidate({
inquiryId: `T-${index}`,
category: "support",
subject: "問い合わせ",
body: test.body
});
if (result.riskStatus !== test.expectedStatus) {
throw new Error(`${test.name}: status mismatch`);
}
test.mustNotContain.forEach(value => {
if (result.safeBody.includes(value)) {
throw new Error(`${test.name}: redaction failed`);
}
});
});
}
本番前には、全角文字、海外番号、長文、添付、空欄、誤検知、置換漏れもテストしてください。
この構成で自動化しないこと
- 個人情報の完全検出
- 危険度の最終判断
- AI要約の無確認CRM登録
- 苦情、契約、決済、事故、権限変更への自動返信
- 保存期間や外部AIの利用条件の法的判断
組織のデータ分類、利用するAIサービスの規約、保存地域、学習利用設定、委託先管理を確認したうえで運用してください。
導入チェックリスト
- AIへ渡す項目を許可リストで定義した
- 原文と送信候補を別シート・別権限にした
- HOLD条件と確認担当者を決めた
- APIキーをコード・シート・ログへ残していない
- 送信候補のハッシュと理由コードを記録した
- 要約結果を人間が原文と照合する
- 保存期間と削除手順を決めた
- 誤送信時の停止・報告手順を決めた
Miraigentでは、AI導入そのものより先に、入力項目、送らない情報、人間確認、記録先を整理する無料診断を案内しています。まずは実データを使わず、架空の問い合わせで安全ゲートを通すところから始めるのが現実的です。
