个人信息防线:Microsoft Presidio

Presidio是一个开源工具集,用于在文本中查找并遮盖或替换个人身份信息(PII)。它独立于LLM“此句子是安全的”判断,通过代码为电子邮件、电话号码、账户标识符等具有特定格式的敏感信息构建一道防线。

如果说前一篇的Nova Lite和NeMo Self-check是分类请求的含义和意图,那么Presidio则是查找数据中的个人信息段落。它们并非竞争产品,而是减少不同类型失败的层次。本次实践中,我们将仅使用合成数据,通过Analyzer定位,Anonymizer进行遮蔽,然后连接到NeMo Retrieval Rail。

虽然名称中仍保留Microsoft,但目前的官方文档指出Presidio正在向社区拥有的项目过渡。新的容器镜像将从Data Privacy Stack组织的GitHub Container Registry发布。本文基于Microsoft发起的Presidio的当前官方文档和分发路径。

包含个人信息的文件片段经过检测和匿名化关卡精炼后,传递给LLM的场景

Presidio的功能与非功能

Presidio的核心分为两个阶段。AnalyzerEngine返回文本中个人信息候选的类型、起始/结束位置和置信度分数,AnonymizerEngine则使用Replace、Redact、Mask、Hash、Encrypt等操作符转换这些位置。

원문
  ↓
Analyzer: 개인정보 후보와 위치 탐지
  ↓
정책: 종류·점수·처리 방식 결정
  ↓
Anonymizer: 치환·삭제·마스킹·암호화
  ↓
정제된 문장

Presidio并非协调整个LLM管道的编排器。它不负责用户认证、租户权限、文档访问控制和工具授权。应用程序或NeMo的Custom Action调用Presidio,最终访问权限必须由可信的服务层强制执行。

— — —
组件 职责 结果
Presidio Analyzer 使用正则表达式、命名实体识别、规则、校验和与上下文检测PII候选 实体类型、位置、分数
Presidio Anonymizer 对检测到的位置应用操作符 去标识化文本
应用程序·NeMo 决定调用时机和失败策略 允许、阻止、限制模式
权限层 验证用户·租户·文档权限 访问允许范围

在LLM安全管道中如何放置

用户输入在发送到LLM之前需要进行检查。RAG(Retrieval-Augmented Generation)是一种检索与问题相关的文档片段并添加到Prompt中的方式,此时应检查检索后返回的原始Chunk,而非嵌入向量。仅检查向量无法确认实际进入模型的字符串。

Browser
  ↓
Application: 인증·Tenant 확정
  ↓
NeMo Input Rail: 입력 Safety 분류
  ↓
권한 있는 Retrieval API: Tenant Filter 검색
  ↓
NeMo Retrieval Rail → Presidio: 원문 Chunk 검사·익명화
  ↓
Prompt 조립 → LLM → Output Rail
  ↓
Application: 최종 권한·감사 기록

Presidio也可以放置在输出端。然而,即使输入和检索文档已经过精炼,模型仍可能从对话上下文或工具结果中重新生成信息,因此敏感服务应在Output Rail中再次检查。在任何环节,是将检测错误更改为ALLOW、阻止,还是转换为受限响应,都必须根据业务影响明确规定。

实践1:用Python创建最小的检测·匿名化

创建Python虚拟环境并安装官方包。官方安装文档中支持的版本和NLP模型要求请在运行时再次确认。

mkdir presidio-privacy-lab
cd presidio-privacy-lab

python3 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
pip install presidio-analyzer presidio-anonymizer
python -m spacy download en_core_web_lg

不要复制实际的客户信息,请使用预留的.test域名和虚拟标识符。

from presidio_analyzer import AnalyzerEngine
from presidio_anonymizer import AnonymizerEngine
from presidio_anonymizer.entities import OperatorConfig

text = "문의 주소는 student@example.test 입니다."

analyzer = AnalyzerEngine()
anonymizer = AnonymizerEngine()

findings = analyzer.analyze(
    text=text,
    entities=["EMAIL_ADDRESS"],
    language="en",
    score_threshold=0.5,
)

