設計

本番での PII スキャン

上流呼び出しの前にプロンプトをスキャンするのに実際に何が必要か — カテゴリ、検証、偽陽性の取扱い、そして block がスキャナの決定でなくポリシーの決定である理由。

SchneeAI2026-07-0313 分読了

事後 vs 呼び出し前

LLM アプリケーションのほとんどの PII 検出は事後 — リクエストがプロバイダーに届き、レスポンスが返ってきて、それからスキャナが保存されたコンテンツをレビューし、マスキングされるべきだったものをフラグ。これが、目標が何が起きたかを知ることだけなら問題ない。目標が漏洩を防ぐことなら問題がある。

その違いは、インシデント報告とコントロールの違い。

SchneeAI は上流呼び出しの前にスキャン。ポリシーが block と言えば、リクエストはプロバイダーに届かない。ユーザーは構造化されたエラーを見る。運用チームは監査イベントを見る。raw プロンプトはゲートウェイを離れない。

シンプルに聞こえる。そうではない。リクエストレイテンシで、低い偽陽性率で、異なるポリシーを持つテナント横断で実行するのが、より難しい設計問題。

スキャナが見るもの

プロンプトは単なるテキストではない。スキャナに届くまでに、それは:

  • システムプロンプト(多くはテナント構成)
  • ユーザーメッセージ(自由テキスト、おそらく構造化)
  • ツール呼び出しパラメータ(JSON、多くは文字列埋め込み)
  • ファイルコンテンツ(PDF、OCR 経由の画像から抽出されたテキスト)
  • 会話履歴(以前のスキャンからマスキングされたコンテンツを含みうる前のターン)

ユーザーメッセージだけを見るスキャナはシステムプロンプトを見落とす。リクエスト全体を 1 つのブロブとして見るスキャナは、発見場所の場所を伝えられない。スキャナはフィールド毎に動作し、発見と共にフィールドパスを捕捉する必要。

「PII あり/なし」ではなく 17 カテゴリ

SchneeAI は 17 の検出カテゴリを出荷:

カテゴリ重大度検証ステップ
credit_card_numbercriticalLuhn チェックサム
us_ssncriticalエリア/グループ/シリアル規則
us_passportcritical書式 + チェックサム
us_drivers_licensewarning書式のみ
us_bank_accountcriticalABA ルーティングチェック
ibancriticalIBAN mod-97
swift_bicinfo書式のみ
japan_mynumbercriticalmod-11 チェックデジット
japan_banking_accountwarning書式 + 銀行コード照会
email_addressinfoRFC 5322 簡略化
phone_numberinfoE.164 正規化
ip_addressinfoIPv4/IPv6 書式
mac_addressinfo書式のみ
api_key_genericcriticalエントロピー + プレフィックス一致
aws_access_key_idcritical書式 (AKIA…)
github_patcritical書式 (ghp_…)
private_keycriticalPEM ヘッダー + 構造

なぜこのリスト?「規制当局が気にするもの」(クレジットカード、SSN、マイナンバー、IBAN)、「攻撃者が即座に使えるもの」(API 鍵、秘密鍵、PAT)、「個人が特定できるが日常的なもの」(メール、電話、IP) の和集合。

第 3 のグループがリストにいるのは、一部のテナントがフラグを望むため。他のテナントは望まない。重大度とアクションはポリシーであって検出ではない。

重大度はヒントであって評決ではない

各カテゴリにはデフォルトの重大度があるが、重大度だけでは何が起きるかを決めない。テナントのポリシーが決める:

  • critical → デフォルト blockmask に格下げ可能
  • warning → デフォルト maskflag に格下げまたは block に格上げ可能
  • info → デフォルト flag、厳格なテナントは mask に格上げ可能

SchneeAI を動かす病院は email_address を mask に設定するかもしれない。コンシューマー チャットボットは flag のまま。スキャナは重大度付きで発見を発信。ポリシーエンジンがアクションを決定。

この分割の理由 — 検出はほぼ客観的(文字列がクレジットカード番号として有効かそうでないか)。アクションは文脈依存(その発見が block すべきかはテナント、プロンプト、時にはユーザーの同意状態に依存)。

検証ステップ

検証のない検出は偽陽性を生む。パターン 4111-1111-1111-1111 はクレジットカード形状の文字列。それが本物のカード番号かは Luhn チェックサム次第。パターン 123-45-6789 は SSN 形状の文字列。それが本物の SSN かはエリア/グループ妥当性規則次第。

決定論的な検証ステップを持つカテゴリについて、スキャナは発見を発する前に実行:

正規表現一致を検出 → チェックサムを検証 → 発見を発する
                    検証失敗 → 発見を抑制

これにより検出を弱めることなく偽陽性を桁違いに削減。検証ステップを持つカテゴリは、検証が安価で信頼できるもの — credit_card_number (Luhn)、iban (mod-97)、japan_mynumber (mod-11)、us_ssn (エリア/グループ)、aws_access_key_id (書式)。

検証ステップのないカテゴリ — email_addressphone_numberapi_key_generic — スキャナは書式一致を受け入れ、ノイズの処理はポリシーに依存。ほとんどのテナントはこれらを block ではなく flag に設定。

オーバーラップの解決

1 つの文字列が複数カテゴリに一致しうる。4111 1111 1111 1111 はクレジットカード形状でもあり、正規表現が緩ければ電話番号にも。ghp_xxxx... は GitHub PAT だが、汎用 api_key パターンにも。

