Laranon 库实现了 Laravel 应用中敏感信息的可逆匿名化,解决了向 LLM 发送真实用户数据的安全困境,是构建 AI 应用时的实用防护方案。
Laravel 中的可逆 PII 匿名化解决方案 Laranon
我开发了一个法律科技产品,AI 助手可以回答真实案件相关的问题:客户名字、身份证号、IBAN、电话号码等等。这些对话会被发送到我无法控制的 LLM。原样发送是不可行的。
显而易见的解决方案——用一堆正则表达式把看起来像 ID 的内容抹掉——在两个方向上同时失败了。它既有假阳性(它删除了不是 ID 的内容)又有假阴性(它漏掉格式奇怪的内容),最糟糕的是它是单向的:一旦你抹掉数据,就无法得到连贯的答案,因为模型现在在推理 [REDACTED] 与 [REDACTED] 的对话。
我需要一个东西能够正确检测 PII,把它替换为稳定的占位符让模型能够推理,然后把真实值换回答案中。这就是 Laranon。
它以单个 Composer 包的形式发布:
composer require edulazaro/laranon
这就是它所需要的。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.
这就是整个想法。下面的一切是使其可信的机制,以及将其集成到实际应用中的方式。
有几个设计选择使得它从演示升级为可以信任处理客户数据的东西。
西班牙 DNI 不是"八位数字和一个字母",而是八位数字和正确的 mod-23 控制字母。Laranon 验证这个字母,IBAN 用 mod-97,信用卡用 Luhn 加 IIN,NIE、CIF、NSS、CCC 也是如此。一个 12345678A 如果字母错误就不会被标记,这消除了大多数让简单的脚本工具无法使用的假阳性。
名字按单词而非按人进行标记,从不进行身份猜测。"John Smith" 变成 «PER_1» «AP_1»,名字和姓氏各自获得自己的稳定令牌。稍后出现的单独"John"再次获得 «PER_1»,因为令牌属于这个单词,不属于这个人。"Mr. Baker" 与 "John Baker" 的 «AP_2» 共享。敬语和虚词保持明文("John de la Cruz" 读作 «PER_1» de la «AP_3»)。确切地说,就是一个人类读者能够拥有的信息,不更多。
令牌映射表保证两个不同的值永远不会共享一个占位符。如果他们这样做了,你会合并两个人并搞乱恢复过程。
每个令牌逐字节映射回原始文本。而且它是流安全的:一个令牌即使在两个 SSE 块之间被分割,即使在多字节 « 字符内部,也会被缓冲并正确恢复。
对于聊天轮次,你需要一个在内存中的映射表在整个提示中共享(用户消息、检索到的上下文、工具结果),并且你希望它在请求结束时消亡。这就是一个会话:一个一次性对象,拥有它的映射表,不持久化任何东西,随请求一起消亡。
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.
anonymize() 和 restore() 接受一个字符串、一个列表或进入消息列表的键路径,包括嵌套的点路径和 * 通配符:
$anon->anonymize($messages, 'content');
$anon->anonymize($messages, 'tool_calls.*.function.arguments');
这里的 $messages 是一个普通的 PHP 数组,采用通常的 OpenAI 聊天形式(role、content、tool_calls 等)。Laranon 既不定义也不要求这种形式:就像 Laravel 的 data_get(),它只是通过任何嵌套数组遍历你给它的点路径,并匿名化它到达的字符串。角色、id、工具名称和其他一切都保持不变。
因为你以明文形式保存聊天历史(真实值),你不会持久化任何匿名化的内容。每个轮次只是构建一个全新的会话并从头再次匿名化整个提示。令牌会输出相同的值(它们在读取顺序中是确定性的),所以多轮对话保持连贯,轮次之间没有状态跨越。
会话存在于内存中,随请求消亡,这对于同步聊天轮次来说完全正确。队列任务不同:它稍后运行,在另一个进程中,远在创建会话的请求结束之后。没有内存中的映射表可以共享。
为此,使用由持久保险库支持的范围。映射表使用你的应用密钥进行加密存储,在你选择的密钥下,所以任务可以重新打开它并恢复:
// 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
数据库保险库是唯一需要一个表的部分。发布其配置和迁移一次:
php artisan vendor:publish --tag=laranon-config
php artisan vendor:publish --tag=laranon-migrations
然后在 config/laranon.php 中指向范围保险库到数据库,使其跨越任务边界持久化;缓存对于短期工作很好,数组仅持续一个请求。forget() 是将可逆伪匿名化转变为真实匿名化的开关:一旦映射表消失,令牌就永远无法被转换回来。
这三个钩子就是全部模式。我在法律 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);
钩子 2 是让它发挥作用的那个。模型在 «AP_1» 上推理,但当它决定调用"查找客户的未结案件"时,它会把 «AP_1» 交给你,你把它恢复为真实的姓氏,查询命中数据库。模型永远看不到真实值;数据库永远看不到令牌。
有一个语言细节值得指出,就是 except('person'):
$anon = app('laranon')->except('person')->newSession();
那会对姓氏、DNI、IBAN、电话和电子邮件进行标记,但保留名字为明文。在西班牙语中,名字带有语法性别,所以对其进行标记会使模型错误地猜测协议("estimad@ «PER_1»")。保留"María"同时隐藏"López García"为它提供足够的信息来写自然西班牙语,同时仍然保护识别部分。
上面的令牌策略是用于 LLM 往返的可逆策略。还有两个更多的:
Laranon::strategy('faker')->anonymize($text); // valid surrogates, same format, reversible
Laranon::strategy('redact')->anonymize($text); // [DNI], one-way, nothing vaulted
faker 用有效的虚假 DNI 和合理的名字替换真实的 DNI 和名字,这是你想要在生成必须自然读取的文档时要做的。redact 是日志和任何出站内容的单向版本。说到这一点,Laranon 也可以放入你的日志堆栈以清理每条日志线,并放入 HTTP 客户端用于出站请求体的单向清理:
Http::scrubPii()->post($url, $payload);
还有一个 laranon:scan 命令来审计一个语料库在你在生产中信任它之前会检测什么。
在发出时匿名化,在返回时恢复,校验和保持假阳性之外,令牌映射永不离开你的服务器。与手卷的正则表达式层相比,我更看重的是平凡的部分:恢复是精确的,相同的值总是映射到相同的令牌,会话和范围模型直接降落到请求或队列任务中。将其集成到现有的聊天循环中只需花费一个下午,而不是一次冲刺。
📌 你可以通过 Inis 法律聊天机器人看到 Laranon 的实际应用。
👉 Packagist 上的包。👉 GitHub 上的源代码。
对于进一步的操作,你可能需要考虑屏蔽此人和/或报告滥用。