← Blog
2026-09-18 · 6 min read

PIIマスクをLLM無しで4層・0.1msにした
— 遅くて「壊れて見えた」反省から

Sente(先手)には、送信直前にこのMac上で氏名・住所・電話・鍵をプレースホルダに置き換えてから teai.io へ送る オプトイン機能があります(8月の記事)。当初はローカルLLM(Ollama)で 検出していましたが、9月17日に有効化した本人環境で「先手が動かない」状態になりました。 原因を追い、検出をLLMから完全に切り離して4層に作り直したのが今回の話です。

3行まとめ
# 有効化(Ollama不要・swiftc があれば Apple の固有表現認識も自動ビルド)
te privacy scrub on

# 連絡先と git の名前を辞書に(このMacの外に出ません・1回だけ)
te privacy scrub harvest

# 送らずに確認(所要msも出ます)
te privacy scrub preview --file ./sample.txt

何が起きたか

症状は「TUIは立ち上がるが答えが返らない」。ログを追うと、マスク用プロキシが毎リクエストの全文を 800字ごとに Ollama(qwen3:8b)へ投げ、プロセス内のロックで直列に待っていました。エンジンは毎ターン数十KBの システムプロンプトとツール定義を送り直すので、それだけで数十回の推論。加えて放置された先手のセッションが 十数個残っていて、長時間の te run がファイルを読むたびに同じ検査を走らせ、1台のOllamaを 全員で取り合っていました。te run "1たす1は?" の実測が112秒。画面に何も出ないので、 使う側には「固まった」としか見えません。

判断は一つでした。検出の遅さは精度より先に「壊れている」体験になる。LLMを使わずに 即時で動くものを既定にし、LLMは重ねたい人だけのオプションにする。

4層の中身

層拾うもの実装
1. パターンメール・電話・郵便番号+住所・主要サービスの鍵(GitHub / Stripe / Slack / Google / AWS / JWT / npm / Fly / Tailscale ほか)・PEM秘密鍵・高エントロピー文字列・[secret]…[/secret]・password= の値正規表現+エントロピー計算。hex(コミットSHA)・UUID・パスは除外
2. 辞書自分の連絡先の名前・組織名、git の名前、手書きの固定語te privacy scrub harvest が macOS 連絡先から収集。英数字語は単語境界で照合(raven が ravenous を壊さない)。隠さない語は scrub-allow.txt
3. 日本語ヒューリスティック「〜さん / 様 / 氏 / 先生」直前の名前、法人格(株式会社・Inc. など)の前後、都道府県+市区郡町村から始まる住所「お客様」「皆さん」「株式会社の設立方法」「東京都内」は残す。分かち書きは使わない
4. Apple固有表現認識人名・組織名macOS 内蔵の NaturalLanguage。Swift ソースを同梱し、swiftc があれば on 時に1秒でビルド。常駐プロセスに1行JSONで問い合わせ。失敗したらこの層だけ外す

4層の検出結果は「原文の部分文字列」として集め、出現範囲の和集合を1つのプレースホルダに置きます。 応答が返ったら対応表で復元。対応表はリクエストごとにメモリ上だけ。Ollamaを重ねる --llm は残していますが、 ラベルもヘルプも「実験用・遅い」に変えました。

実測

条件所要
1リクエスト(4層・Apple層を除く)0.1ms
60KBのテキスト10ms
Apple層込みの preview(ヘルパー起動含む)40ms
te run 一往復4〜5秒(Ollama検出時は28〜53秒、障害時112秒)
顧客は青葉花子、担当の田中太郎さんと株式会社星雲工房。John Smith from Acme Corporation. 連絡先 aoba@example.invalid 090-1234-5678 token=ghp_ABCD… → 顧客は青葉花子、担当の[PRIVATE_…_1]さんと[PRIVATE_…_2]。[PRIVATE_…_3] from [PRIVATE_…_4] Corporation. 連絡先 [PRIVATE_…_5] [PRIVATE_…_6] [PRIVATE_…_7]

上の例がそのまま限界を示しています。「青葉花子」は残ります。敬称も付いておらず、辞書にも無い名前は どの層も拾えません。「田中太郎さん」は敬称で、「星雲工房」は法人格で、John Smith は Apple の層で拾えています。

踏んだバグ: 重なりで後半が漏れる

「090-1234-5678 token=ghp_…」という並びで、郵便番号のルール(3桁-4桁+40文字)が電話番号から先を巻き込み、 鍵のルールと範囲が重なりました。検出結果を長いものから順に replace していたため、先に長い方が プレースホルダになった時点で短い方の文字列が見つからなくなり、鍵の後半14文字が平文で残る状態に。 マスクを謳う機能で最悪の形です。置換を「全出現位置を集めて範囲の和集合を作り、まとめて1つに置く」方式に変え、 回帰テストを足しました。郵便番号のルールも後続を日本語の住所文字に限定しています。

正直に書いておきます: Apple の固有表現認識は英語の人名・組織名には効きますが、日本語の人名は実測でほぼ空でした。日本語は敬称と辞書で 担うので、te privacy scrub harvest で連絡先を辞書に入れるかどうかで実効性が大きく変わります。 どの層もパターンか辞書かモデルの範囲でしか拾えず、完全な検出は保証しません。「送る前に、手元でもう一段壁を作る」 ためのオプトイン機能である点は8月から変わっていません。応答の一括表示(ストリーミング無し)も同じです。

コマンド早見表

コマンド動作
te privacy scrub on次回起動から有効(内蔵4層・Ollama不要)
te privacy scrub harvest連絡先+git の名前を辞書に(許可ダイアログ・Mac の外に出ない)
te privacy scrub preview --file F送らずに結果と所要msを確認
te privacy scrub status有効・層・辞書件数
te privacy scrub on --llm実験用: Ollama の検出を重ねる(数十秒〜数分遅くなる)
~/.config/teai/scrub-terms.txt / scrub-allow.txt必ず隠す語 / 隠さない語(1行1語)
te privacy scrub off無効化(既定)

関連記事