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で要約すると、引き継ぎや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"
  };
}

原文をハッシュ化しただけでは匿名化になりません。外部送信に使うのは必ず safeSubjectsafeBody から作ったペイロードです。

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導入そのものより先に、入力項目、送らない情報、人間確認、記録先を整理する無料診断を案内しています。まずは実データを使わず、架空の問い合わせで安全ゲートを通すところから始めるのが現実的です。

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?