AI 数据泄露防护(PII)· 运维手册
适用场景:生产环境 LLM / RAG / Agent 应用处理含个人信息的业务数据,需要在「能用 AI」和「数据不出事」之间划一条可审计的线。
1. PII 泄露的全景:AI 时代多了哪些新口子
传统 Web 应用的数据安全边界很清晰:DB 里存什么、接口吐什么,基本可枚举。但 AI 应用把 PII 的暴露面一下子撑大了:
- 数据不再只躺在库里,而是被切碎、向量化、塞进 Prompt、流进 Agent 的工具调用,散落在向量库、会话缓存、第三方模型 API、日志文件里。
- 模型本身可能「背下来」:微调语料、RAG 文档一旦被喂进去,PII 就进了模型权重或检索索引,删起来比删 DB 记录难得多。
- 输出是新的泄露点:同一个用户 A 问出来的答案,可能被用户 B 看到;模型还可能把检索到的隐私「复述」回给用户。
一句话:PII 的泄露面从「一处」变成了「四层 + 一条尾巴(日志)」。这节先对齐认知,后面逐层拆。
2. 六条 PII 泄露路径(先数清楚有几条)
| 路径 | 具体表现 | 典型后果 |
|---|---|---|
| 1. Prompt 输入 | 用户/业务把含 PII 的原文直接喂给模型 | 数据出域到第三方 API |
| 2. RAG 检索 | 检索回的文档段落含客户隐私 | 隐私跨用户串出 |
| 3. 工具调用 | Agent 调 API 拿回敏感数据 | 未脱敏数据进上下文 |
| 4. 模型输出 | 输出把 PII 回吐/复述 | 不该看的人看到 |
| 5. 日志留痕 | 请求/响应全文写进日志 | PII 长期沉淀、难删 |
| 6. 训练/微调语料 | 微调/评测集混入 PII | 模型「记住」隐私 |
排查泄露时我习惯拿这张表当 checklist,逐条确认有没有防护,而不是凭感觉。
3. 数据分级:先划清「禁止 / 脱敏后可用 / 可直接用」
脱敏之前先分级,不分级就是无差别脱敏,最后要么误伤(把该用的数据也脱了)、要么漏(该禁的字段没拦)。我们团队的三级清单:
禁止输入(P0) → 身份证、银行卡全号、医疗诊断、精确地址
直接进模型 = 事故;能本地处理就不走公有 API
脱敏后可用(P1)→ 姓名、手机号、邮箱、订单号
识别 → 替换占位符 → 送模型 → 按需还原
可直接用(P2) → 公开信息、已聚合统计、脱敏后的 token
正常流转,但日志仍不落明文
分级表要落到字段级并写进接入文档,新业务接入先填这张表,再谈脱敏策略。
4. 输入层:送模型前先脱敏(别用正则黑名单)
最大的误区是「我自己写几条正则匹配手机号/卡号」——线上跑两个月就会漏出一堆变体。现在团队的前置脱敏长这样:
from presidio_analyzer import AnalyzerEngine
from presidio_anonymizer import AnonymizerEngine
analyzer = AnalyzerEngine()
anonymizer = AnonymizerEngine()
def redact_prompt(prompt: str) -> str:
"""送模型前脱敏 PII"""
results = analyzer.analyze(text=prompt, languages=["zh", "en"])
text, ops = anonymizer.anonymize(text=prompt, results=results,
return_span_type=True)
return text # 实体已被替换为占位符,如 <PHONE>、<EMAIL>
要点:
- 用实体识别(Presidio / DeBERTa NER)而不是硬编码规则,覆盖面和召回率都高得多。
- 占位符要保留类型(
<PHONE>而非笼统***),下游才知道怎么还原、怎么审计。 - 脱敏要发生在所有出域点之前:调第三方 API 前、写日志前、存缓存前。
5. 处理层:RAG / 向量库里的 PII 残留
向量库是 PII 最容易「沉淀」的地方,因为它是存了才用、删了才没:
- 入库前先脱敏再向量化:不是存明文再打掩码。明文一进去,检索就可能把它捞出来。
- 定期向量反查扫描:抽样反查 + 实体引擎扫一遍,找出漏网的高敏字段。
- 分级=「禁止输入」的字段直接不进库,源头掐断。
- 保留期:向量库也要设 TTL,业务数据过期的批次要能按批次删,别全库重灌。
我踩过一次:一个向量库存了 8 个月没清,早期批次里混进了带身份证的客服记录,后来做数据盘点才发现。现在所有向量集合都带 batch_id + ttl,到点自动淘汰。
6. 输出层:别让模型把 PII 回吐给不该看的人
输入脱敏了还不够,输出是独立的一条泄露通道:
- 输出侧再跑一遍实体识别:模型可能从检索文档里「复述」出隐私,输出前扫一遍,命中 PII 就替换或拒答。
- 跨用户隔离:会话上下文严格按用户隔离,A 的检索结果不串进 B 的回答。
- 拒答兜底:识别到高频 PII 输出时,不是静默替换,而是走「信息不足 / 无法回答」的安全兜底。
7. 日志与审计:PII 别在日志里躺三个月
日志是「尾巴」,最容易被忽略也最持久:
- 审计日志只记脱敏值/哈希,不记明文:
{"user_hash":"sha256:...","model":"...","tokens":1234,"pii_redacted":3}。 - 确需留痕的敏感字段进加密存储 + 审批访问。
- 日志本身设保留期,到期物理删除(不是软删)。
- 把「日志里 PII 残留率」做成监控指标,一旦 >0 就告警。
8. 技术选型对照
| 场景 | 推荐 | 理由 |
|---|---|---|
| 通用实体脱敏 | Presidio / DeBERTa NER | 覆盖 50+ 实体类型,可私有部署 |
| 数据不出域 | 本地 LLM + 本地脱敏 | 彻底避免 PII 出域 |
| 合规留痕 | 哈希审计 + 加密存储 + TTL | 可回溯、可删除 |
| 高风险决策 | 人工复核通道 | 自动化兜不住的部分留人工 |
参考官方材料:OWASP LLM Top 10(隐私相关项)、Presidio 脱敏库、NeMo Guardrails。
9. 一次真实故障复盘:脱敏漏了一类,日志躺了 3 个月
去年做数据合规审计时撞上一件事,复盘完就固成了流程:
- 我们早期脱敏只覆盖了「姓名 + 手机号」,漏了订单号关联的收货地址。某批次 RAG 文档里混进了带地址的客服记录。
- 更糟的是,旧版日志把请求原文写进文件,这批含 PII 的日志一躺就是 3 个月。
- 处理:紧急下线该向量批次、按
batch_id重灌脱敏版;日志侧全量扫描替换 PII 字段为哈希,剩余加密封存;给脱敏引擎补上「地址」类,并把「脱敏覆盖率」和「日志 PII 残留率」两个指标接进监控。
教训:脱敏覆盖率不是配置完就结束,是持续跑指标的事。漏一类、且日志没设 TTL,等于隐私泄露被放大了三倍。
10. 上线后的巡检与防退化纪律
脱敏策略写完不是终点,没人盯三个月后一定退化。现在每周固定这套动作:
- 周一:拉脱敏引擎的「实体召回率」回归集(我们维护一批 500 条标注样本),任何一类召回掉就当天补规则。
- 每天:核对「日志 PII 残留率」,>0 直接 P0,先断写再查源。
- 每月:向量库按
batch_id抽 3 批做反查实体扫描,确认无漏网高敏字段。 - 每季度:跑一遍完整合规清单(数据分级表 / 保留期 / 访问审批 / 加密存储),配合法务复审一次。
这些动作的价值,是把「PII 泄露没被及时发现」从靠人肉审计,变成靠指标和告警。
11. 常见问题排查
问题 1:脱敏后业务逻辑失效
排查:占位符是否保留了类型(<PHONE> 而非 ***)?还原链路是否覆盖所有下游?只保留原始值给必要方。
问题 2:向量库删不干净
排查:确认是物理删除批次而非仅打掩码;核对 batch_id + TTL 配置;重灌前跑一次反查扫描。
问题 3:模型把检索到的隐私复述出来
排查:输出层是否跑了实体识别?命中 PII 是否走拒答/替换兜底?跨用户会话是否已隔离?