个人隐私信息检测、脱敏策略、双重扫描架构与面试高频考点
PII(Personally Identifiable Information)指能够单独或与其他信息结合后识别特定个人身份的数据。常见 PII 包括:身份证号、手机号、银行卡号、邮箱地址、姓名、住址等。
| PII 类型 | 模式特征 | 示例 | 正则表达式 | 说明 |
|---|---|---|---|---|
| 身份证号 | 18 位数字 + 校验位 | 310101199001011234 | \d{17}[\dXx] | 末位支持 X/x |
| 手机号 | 11 位,1 开头 | 13812345678 | 1[3-9]\d{9} | 覆盖所有运营商号段 |
| 银行卡号 | 16-19 位数字 | 6222021234567890123 | [3-6]\d{15,18} | Luhn 校验辅助判定 |
| 邮箱地址 | xxx@xxx.xxx | user@example.com | [\w.+-]+@[\w-]+\.[\w.]+ | 标准 email 格式 |
| 中文姓名 | 2-4 个汉字 | 张三 | 上下文依赖 | 需结合称呼词 / 姓氏词典 |
public class PiiPatterns {
private static final Map<PiiType, Pattern> PATTERNS = Map.of(
PiiType.ID_CARD, Pattern.compile("(?<!\\d)(\\d{17}[\\dXx])(?!\\d)"),
PiiType.PHONE, Pattern.compile("(?<!\\d)(1[3-9]\\d{9})(?!\\d)"),
PiiType.BANK_CARD, Pattern.compile("(?<!\\d)([3-6]\\d{15,18})(?!\\d)"),
PiiType.EMAIL, Pattern.compile("([\\w.+-]+@[\\w-]+\\.[\\w.]+)"),
PiiType.NAME, Pattern.compile("(?<=姓名[::]\\s{0,2})([\\u4e00-\\u9fa5]{2,4})")
);
// 预编译 Pattern 复用,避免重复编译开销
public static Pattern getPattern(PiiType type) {
return PATTERNS.get(type);
}
}
| 策略 | 效果示例 | 适用场景 |
|---|---|---|
| 掩码 MASK | 130****1234 / 3201**********1234 | 保留部分特征,便于用户核对 |
| 替换 REPLACE | [PHONE] / [ID_CARD] | 完全消除,适合发送给 LLM |
| 删除 REMOVE | 直接移除整段文本 | 高敏感场景(如军事、医疗) |
脱敏策略选择决策树:
检测到 PII
│
├── 目标: 发送给 LLM?
│ │
│ ├── YES → 使用 REPLACE 策略
│ │ "[PHONE]" 保留语义位置
│ │ LLM 仍能理解 "请给[PHONE]发消息"
│ │
│ └── NO (返回给用户)
│ │
│ ├── 用户需要核对? → MASK 策略
│ │ "130****1234" 保留前后缀
│ │
│ └── 高敏感场景? → REMOVE 策略
│ 直接移除,不留痕迹
│
策略代码:
┌──────────────────────────────────────────────┐
│ public String sanitize(String text, │
│ PiiType type, MaskStrategy strategy) { │
│ │
│ return switch (strategy) { │
│ case MASK → applyMask(text, type); │
│ case REPLACE → "[" + type.name() + "]"; │
│ case REMOVE → ""; │
│ }; │
│ } │
└──────────────────────────────────────────────┘
PiiDetectionHook 同时注册两个 Hook 类型,在输入和输出两端各设一道扫描关卡:
PII 双重扫描架构:
User Input User
│ ▲
▼ │
┌─────────────────────┐ ┌─────────────────────┐
│ ON_USER_MESSAGE │ │ ON_OUTPUT │
│ (Input Scan) │ │ (Output Scan) │
│ ┌─────────────────┐ │ │ ┌─────────────────┐ │
│ │ PII Scan │ │ │ │ PII Scan │ │
│ │ ├── 身份证检测 │ │ │ │ ├── 身份证检测 │ │
│ │ ├── 手机号检测 │ │ │ │ ├── 手机号检测 │ │
│ │ ├── 银行卡检测 │ │ │ │ ├── 银行卡检测 │ │
│ │ ├── 邮箱检测 │ │ │ │ ├── 邮箱检测 │ │
│ │ └── 姓名检测 │ │ │ │ └── 姓名检测 │ │
│ └────────┬────────┘ │ │ └────────┬────────┘ │
│ ▼ │ │ ▼ │
│ REPLACE 策略 │ │ MASK 策略 │
│ "[PHONE]" │ │ "130****1234" │
└──────────┬──────────┘ └──────────┬──────────┘
▼ │
Clean Input ──────▶ LLM ──────▶ Raw Output ──────────────┘
两道防线的协作关系:
用户输入
│
▼
┌───────────────────────────────┐
│ ① 提示词注入防护 (order=5) │ ← 先检查是否为攻击
│ 检测: "忽略之前的指令..." │
│ 结果: BLOCK / ALLOW │
└──────────────┬────────────────┘
▼ (ALLOW)
┌───────────────────────────────┐
│ ② PII 检测 (order=15) │ ← 再检查是否含隐私信息
│ 检测: 手机号、身份证... │
│ 结果: 脱敏后的消息 │
└──────────────┬────────────────┘
▼
Clean Input → Agent
两者互补而非互斥:
├── 注入防护: 防攻击 (保护系统)
└── PII 检测: 防泄露 (保护用户)
# application.yml — PII 检测与脱敏配置
agent:
security:
pii-detection:
enabled: true
# 扫描方向:INPUT(仅输入)、OUTPUT(仅输出)、BOTH(双向)
scan-direction: BOTH
# 检测规则
rules:
id-card:
enabled: true
pattern: "(?<!\\d)(\\d{17}[\\dXx])(?!\\d)"
mask-strategy: REPLACE # 发送给 LLM 时完全替换
output-strategy: MASK # 返回用户时掩码
phone:
enabled: true
pattern: "(?<!\\d)(1[3-9]\\d{9})(?!\\d)"
mask-strategy: REPLACE
output-strategy: MASK
bank-card:
enabled: true
pattern: "(?<!\\d)([3-6]\\d{15,18})(?!\\d)"
mask-strategy: REPLACE
output-strategy: MASK
luhn-check: true # 启用 Luhn 校验减少误报
email:
enabled: true
pattern: "([\\w.+-]+@[\\w-]+\\.[\\w.]+)"
mask-strategy: REPLACE
output-strategy: MASK
name:
enabled: false # 中文姓名默认关闭(误报率高)
confidence-threshold: 0.9 # 高置信度才脱敏
# 白名单:跳过检测的上下文
exclude-contexts:
- code-block # 代码块中的数字不检测
- example-marker # 示例标记内的内容不检测
# 性能优化
pre-filter:
enabled: true # 启用快速预过滤
# 文本中没有数字和@符号时跳过正则匹配
[PHONE] 保留语义位置,LLM 知道"请给[PHONE]发消息"的意图是发消息。关键信息保留前后缀——130****1234 保留运营商号段,用户可核对。对于需要实际值的场景(如工具调用),系统在执行层还原原始值(脱敏仅作用于 LLM 推理层),确保工具拿到的是真实参数。