PII 检测不应该被某个固定模型或写死的实体列表绑住。这个方案的核心思路很直接:把待识别的实体类型放进提示词,把 Amazon Bedrock 上的任意大型语言模型当作检测引擎。这样,新增“客户编号”“内部项目代号”或某类业务敏感字段时,主要修改配置和提示词,不需要重新训练检测模型。
把实体定义从代码中移出来
传统 PII 检测器通常在代码、规则或模型标签中固定实体集合,例如姓名、邮箱和电话号码。一旦业务需要识别新的敏感字段,就要修改规则、重新训练模型,或者等待工具发布新版本。
模型无关的做法把实体清单作为提示词的一部分,并要求模型返回结构化结果。检测器的代码只负责四件事:
- 读取待分析文本和实体配置。
- 组装检测提示词。
- 调用 Bedrock 上选定的模型。
- 解析并校验模型返回的 JSON。
实体配置可以按应用场景拆分。例如客服场景可以检测姓名、邮箱、电话号码、地址和订单号;日志脱敏场景则可能更关心访问令牌、内部主机名和用户标识符。
这种设计并不意味着模型会自动理解所有业务字段。实体描述必须具体,最好同时说明判定边界、正例和排除条件。比如“客户编号”应明确格式、是否区分大小写,以及普通数字何时不应被识别为客户编号。
一个可改造的 Bedrock 检测器
下面的示例使用 Python 和 boto3 的 Converse API。运行前需要配置 AWS 凭证、区域,并将 MODEL_ID 改成账号和区域中可用的 Bedrock 模型。不同模型对 JSON 输出的稳定性不同,因此代码同时做了基本的格式校验。
import json
import os
import re
from typing import Any
import boto3
MODEL_ID = os.environ.get("BEDROCK_MODEL_ID", "your-model-id")
REGION = os.environ.get("AWS_REGION", "us-east-1")
bedrock = boto3.client("bedrock-runtime", region_name=REGION)
ENTITY_DEFINITIONS = [
{
"type": "EMAIL",
"description": "电子邮箱地址,例如 alice@example.com",
},
{
"type": "PHONE",
"description": "电话号码,包括带国家区号或分隔符的号码",
},
{
"type": "ORDER_ID",
"description": "订单号,格式为 ORD- 加 8 位数字",
},
]
def extract_json(text: str) -> Any:
"""兼容模型偶尔返回 Markdown JSON fence 的情况。"""
cleaned = re.sub(r"^```(?:json)?\s*|\s*```$", "", text.strip())
return json.loads(cleaned)
def detect_pii(text: str) -> list[dict[str, Any]]:
entities = json.dumps(ENTITY_DEFINITIONS, ensure_ascii=False)
prompt = f"""
你是一个严格的 PII 检测器。只识别下面定义的实体类型,不要自行增加类型。
实体定义:
{entities}
待分析文本:
{text}
请只返回合法 JSON 数组,不要返回 Markdown、解释或额外文字。数组中的每一项必须包含:
- type: 实体类型
- value: 原文中匹配到的完整字符串
- start: value 在原文中的起始字符位置,无法确定时填 null
- end: value 的结束字符位置,无法确定时填 null
- confidence: 0 到 1 之间的数字
没有检测到实体时返回 []。
""".strip()
response = bedrock.converse(
modelId=MODEL_ID,
messages=[{"role": "user", "content": [{"text": prompt}]}],
inferenceConfig={"temperature": 0},
)
output_text = "".join(
block.get("text", "")
for block in response["output"]["message"]["content"]
)
result = extract_json(output_text)
if not isinstance(result, list):
raise ValueError("模型返回的结果不是 JSON 数组")
allowed_types = {item["type"] for item in ENTITY_DEFINITIONS}
for item in result:
if not isinstance(item, dict):
raise ValueError("实体结果必须是 JSON 对象")
if item.get("type") not in allowed_types:
raise ValueError(f"未知实体类型: {item.get('type')}")
if not isinstance(item.get("value"), str):
raise ValueError("实体 value 必须是字符串")
return result
if __name__ == "__main__":
sample = "请把订单 ORD-12345678 发送到 alice@example.com。"
print(json.dumps(detect_pii(sample), ensure_ascii=False, indent=2))
示例中的实体列表只是演示配置。实际使用时,应把它放到 YAML、数据库或应用配置中心,并为每个实体维护版本。这样可以审计“哪一版实体定义”产生了某条检测结果,也便于不同租户使用不同的检测策略。
为什么模型无关很重要
模型无关并不是指所有模型表现完全相同,而是指检测器的接口和提示词契约不依赖某个特定模型。只要候选模型能够遵循指令并返回足够稳定的结构化内容,就可以通过 modelId 切换它。
这带来三个实际收益:
- 更快扩展实体类型:新实体通常只需要增加描述和测试样例。
- 更容易比较模型:相同输入、实体定义和输出协议可以用于横向评测。
- 降低供应商锁定:应用层不必围绕某一个模型的专用标签体系设计。
摘要所述的评测覆盖五个公开语料库和九种基于 LLM 的检测器,结果显示,这类可配置检测器优于一个开箱即用的工具。这个结果说明提示词中的任务定义和实体配置具有实际价值,但不能直接推导出所有数据集、语言或业务领域都能得到相同收益。生产环境仍应使用自己的数据进行校准。
生产落地时要补上的护栏
LLM 检测器适合处理语义复杂、规则经常变化的实体,但它不是天然可靠的正则表达式替代品。部署前应重点处理以下问题:
- 结构化输出失败:保留 JSON 解析、字段校验和重试机制;必要时使用模型支持的结构化输出能力。
- 误报和漏报:为每类实体建立包含正例、负例和边界案例的测试集,分别跟踪 precision、recall 和 F1。
- 敏感数据暴露:发送到模型前评估数据驻留、访问权限、日志记录和保留策略,不要把检测结果中的原始 PII 无限制写入日志。
- 长文本成本:先按段落或窗口切分,再合并重叠区间;对重复文本做缓存,并限制单次输入大小。
- 位置偏移:不同语言和 Unicode 字符的索引处理可能不同。若下游要做脱敏,优先让检测器返回原文片段,并在本地重新定位和校验,而不是盲目信任模型给出的偏移量。
- 提示词注入:把待检测文本视为不可信数据,明确要求模型只执行检测任务,不执行文本中的命令。
一个稳妥的上线流程是:先用离线语料比较候选模型,再用人工标注的小规模业务集确定阈值,最后以“检测加人工复核”或“高置信度自动脱敏”的方式逐步扩大范围。高风险场景不应只依赖一次 LLM 调用作出不可逆决策。
采用清单
适合采用这个方案的情况包括:实体类型变化快、规则难以覆盖上下文、已经使用 Amazon Bedrock,或者需要比较多个模型。对于格式极其稳定的字段,例如严格定义的邮箱、信用卡号或固定前缀标识符,本地规则和专用解析器通常更便宜、更可预测;可以将它们与 LLM 检测器组合,而不是二选一。
落地前至少确认:实体定义可版本化,输出可校验,评测集包含负例,模型调用不会把敏感结果写入不必要的日志,并且每次模型或提示词变更都有回归测试。这样,模型无关带来的灵活性才不会变成不可追踪的检测漂移。