result = anonymizer.anonymize(
    text=text,
    analyzer_results=findings,
    operators={
        "EMAIL_ADDRESS": OperatorConfig(
            "replace", {"new_value": "<EMAIL>"}
        )
    },
)

print([(x.entity_type, x.start, x.end, x.score) for x in findings])
print(result.text)

预期结果是句子中包含而不是电子邮件字符串。确切的分数可能因安装的NLP引擎和版本而异,因此不要将特定数字固定为正确答案。

实践2:将组织标识符添加为Custom Recognizer

仅凭默认的Recognizer无法识别组织特有的员工编号、咨询编号或资产编号。例如,如果实践用的员工编号格式是EMP-年份-四位数字,则需要添加Pattern Recognizer。

from presidio_analyzer import AnalyzerEngine, Pattern, PatternRecognizer

employee_pattern = Pattern(
    name="synthetic_employee_id",
    regex=r"bEMP-20d{2}-d{4}b",
    score=0.85,
)

employee_recognizer = PatternRecognizer(
    supported_entity="EMPLOYEE_ID",
    patterns=[employee_pattern],
    context=["사번", "employee", "담당자"],
)

analyzer = AnalyzerEngine()
analyzer.registry.add_recognizer(employee_recognizer)

sample = "담당자 사번은 EMP-2026-0001 입니다."
findings = analyzer.analyze(
    text=sample,
    entities=["EMPLOYEE_ID"],
    language="en",
)

print(findings)

正则表达式擅长捕捉格式,但不能完全理解上下文。过于宽泛的模式可能会将正常字符串误识别为个人信息,而过于狭窄则可能遗漏变体。对于韩语姓名或地址等语言和上下文依赖性强的实体,需要结合支持的NLP模型、Custom Recognizer和评估数据集进行设计。

实践3:按策略分离操作符

并非所有PII都应以相同方式处理。在LLM Prompt中,如果不需要恢复原文,Replace或Redact更为简单;在分析过程中,如果需要保持同一用户的连续性,可以考虑Hash或Pseudonymization。

— — —
目的 操作符候选 注意事项
不向LLM发送原文 replace, redact, mask 确认不删除必要的上下文
保持相同值的连接性 hash 固定Salt在单独的Secret Store中管理
需要批准的恢复 encrypt 不将Key放入Prompt·Log·配置文件
保留部分实体 keep 需要明确的业务依据和访问控制

根据官方文档,最近的Hash Operator默认对每个实体使用随机Salt。如果相同的输入需要相同的哈希值,则必须提供一致的Salt,但如果Salt泄露,再识别的风险会增加。Salt或Encrypt Key应由KMS·Key Vault·Secret Manager等外部秘密管理层处理。

实践4:从NeMo Retrieval Rail调用

以下函数是搜索API完成权限检查并返回原始Chunk后,通过Presidio进行精炼的最小示例。Presidio不直接查询Vector DB或判断租户权限。

from presidio_analyzer import AnalyzerEngine
from presidio_anonymizer import AnonymizerEngine

analyzer = AnalyzerEngine()
anonymizer = AnonymizerEngine()

def sanitize_retrieved_chunks(chunks: list[str]) -> list[str]:
    sanitized = []

    for chunk in chunks:
        findings = analyzer.analyze(
            text=chunk,
            entities=["EMAIL_ADDRESS", "PHONE_NUMBER"],
            language="en",
            score_threshold=0.6,
        )
        clean = anonymizer.anonymize(
            text=chunk,
            analyzer_results=findings,
        )
        sanitized.append(clean.text)

    return sanitized

在NeMo中,可以将此函数注册为Custom Action,并从Retrieval Rail调用。或者,也可以在应用程序拥有的Retrieval API内部执行相同的检查,然后将精炼后的Chunk传递给NeMo。两种方式均可行,选择取决于运营主体、延迟和策略跟踪位置。

重要的顺序是:租户过滤器搜索 → 返回有权限的原文 → PII检查 → Prompt组装。先调用Presidio并不会获得读取错误租户文档的权限。

通过容器分离调用