スキャナは優先順位でオーバーラップを解決:

  1. 検証付き critical (例: Luhn 通過の credit_card_number) が phone_number に勝つ。
  2. 特定 (github_pat) が 汎用 (api_key_generic) に勝つ。
  3. 同順位ならより高い重大度が勝つ。

出力は一致範囲毎に 1 つの発見で、解決されたカテゴリ付き。優先度の低い一致は個別の発見としてではなく、発見メタデータの代替として記録。

スキャナが発火したときの対応

ポリシーエンジンが重大度を 3 つのアクションのいずれかにマップ:

flag

発見は監査ログとインタラクションメタデータに記録されるが、リクエストは変更せずに進む。ユースケース: 運用チームが中断なしで可視性を望む低重大度の検出。

mask

検出された部分文字列は上流呼び出しの前にカテゴリラベル付きトークンで置換。4111 1111 1111 1111[CREDIT_CARD_NUMBER] になる。モデルはマスクを見る。オリジナルは送信されない。ユースケース: PII なしでもプロンプトが有用な中重大度の検出。

マスクは Vault 経由でのみ可逆 — オリジナルは発見 ID でリンクされ、インタラクションと共に暗号化されて保存。運用はインシデント調査のために必要ならオリジナルのプロンプトを再構築可能。

block

リクエストは上流呼び出しの前に拒否。呼び出し元は発見カテゴリ、重大度、フィールドパス付きの構造化エラーを受信。監査ログがブロックされたインタラクションを捕捉。ユースケース: プロンプトを送ることより拒否する方がマシな critical な検出。

block は最も強いアクション。同時にエンドユーザーに最も見える — ブロックされたリクエストは失敗したリクエント。critical → block を設定するテナントは、スキャナを引っ掛ける正当なプロンプトの計画が必要 (critical カテゴリの偽陽性は稀だが実在する)。

Vault との接続

すべての発見 — flag、mask、block — は監査ログに以下と共に書かれる:

  • インタラクション ID
  • カテゴリ、重大度、取られたアクション
  • 発見が検出されたフィールドパス
  • mask の場合: マスクトークンとオリジナルへの Vault ポインタ
  • block の場合: 呼び出し元に返された拒否理由

raw の一致部分文字列は監査ログに決して書かれない。mask アクションについて、オリジナルはインタラクションと同じ保持クラスで Vault に保存。block アクションについて、オリジナルはインシデント調査のために短い保持ウィンドウ (デフォルト 30 日) で Vault に保存され、その後消去。

この分離は Usage Ledger / Vault 分割と同じパターン — 広く分析に有用なメタデータはあるストアに、アクセスが重要な raw コンテンツは別のストアに。

レイテンシ

スキャナはリクエストパスで実行。高速である必要。

検出は安価な検証ステップ付きの階層化正規表現マッチャーのセット。リファレンスデプロイで、フルリクエストスキャンレイテンシは典型的なプロンプト (合計コンテンツ 4KB 未満) で一桁ミリ秒、大きなリクエスト (50KB 超) で 20ms 未満。これはどの LLM プロバイダーへのラウンドトリップ時間より十分に短い。

高コストのケースはファイルコンテンツ — OCR された PDF、文字起こしされたオーディオ — 抽出テキストが数万文字になりうる。これらについて、スキャナはファイル抽出と並列に実行されるため、限界コストは通常、抽出時間の背後に隠れる。

スキャナが誤っているとき

偽陽性は起きる。2 つのケースが問題。

検証付き偽陽性。 検証ステップを持つカテゴリが、それでも非 PII 文字列で発火。例: たまたま Luhn を通過する 16 桁の注文番号。稀だが実在。解決: テナントは特定のフィールドパスにスコープされた許可ルールを追加 (例: 「ツール呼び出し lookup_order 内の注文番号はクレジットカード番号ではない」)。スキャナは一致したフィールドで検出をスキップ。

ポリシー不一致。 検出は正しいがアクションが厳格すぎる。例: テナントのカスタマーサポートトランスクリプトに顧客が入力したクレジットカード番号が含まれ、テナントはこれを block ではなく mask したい。解決: テナントは credit_card_numberblock から mask に格下げ。スキャナの挙動は同一。ポリシーだけが変わる。

スキャナはポリシーについて意見を持たない。検出と検証をする。テナントがどうするかを決める。

持ち帰るべきこと

呼び出し前 PII スキャンを構築しているなら:

  1. 検出をポリシーから分離。 スキャナは発見を発し、ポリシーエンジンがアクションを決める。混ぜると両方の推論が難しくなる。
  2. 可能なら検証。 安価なチェックサム (Luhn、mod-97、mod-11) が偽陽性を劇的に削減。安価なチェックが存在しないカテゴリについてのみ検証をスキップ。
  3. リクエスト毎でなくフィールド毎にスキャン。 発見がどこにあるかを知ることは何かを知ることと同じくらい重要。
  4. block はスキャナの決定でなくポリシーの決定。 ブロックするスキャナは格下げできないスキャナ。
  5. すべての発見を監査、raw の一致はログしない。 発見メタデータで十分。オリジナルはどこかに行く必要があれば Vault へ。

PII スキャンは、デモは簡単だが運用は難しい機能のひとつ。作業は正規表現ではなく、ポリシーモデル、検証ステップ、偽陽性の取扱い、監査トレイルにある。


スキャナは SchneeAI の呼び出し前コントロールのひとつ。プロダクト概要を読んでゲートウェイ、Vault、ポリシーエンジンとの組み合わせを確認するか、PII ハンドリング要件について会話を始めてください。