AI

個人情報を LLM に渡さずに、AI で下書きを作る — 置換して送り、戻ったら復元する

封蝋で閉じられた封筒
Photo by John-Mark Smith on Pexels

仕事で、お客様の情報を扱う業務ツールを作っています。その中に「担当者が書いたメモを、AI が丁寧な文面の下書きに整える」機能を足しました。

便利な機能なんですが、お客様の名前や連絡先みたいな個人情報を、外部の AI に渡すことになります。ここをどう設計するか、というのが今回の話です。

実装の細かいコードはほとんど出てきません。考え方が中心で、最後にひとつだけ、仕組みの核になる短いコードを載せます。

まず、送ったデータは学習には使われない

最初に、誤解されがちなところを押さえておきます。「個人情報を AI に送ると、その内容が AI の学習に使われてしまうのでは」と心配されることがあるんですが、業務で使う商用の LLM サービスでは、基本的にそうはなりません。

送った文章(入力)も、返ってきた文章(出力)も、モデルの再学習には使わない——というのが、商用利用での標準的な扱いです。むしろ学習に使われるのは、利用者が自分から許可したときに限られる、という構造になっています。

加えて、通信は暗号化されますし、データを処理するリージョンも国内に固定しています。「外部の AI に個人情報の処理を委託している」という整理も、個人情報保護法を踏まえて文書にしてあります。

つまり、一般的に押さえるべきところは、ひと通り押さえてある状態です。

それでも、送る範囲は最小にしたい

では「学習されないなら、全部そのまま送っていいか」というと、そこは別の話だと考えました。

個人情報の扱いでは、必要なものを必要な分だけ——という最小化の考え方が基本にあります。学習に使われないとしても、外部のサービスに送る個人情報は、少ないに越したことはありません。送る範囲が広いほど、通信経路やログ、「何をどこに送っているか」を説明する責任など、気を配る対象がそのぶん増えていきます。

そして冷静に考えると、AI に文章を整えてもらうのに、相手が誰かという情報は本質的には要りません。「○○様」が田中さんなのか佐藤さんなのかを知らなくても、丁寧な文面は作れるはずです。

であれば、送らずに済むものは送らない。一般的なガードをかけた上で、もう一段、渡す情報そのものを減らしておく。これが今回やったことです。

考えたのは「名前のところだけ伏せ字にする」

やったことはシンプルで、AI に渡す前に、名前や連絡先を目印に置き換えるだけです。

郵便で差出人を隠したいときに、宛名のところを記号で塗りつぶすのに近い感覚ですね。本文の中の「田中様」を {{CUSTOMER_NAME}} のような目印に置き換えてから AI に渡します。

あなたが書いたメモ
   │  ① 名前・連絡先を目印に置き換える(マスク)
   ▼
{{CUSTOMER_NAME}} 様の件について…
   │  ② この状態で AI に渡す
   ▼
AI(誰のことか分からないまま、文面だけ整える)
   │  ③ 返ってきた文章の目印を実名に戻す(復元)
   ▼
田中様の件について…   ← 画面に表示される

AI から見ると、相手が誰かは分かりません。{{CUSTOMER_NAME}} という記号が並んでいるだけです。それでも「丁寧な文面に整える」という仕事はちゃんとこなせます。整え終わった文章が返ってきたら、目印を実名に戻して画面に出す、という流れです。

送る前に隠して、戻ってきたら戻す

ポイントは、この操作が対称になっていることです。

送る前に「隠す」操作をして、返ってきたら「戻す」操作をする。隠したものと戻すものが1対1で対応していれば、利用する担当者から見ると、ふつうに名前入りの文面が出てくるように見えます。裏で隠して戻している、というのは意識しなくていい。

使う人の体験を一切変えずに、外に出る情報だけを減らせる。ここが気に入っているところです。

どこまでやるか、の割り切り

ここで割り切ったことが2つあります。

ひとつは、電話番号やメールアドレスも目印に置き換えるけれど、こちらは戻さない、という点です。整えてもらう文面に電話番号やメールが出てくる場面は想定していないので、AI の出力に目印が残っていても困りません。むしろ消えてくれて構わない、という扱いにしました。

もうひとつ、これは大事な前提なんですが、この置き換えは完璧な匿名化ではありません。機械的な置換なので、表記の揺れには弱いんですよね。「田中様」は隠せても、メモに「タナカさん」と書かれていたら、別の文字列として素通りしてしまいます。

なので、これだけで「個人情報は絶対に漏れない」とは言えません。位置づけとしては、多層で守るうちの一段目です。「渡さずに済むものは、そもそも渡さない」という一番外側の防御線で、ここで減らせるものを先に減らしておく。残りはログの扱いやアクセス権限など、別の層で守ります。

仕組みは、これくらいシンプルです

ここまで考え方の話をしてきましたが、実装そのものは短いです。特別なライブラリも要りません。文字列を置き換えるだけです。

// 送る前:名前や連絡先を目印に置き換える
function mask(text, ctx) {
  return text
    .replaceAll(ctx.customerName, "{{CUSTOMER_NAME}}")
    .replaceAll(ctx.itemName, "{{ITEM_NAME}}")
    .replace(PHONE_PATTERN, "{{PHONE}}")
    .replace(EMAIL_PATTERN, "{{EMAIL}}");
}

// 戻すとき:名前・案件名だけ実値に戻す(電話・メールは戻さない)
function unmask(text, ctx) {
  return text
    .replaceAll("{{CUSTOMER_NAME}}", ctx.customerName)
    .replaceAll("{{ITEM_NAME}}", ctx.itemName);
}

実際のコードでは、名前で検索をかける前に正規表現用の特殊文字をエスケープする、といった細かい処理は入っています(名前に記号が含まれていると検索が壊れてしまうので)。とはいえ骨格はこれだけです。「送る前に隠して、戻ってきたら戻す」をそのまま書いただけ、というのが伝わるかと思います。

まとめ

AI に何かを任せるとき、つい「全部渡してから、よしなにやってもらう」と考えがちです。でも個人情報がからむ場面では、学習に使われないといった一般的な前提を押さえた上で、さらに「そもそも渡さなくていいものは何か」を考えるほうが、結果としてシンプルで強い設計になりました。

  • 学習されない・暗号化・委託先の整理といった一般的なガードはかける
  • その上で、渡さずに済む情報はそもそも渡さない(データ最小化)
  • 使う人の体験は変えない(隠して戻すのは裏でやる)
  • これは完璧な匿名化ではなく、多層防御の一段目