起因

我经常想让 ChatGPT / Claude 这类云端强模型帮我分析一些私密材料——会议笔记、合同、聊天记录。它们确实比我本地能跑的模型聪明得多。

但每次复制粘贴的时候都心里发虚:里面的真实姓名、公司、电话、邮箱、项目代号,全都原封不动地交出去了。云端模型是好用,可“这是谁”这件事,真没必要让它知道。

于是我把“出云前的身份脱敏”单独做成了一个开源工具 vault-engine。这篇讲讲它的设计——以及一个我觉得最关键、但很多人会忽略的决定。

项目地址:https://github.com/fishonbike/vault-engine

一个看似可行、其实危险的做法

最直觉的做法是:把文本丢给本地 Ollama,提示词写「把所有人名、机构、PII 替换成代号」,拿回改写后的文本。

别这么干。 让模型改写你的文本,等于让它重新生成整段内容,后果是:

  • 可能漏掉几个、可能改了数字、可能幻觉出原文没有的东西;
  • 非确定性——同一段文本,每次跑结果都不一样;
  • 最致命:你拿不到映射,无法把代号可靠地还原回真名

对一个隐私工具,这几条都是硬伤。

vault-engine 的思路:检测 / 替换分离

核心就一句话:让模型只输出“要替换哪些片段”的清单,真正的替换交给代码确定性完成。
原文 ──▶ ① 正则检测(PII)
② 本地 LLM 检测(人名/机构/地点/代号/准标识符)
│ 只返回 span 列表,不改写正文

③ 代码确定性替换 + 一致化代号(同名同号)

脱敏文本 ──▶(你手动喂云端)
反向映射 ──▶ 只存本地,永不上传

云端用代号回 ──▶ ④ 本地 rehydrate 还原真身

这样换来三个裸提示词给不了的性质:

  1. 原文逐字保留——只有被识别的身份被替换,其余一个字都不动,零幻觉。
  2. 可逆——代号 ↔ 真名的映射在本地,云端用代号回复后能一键还原。
  3. 可审计 + 一致——同一个“张三”全篇都是同一个 P-n1
    效果长这样:
    原文(私有)
    林若曦是星澜资本的合伙人,在深圳见了字节跳动的陈大壮,邮箱 lin@xinglan.example

脱敏后(这份才出云)
P-n1 是 ORG_1 的合伙人,在 LOC_1 见了 ORG_2 的 P-n2,邮箱 EMAIL_1

云端看到的是 ORG_1,而不是你的客户名字。

几个值得说的工程决定

  • 模型只当检测器:返回结构化 JSON span,不生成正文(上面已说,这是要害)。
  • 纵深兜底:①确定性正则层(断网也能抓邮箱/手机/证件/卡号 Luhn)②本地 LLM 语义层 ③残留风险复审。模型不可用时降级为正则并以非零退出——绝不静默吐出“没脱干净”的文本。
  • 一致化代号 + 边界处理:最长优先替换、ASCII 词边界(Jack 不误伤 jackpot)、中文直接字面替换、保护 fenced 代码块、复用已存在的占位符。
  • 可替换后端:默认 ollamaqwen3.6:27b),也支持 openai-compat 和纯离线 null,一行配置切换。
  • 纯标准库、零运行时依赖

benchmark:本地 LLM 到底比正则强多少

我拿一个双语合成评测集(15 篇文档、77 个标注实体,仓库里可复现)跑了三方对比:

检测器 人物 机构 地点 项目 联系方式 证件 总召回 过度脱敏
纯正则 0% 0% 0% 0% 69% 33% 13% 0%
Microsoft Presidio (en/zh lg) 78% 59% 80% 33% 38% 0% 61% 4%
本地 qwen3.6:27b 100% 100% 100% 100% 100% 100% 100% 0%

⚠️ 这是个小规模合成集,只用于回归测试和粗略对比,不代表法律意义上的匿名化。LLM 检测是非确定性的。
差距最大的地方是项目代号、@账号、证件号、中文人名/机构——传统正则/NER 基本抓瞎,本地大模型全拿下。代价是速度:Presidio ~6s,本地 LLM ~25s/篇。

用起来

pip install vault-engine
ollama pull qwen3.6:27b
vault-engine scrub notes.txt -o notes.safe.txt
#   → notes.safe.txt        (发给云端的)
#   → notes.safe.txt.map.json(只存本地,别上传)
vault-engine rehydrate reply.json --map notes.safe.txt.map.json -o reply.real.json
最顺手的是剪贴板模式——贴进 ChatGPT 前先洗一遍:

vault-engine clip            # 脱敏当前剪贴板
vault-engine clip --rehydrate  # 把云端回复里的代号还原成真身
库调用:

from vaultengine import deidentify, rehydrate, Config

r = deidentify(open("notes.txt").read(), Config(model="qwen3.6:27b"))
send_to_cloud(r.text)                  # 只有代号出门
restored = rehydrate(get_reply(), r.vault)   # 本地还原
r.vault.save("notes.map.json")         # 映射表,留本地
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