官方安装文档提供了将Analyzer和Anonymizer分别作为HTTP服务运行的容器镜像。截至2026年9月,新版本在ghcr.io/data-privacy-stack下发布,生产环境中应固定经过验证的Release Tag,而非latest。

podman pull ghcr.io/data-privacy-stack/presidio-analyzer:latest
podman pull ghcr.io/data-privacy-stack/presidio-anonymizer:latest

podman run --rm -p 5002:3000 
  ghcr.io/data-privacy-stack/presidio-analyzer:latest

podman run --rm -p 5001:3000 
  ghcr.io/data-privacy-stack/presidio-anonymizer:latest

在实践中,为了说明结构,我们展示了latest,但对于可复现的环境,应固定Digest或明确的版本。服务之间的原文传输段也应应用TLS、服务认证和网络策略,并且不应将请求体保留在访问日志中。

测试不应只看检测率

Presidio是一个自动判断工具,因此可能同时出现误报(False Positive)和漏报(False Negative)。官方文档也警告说,它不保证一定能找到所有敏感信息,需要根据实际用途进行额外的系统和评估。

— —
测试用例 预期结果
合成电子邮件·电话号码 检测到并替换为指定实体
组织特有虚拟员工编号 通过Custom Recognizer检测
格式相似的正常订单号 不过度检测
韩文·英文·空格变体 确认各语言的目标检测率
Analyzer超时·错误 在敏感路径中采用Fail-closed或限制模式
已遮蔽的字符串 不因重复转换而破坏上下文

测试日志应记录case_id、实体类型、数量、分数区间、策略版本和处理结果,而不是原文。将检测到的字符串本身复制到Log·Trace·Metric Label会使个人信息防线成为新的泄露途径。

常见被忽视的局限性

第一,遮蔽不等于删除。如果原始数据已首先存储在应用程序日志、跟踪、队列或缓存中,即使遮蔽发送给LLM的句子,其他副本也已存在。Presidio应尽可能靠近数据收集点部署。

第二,即使删除单个字段,通过组合上下文仍可能重新识别个人。结合稀有职位、日期、地区和事件描述的再识别风险,并非简单的正则表达式就能解决。

第三,匿名化不能替代授权。首先,应确认用户是否有权读取原始文档,然后只处理最少必要的信息。

第四,性能和策略应同时衡量。每次请求都用多个Recognizer检查长RAG Chunk会增加延迟和CPU使用率。应结合摄取阶段的预处理和检索阶段的再检查,并为策略变更时现有文档重新评估设置路径。

实践完成检查清单

  • 区分了Presidio Analyzer和Anonymizer的职责。
  • 使用了.test地址和虚拟标识符代替实际个人信息。
  • 分别测试了默认Recognizer和组织专用的Custom Recognizer。
  • 按实体分离了Replace·Mask·Hash·Encrypt策略。
  • 在RAG中,检查的是检索后的原始Chunk,而非向量。
  • 在Presidio前端保留了租户过滤器和文档权限检查。
  • 设定了失败策略,以防止错误时静默绕过(Allow)。
  • 未将原文、Salt、Encrypt Key留在Prompt和Telemetry中。
  • 创建了误报(False Positive)和漏报(False Negative)两种测试用例。
  • 将阻止·遮蔽结果保存为下一个Promptfoo回归测试用例。

总结

Presidio将“LLM会自动隐藏个人信息”的期望转化为可执行的检查和转换步骤。Analyzer查找个人信息候选,Anonymizer根据组织策略对这些段落进行去标识化。NeMo或应用程序在输入、搜索和输出边界调用此功能。

然而,Presidio不替代认证·授权、租户隔离和整体编排。最安全的部署是:只搜索有权限的数据,检查原始Chunk,只将精炼后的数据传递给LLM,并在输出端再次确认。下一篇将探讨如何串联多个防线,并在控制平面管理策略、版本和观测。

官方资料

※ 支持的语言、NLP模型、包API和容器注册表可能因版本而异。在应用于生产之前,请根据当前的官方文档和固定的版本,首先在合成数据集中进行验证。


Comments

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注