#0126 你的 AI Agent 正在把你的身份证号发给 OpenAI
type
Post
status
Published
date
Feb 20, 2026
slug
30da745569bb814eb624ded1baeb952a
summary
你有没有想过一个问题:你跟 AI 聊天时说的每一句话,都去了哪? 答案很简单——云端 API。不管你用的是 ChatGPT、Claude 还是自建的 Agent 系统,只要模型不是本地跑的,你的对话内...
tags
行情
新闻
工具
category
投资
icon
password
你的 AI Agent 正在把你的身份证号发给 OpenAI
你有没有想过一个问题:你跟 AI 聊天时说的每一句话,都去了哪?
答案很简单——云端 API。不管你用的是 ChatGPT、Claude 还是自建的 Agent 系统,只要模型不是本地跑的,你的对话内容就在互联网上裸奔。
这本身不是新闻。但当你开始把 AI Agent 接入真实生活——帮你处理邮件、管理日程、分析文档——问题就变了。你不再是在跟 AI 聊"帮我写首诗",而是在说"帮我查一下 138xxxx1234 这个号码的快递"、"把这份合同里张三的身份证号提取出来"。
手机号、邮箱、身份证、银行卡号……这些东西混在你的对话里,安安静静地坐上了发往 API provider 的 HTTPS 请求。
我搭了一个 AI 自动化系统,跑了大半年。某天心血来潮扫了一遍历史 session 日志,结果吓了一跳——1500 多个 session、上万条消息里,到处散落着真实的个人信息。
所以我给这个系统加了一道 PII 过滤网关。今天聊聊怎么做的,踩了哪些坑。
先搞清楚:你到底在保护什么
PII,Personal Identifiable Information,个人身份信息。在中国语境下,核心就这几类:
- 手机号:11 位,1 开头,第二位 3-9
- 身份证号:18 位,最后一位可能是 X
- 银行卡号:16-19 位数字,有 Luhn 校验
- 邮箱地址:不用解释
- IP 地址:内网外网都算
- API Key / Token:各种
sk-、ghp_、AKIA开头的密钥
- SSH 私钥:
-----BEGIN开头那一坨
7 类规则,覆盖了 AI Agent 系统里最常见的泄露场景。
你可能会问:为什么不用现成的方案?比如微软的 Presidio,或者 Google 的 DLP API?
三个字:不需要。
Presidio 是 ML-based 的 NER 方案,跑起来要装一堆依赖,模型加载要时间,还得联网下载模型文件。对于一个本地跑的 Agent 系统来说,这太重了。而且 NER 模型擅长的是从自然语言里识别"这是人名""这是地址"——但手机号、身份证这些东西,格式是固定的,正则表达式比 NER 精准十倍。
所以我选了最老土的方案:纯正则 + 规则引擎。零依赖,可解释,本地运行,够用。
写正则谁都会,难的是不误报
规则引擎写起来不难。手机号
/1[3-9]\d{9}/,邮箱一个标准正则,身份证 18 位数字加结构校验——半天就能写完。然后你跑一遍测试,发现命中了 1228 条"PII"。
你兴冲冲打开一看——80% 是误报。
误报调优才是这个项目的核心战役。 从 1228 降到 58,95% 的降幅,花的时间比写规则多了十倍。
让我细数这些坑:
坑 1:Discord Snowflake ID 被当成身份证/银行卡
Discord 的消息 ID、用户 ID、频道 ID 都是 Snowflake 格式——17 到 20 位的纯数字。
1468256454954975286这玩意儿 18 位,正好撞上身份证的长度。某些 Snowflake ID 甚至能通过 Luhn 校验,被当成银行卡号。
Agent 系统天天跟 Discord 打交道,日志里全是这种 ID。不处理的话,每条消息都会被标红。
解法: 加白名单机制。已知的系统 ID(频道、用户、服务器)直接跳过。同时对身份证做更严格的结构验证——不是随便 18 位数字就是身份证。
坑 2:UUID 片段变成手机号
这是最骚的一个。
UUID 长这样:
550e8400-e29b-41d4-a716-446655440000但在某些日志格式里,UUID 会被截断或者拼接到其他字符串里,比如:
18160019229f-4b7a-...正则引擎看到
18160019229——11 位,1 开头,第二位是 8——恭喜,完美匹配手机号模式。解法: 加 hex 边界检测。如果匹配到的"手机号"前后紧跟着十六进制字符(a-f),那大概率是 UUID 的一部分,跳过。
这个规则一加,误报直接砍掉一大片。
坑 3:Unix 时间戳伪装成手机号
1708416000 — 这是 2024-02-20 的 Unix 时间戳。10 位数字,1 开头,第二位是 7……嗯,不太像手机号。但换个时间:
1389168000 — 2014-01-08。13 开头,11 位?不对,这才 10 位。但如果日志里时间戳后面跟了个别的数字呢?
13891680001——完美的手机号。解法: 对手机号匹配结果做上下文检查。如果周围是明显的时间戳语境(比如
timestamp 字段里),降低置信度。坑 4:JSON 浮点数尾巴匹配银行卡
JSON 日志里经常有这种东西:
"amount"那个
6222021234567890——16 位,622 开头——银行卡号的经典格式。解法: 检测数字前后是否有小数点。有小数点的跳过,那是浮点数不是卡号。
坑 5:URL slug 匹配 API Key
很多 API Key 的特征是"一长串字母数字混合"。但 URL 路径里也经常有这种东西:
/api/v1/sessions/a]b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6正则看到一长串随机字符,开心地标记为"疑似 API Key"。
解法: 对 API Key 检测加前缀要求。只有以
sk-、ghp_、AKIA、xox 等已知前缀开头的才标记。通用的长字符串不管——宁可漏过,不能误报。坑 6:身份证需要真正的结构验证
随便 18 位数字不是身份证。中国身份证有严格的结构:
- 前 6 位是区域码,范围 11(北京)到 82(澳门),不是随便什么数字都行
- 中间 8 位是出生日期,必须是合法的 YYYYMMDD(不能是 20251340)
- 最后一位是校验码,有标准算法
加了这些验证后,大量 Snowflake ID 和随机数字串被正确排除。
静态规则的天花板
正则规则能搞定格式化的 PII——手机号、身份证、银行卡,这些有固定模式。
但有些东西没有固定格式:
- 人名("张三"是不是 PII?"张三丰"呢?)
- 地址("朝阳区望京 SOHO T1 1234 室")
- 公司名(出现在合同语境里就是 PII)
这些靠正则搞不定。但也不是完全没办法。
我做了一个自动学习机制:
第一步:扫描。 用正则引擎扫全量历史 session。
第二步:过滤。 去掉误报——测试号码(
13800138000)、示例邮箱(example@test.com)、系统邮箱(noreply@xxx.com)、本地域名(localhost、*.local)。这些不是泄露,是噪音。第三步:写入保护名单。 确认是真实 PII 的,自动写入一个实体文件。格式大概是这样的:
实体文件支持分类——person、email、phone、address、company、account——每类有不同的严重级别和处理策略。
第四步:下次生效。 保护名单里的实体,在后续的消息流中会被自动拦截和脱敏,发给 API 之前替换成占位符。
这就形成了一个闭环:扫描 → 发现 → 保护 → 拦截。
误学习防护
自动学习最怕什么?学错了。
如果把
13800138000(运营商测试号)写进保护名单,后续所有包含这个号码的消息都会被误拦截。如果把 admin@example.com 当成真实邮箱保护起来,那所有文档示例都会被打马赛克。所以我加了一层过滤:
- 测试号码黑名单:
13800138000、18888888888这类一看就是假的
- 示例邮箱黑名单: 匹配
example、test、demo、sample等关键词的域名
- 系统邮箱排除:
noreply、no-reply、donotreply开头的
- 本地域名排除:
localhost、127.0.0.1、{{ITALIC、}}.internal
- 短 IP 排除: 一些常见的内网网关地址(
192.168.1.1、10.0.0.1)不算泄露
这些规则确保自动学习不会把噪音当信号。
首次审计:1500 个 Session 的真相
系统跑起来后,我做了一次全量审计。
扫描范围:1500+ 个历史 session,10000+ 条消息。
结果?
去掉误报后,确实发现了一些真实的 PII 泄露。具体数字就不说了,但足以说明问题——如果你的 Agent 系统接入了真实生活,PII 泄露几乎是必然的。
最常见的泄露场景:
- 用户主动提供: "帮我查一下这个手机号"——你说的每个字都会发给 API
- 文档处理: Agent 读取包含个人信息的文件,内容进入 context window
- 系统日志: 错误日志里带了用户信息,被 Agent 读取后发送
- 工具调用结果: Agent 调用外部 API,返回结果里包含个人信息
每一个场景都很自然,不经意间就发生了。
架构设计:网关还是插件?
PII 过滤可以放在两个位置:
方案 A:API 网关层。 在 Agent 和 API provider 之间加一个代理,所有请求经过时扫描和脱敏。
- 优点:一劳永逸,所有 Agent 自动覆盖
- 缺点:增加延迟,需要解析请求体
方案 B:Agent 插件层。 在每个 Agent 发送消息前做检查。
- 优点:更灵活,可以根据 Agent 类型定制策略
- 缺点:每个 Agent 都要集成,容易遗漏
我选了方案 B 的变体——审计 + 拦截分离。
审计是离线的,批量扫描历史 session,不影响实时性能。发现的 PII 写入保护名单。
拦截是实时的,基于保护名单做字符串匹配,非常快。
这个设计的好处是:正则引擎可以很复杂(反正是离线跑的,不怕慢),而实时拦截只需要做简单的字符串查找(保护名单里的已知实体),延迟极低。
为什么不用 ML?一个务实的选择
每次聊到 PII 检测,总有人问:为什么不用 NER 模型?Presidio 不香吗?
我的回答:
正则够用的场景,不要上 ML。
手机号、身份证、银行卡、邮箱——这些东西的格式是确定性的。正则表达式可以做到 100% 的召回率(只要格式对就一定能匹配),误报率通过规则调优可以压到很低。ML 模型在这些场景下不会比正则更好,反而引入了不确定性。
ML 真正有优势的场景是非结构化 PII——人名、地址、公司名。这些没有固定格式,正则搞不定。
但这里有个 80/20 原则:结构化 PII(手机号、身份证等)占泄露的绝大多数。 先用正则把这 80% 搞定,剩下的 20% 以后再说。
而且正则方案有几个 ML 方案比不了的优势:
- 零依赖: 不需要安装任何额外的库,不需要下载模型文件
- 可解释: 每条匹配都能告诉你为什么命中——是手机号格式、还是身份证校验码、还是 Luhn 算法。ML 模型只能告诉你"置信度 0.87"
- 本地运行: 不需要联网,不需要 GPU,一个 Node.js 进程就能跑
- 确定性: 同样的输入永远得到同样的输出,不会因为模型版本升级而行为变化
对于一个个人 Agent 系统来说,这些特性比"能识别人名"重要得多。
实操建议:如果你也想加一道 PII 网关
几条经验,可能有用:
💡 先审计,再拦截。 不要上来就做实时拦截。先扫一遍历史数据,搞清楚你的系统里到底有哪些 PII、从哪里来、有多少。没有这个基线,你不知道自己在保护什么。
💡 误报率比漏报率重要。 漏掉一个手机号,风险有限。但如果误报太多,你会关掉这个功能——然后所有 PII 都没人管了。宁可松一点,不要紧到没法用。
💡 分级处理。 不是所有 PII 都一样敏感。身份证号 > 银行卡号 > 手机号 > 邮箱 > IP 地址。高敏感的严格拦截,低敏感的记录告警就行。
💡 测试数据是大坑。 你的系统里一定有测试号码、示例邮箱、mock 数据。这些不是泄露,但会严重干扰检测结果。提前建好排除名单。
💡 上下文很关键。 同一个 11 位数字,在"给 138xxxx1234 发短信"里是手机号,在
"timestamp" 里是时间戳。纯正则做不好上下文分析,但简单的启发式规则(检查周围的 key 名称)就能过滤掉大部分误报。未来:正则的尽头是 NER
正则方案不是终点。
当你想识别"张三是我的同事,他住在望京"这种非结构化 PII 时,正则就无能为力了。这时候需要 NER(Named Entity Recognition)模型。
但不一定要用云端的。现在有不少轻量级的本地 NER 方案:
- GLiNER: 零样本 NER,支持自定义实体类型,模型不大
- spaCy + 中文模型: 成熟方案,但中文 NER 效果一般
- 小型 BERT 微调: 针对 PII 场景微调一个小模型,效果和成本的平衡点
演进路径大概是这样:
阶段 1(现在): 正则引擎覆盖结构化 PII,自动学习补充保护名单
阶段 2: 加入本地 NER 模型,处理人名、地址等非结构化 PII
阶段 3: NER 模型持续学习,根据审计反馈自动优化
但说实话,阶段 1 已经能解决 90% 的问题了。剩下的 10%,等真正需要的时候再做也不迟。
最后一个问题
你现在用的 AI 工具——ChatGPT、Claude、各种 Agent 框架——有没有想过,你跟它们说过的话里,有多少个人信息?
这些信息现在躺在某个云端服务器的日志里。也许它们承诺不会用于训练,也许它们有完善的数据保护政策。但承诺和现实之间,永远隔着一道你看不见的墙。
如果你自己搭了 Agent 系统,PII 过滤不是可选项——是必选项。
不难做,正则就够。难的是把误报调到能用的水平。
但这个投入是值得的。毕竟,你的身份证号只有一个。
Loading...