我正在開發一項法律科技產品:AI 助理負責回答真實案件的問題,當中包含客戶姓名、國民身分證統一編號、IBAN 帳號、電話號碼等各種個人資料。這些對話會送往我無法控制的 LLM,因此絕對不能直接傳送原始內容。
最直覺的做法是用一堆 regex 把看起來像身分證號的字串遮蔽,但這個方法兩面不討好:假陽性(把不是身分證號的內容也遮掉)與假陰性(漏掉格式特例),更嚴重的問題是它是單向的——一旦把資料遮蔽,就無法再得到有意義的回應,因為模型現在只能在 [REDACTED] 與 [REDACTED] 之間進行推理。
我想要的是能正確偵測 PII、替換成模型可推理的穩定占位符,之後再把真實數值還原到回應中的工具。這就是 Laranon。
如何安裝
它以單一 Composer 套件的形式發行:
composer require edulazaro/laranon
Enter fullscreen mode Exit fullscreen mode
這就是全部所需。預設情況下 Laranon 不會把任何東西存進資料庫:一次聊天輪次使用記憶體中的對應表,請求結束即消失。只有在需要佇列工作時,才會用到可選的資料庫保險庫,我稍後會再說明。
如何使用
送出時匿名化、回傳時還原。對應表永遠不會離開你的伺服器。
use EduLazaro\Laranon\Laranon;
$result = Laranon::anonymize(
'Client John Smith, SSN 536-90-4399, wants the transfer to GB29 NWBK 6016 1331 9268 19.'
);
$result->text;
// "Client «PER_1» «AP_1», SSN «SSN_1», wants the transfer to «IBAN_1»."
$reply = $chat->send($result->text);
$result->restore($reply);
// The tokens become the real values again.
Enter fullscreen mode Exit fullscreen mode
這就是全部概念。接下來說明讓它值得信賴的機制,以及如何把它整合進實際應用程式。
為什麼它不只是 regex
幾項設計選擇讓它從示範走向真正值得託付客戶資料的解決方案。
檢查碼,而非僅靠模式
西班牙 DNI 並非「八位數字加一個字母」,而是八位數字加上正確的 mod-23 控制字母。Laranon 會驗證該字母;IBAN 使用 mod-97、信用卡使用 Luhn 加上 IIN,同樣適用於 NIE、CIF、NSS、CCC。帶有錯誤控制字母的 12345678A 不會被標記,藉此大幅降低單純 regex 清理工具常見的假陽性。
依單字產生的姓名 token
姓名會依單字而非整個人名進行 token 化,也不會推測身分。「John Smith」會變成 «PER_1» «AP_1»,名字與姓氏各自獲得獨立的穩定 token。之後若再出現單獨的「John」,仍會被替換為 «PER_1»,因為 token 屬於該單字而非該個人。「Mr. Baker」會共用「John Baker」的 «AP_2»。敬語與連接詞保持明文(「John de la Cruz」會顯示為 «PER_1» de la «AP_3»)。這與人類讀者所能取得的資訊完全一致,不多也不少。
替換永不重複
token 對應表保證兩個不同的值不會共用同一個占位符。若發生這種情況,兩個人的資料會被合併,導致還原失敗。
精確且可逆的還原
每一個 token 都會對應回原始文字的位元組內容。它也支援串流:即使 token 被切分到兩個 SSE chunk(甚至切到多位元組的 « 內),仍會被緩衝並正確還原。
Sessions:適合 LLM 輪次的結構
在一次聊天輪次中,你需要一個記憶體內的對應表,涵蓋整個 prompt(使用者訊息、檢索到的上下文、工具結果),並在請求結束時消失。這就是 session:一個臨時物件,擁有自己的對應表、不持久化任何東西,隨請求結束而消失。
use EduLazaro\Laranon\Anonymizer;
$anon = Anonymizer::create();
$messages = $anon->anonymize($messages, 'content'); // tokenize the prompt
$reply = $anon->restore($model->send($messages)); // real values back in the answer
// $anon goes out of scope here. The map is gone. Nothing was stored.
Enter fullscreen mode Exit fullscreen mode
anonymize() 與 restore() 可接受字串、清單,或是訊息清單中的 key path,包含巢狀的 dot path 與 * 萬用字元:
$anon->anonymize($messages, 'content');
$anon->anonymize($messages, 'tool_calls.*.function.arguments');
Enter fullscreen mode Exit fullscreen mode
這裡的 $messages 是符合 OpenAI chat 格式的普通 PHP 陣列(role、content、tool_calls…)。Laranon 既不定義也不要求此格式:就像 Laravel 的 data_get() 一樣,它只會依照你提供的 dot path 走訪任何巢狀陣列,並對找到的字串進行匿名化。角色、id、工具名稱及其他所有內容均不受影響。
由於你保留明文的聊天歷史(真實數值),因此不需要持久化任何已匿名化的內容。每一次輪次都會建立新的 session,並從頭重新匿名化整個 prompt。token 會以相同方式產生(依閱讀順序決定),因此多輪對話可在零狀態的情況下保持連貫。
佇列工作需要 scope,而非 session
session 存在於記憶體中,並隨請求結束而消失,這對同步聊天輪次而言完全正確。但佇列工作不同:它稍後在另一個行程中執行,遠早於建立 session 的請求已經消失。此時已沒有記憶體內的對應表可供分享。
此時應改用以持久化保險庫為後端的 scope。對應表會以你的應用程式金鑰加密,儲存在你指定的 key 下,讓工作可以重新開啟並還原:
// In the request
$safe = Laranon::scope("job-{$id}")->anonymize($text);
ProcessWithLlm::dispatch($safe->text, $id);
// Later, inside the queued job (a different process)
$reply = Laranon::scope("job-{$id}")->restore($model->send($payload));
Laranon::scope("job-{$id}")->forget(); // drop the map once you are done
Enter fullscreen mode Exit fullscreen mode
資料庫保險庫是唯一需要資料表的部分。請先發佈其設定檔與 migration:
php artisan vendor:publish --tag=laranon-config
php artisan vendor:publish --tag=laranon-migrations
Enter fullscreen mode Exit fullscreen mode
接著在 config/laranon.php 中將 scope vault 指向 database,使其能在工作邊界間存續;cache 適合短時間工作,而 array 則只維持一個請求。使用 forget() 可將可逆的假名化轉為真正的匿名化:一旦對應表消失,token 就再也無法還原。
整合到聊天迴圈
三個 hook 就是整個模式。我在法律 AI 助理中實作了它,但它並非特定應用程式專屬。
$anon = Anonymizer::create();
// 1. Anonymize the prompt before it leaves. Cover the message content AND the
// arguments of any tool calls in the history, or PII leaks back on replay.
$payload = $anon->anonymize($messages, ['content', 'tool_calls.*.function.arguments']);
$response = $client->chat($payload);
// 2. The model asked to call tools. Restore the arguments so the tools query
// your database with the REAL values. The model only ever saw tokens.
// tool_calls is a list, so the keyed path applies to each call, same as hook 1.
$toolCalls = $anon->restore($response->toolCalls, 'function.arguments');
$result = runTools($toolCalls);
// 3. Restore the model's answer before you show it or store it.
$reply = $anon->restore($response->content);
Enter fullscreen mode Exit fullscreen mode
Hook 2 是讓它真正發揮作用的關鍵。模型對 «AP_1» 進行推理,但當它決定查詢「客戶的未結案件」時,它會交給你 «AP_1»,你將其還原為真實姓氏,再將查詢送進資料庫。模型永遠看不到真實數值;資料庫也永遠不會看到 token。
值得一提的語言細節是 except('person'):
$anon = app('laranon')->except('person')->newSession();
Enter fullscreen mode Exit fullscreen mode
這會對姓氏、DNI、IBAN、電話與電子郵件進行 token 化,但保留名字的明文。在西班牙語中,名字帶有文法性別,若將其 token 化,模型可能會猜錯性別一致(例如「estimad@ «PER_1»」)。保留「María」並隱藏「López García」,就能讓模型寫出自然的西班牙語,同時保護可識別的部分。
策略及其他
上述 token 策略是用於 LLM 往返的可逆方案。此外還有兩種策略:
Laranon::strategy('faker')->anonymize($text); // valid surrogates, same format, reversible
Laranon::strategy('redact')->anonymize($text); // [DNI], one-way, nothing vaulted
Enter fullscreen mode Exit fullscreen mode
faker 會將真實的 DNI 替換為有效的假 DNI,並將姓名替換為看似合理的姓名,這在產生需要自然閱讀的文件時很有用。redact 是單向版本,適用於日誌或任何對外傳輸的內容。順帶一提,Laranon 也能插入你的日誌堆疊,清理每一行日誌,以及 HTTP 用戶端,對外送請求主體進行單向清理:
Http::scrubPii()->post($url, $payload);
Enter fullscreen mode Exit fullscreen mode
此外還有 laranon:scan 指令,可在正式環境使用前,先稽核語料庫會偵測到哪些內容。
總結
送出時匿名化、回傳時還原,透過檢查碼排除假陽性,對應表永遠不會離開你的伺服器。讓我選擇它而非手動 regex 層的最大原因是那些不起眼的部分:還原精確、同一個值永遠對應同一個 token,而 session 與 scope 模型可直接套用在請求或佇列工作中。只要花一個下午,就能把它整合到現有的聊天迴圈,不需要整個 sprint。
📌 你可以透過 Inis 法律聊天機器人看到 Laranon 的實際運作。
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.