個人情報を LLM に渡さずに、AI で下書きを作る — 置換して送り、戻ったら復元する
仕事で、お客様の情報を扱う業務ツールを作っています。その中に「担当者が書いたメモを、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 に何かを任せるとき、つい「全部渡してから、よしなにやってもらう」と考えがちです。でも個人情報がからむ場面では、学習に使われないといった一般的な前提を押さえた上で、さらに「そもそも渡さなくていいものは何か」を考えるほうが、結果としてシンプルで強い設計になりました。
- 学習されない・暗号化・委託先の整理といった一般的なガードはかける
- その上で、渡さずに済む情報はそもそも渡さない(データ最小化)
- 使う人の体験は変えない(隠して戻すのは裏でやる)
- これは完璧な匿名化ではなく、多層防御の一段目