你以为在用 ChatGPT 提效,其实在让真名和客户「裸奔」
起因
我经常想让 ChatGPT / Claude 这类云端强模型帮我分析一些私密材料——会议笔记、合同、聊天记录。它们确实比我本地能跑的模型聪明得多。
但每次复制粘贴的时候都心里发虚:里面的真实姓名、公司、电话、邮箱、项目代号,全都原封不动地交出去了。云端模型是好用,可“这是谁”这件事,真没必要让它知道。
于是我把“出云前的身份脱敏”单独做成了一个开源工具 vault-engine。这篇讲讲它的设计——以及一个我觉得最关键、但很多人会忽略的决定。
项目地址:https://github.com/fishonbike/vault-engine
一个看似可行、其实危险的做法
最直觉的做法是:把文本丢给本地 Ollama,提示词写「把所有人名、机构、PII 替换成代号」,拿回改写后的文本。
别这么干。 让模型改写你的文本,等于让它重新生成整段内容,后果是:
- 可能漏掉几个、可能改了数字、可能幻觉出原文没有的东西;
- 非确定性——同一段文本,每次跑结果都不一样;
- 最致命:你拿不到映射,无法把代号可靠地还原回真名。
对一个隐私工具,这几条都是硬伤。
vault-engine 的思路:检测 / 替换分离
核心就一句话:让模型只输出“要替换哪些片段”的清单,真正的替换交给代码确定性完成。
原文 ──▶ ① 正则检测(PII)
② 本地 LLM 检测(人名/机构/地点/代号/准标识符)
│ 只返回 span 列表,不改写正文
▼
③ 代码确定性替换 + 一致化代号(同名同号)
▼
脱敏文本 ──▶(你手动喂云端)
反向映射 ──▶ 只存本地,永不上传
│
云端用代号回 ──▶ ④ 本地 rehydrate 还原真身
这样换来三个裸提示词给不了的性质:
- 原文逐字保留——只有被识别的身份被替换,其余一个字都不动,零幻觉。
- 可逆——代号 ↔ 真名的映射在本地,云端用代号回复后能一键还原。
- 可审计 + 一致——同一个“张三”全篇都是同一个
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 代码块、复用已存在的占位符。 - 可替换后端:默认
ollama(qwen3.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") # 映射表,留本地
更多推荐


所有评论(0)