Prompt 敏感信息脱敏:在发送给 LLM 前自动识别并替换 PII 和密钥
Prompt 敏感信息脱敏在发送给 LLM 前自动识别并替换 PII 和密钥一、深度引言与场景痛点大家好我是赵咕咕。几个月前我们做了一次日志审计发现一件让我后背发凉的事有用户在我们的 RAG 系统中粘贴了一段代码里面包含了一个硬编码的 AWS Access Key。那段代码被当作检索上下文的一部分原封不动地拼进了 Prompt然后发给了 GPT-4。更可怕的是我们根本不知道这件事——直到日志审计才发现。而 OpenAI 的 API 日志里这个密钥已经被保存了。这不是某个马虎的开发者的锅。大模型应用的本质就是把用户的任意输入拼成 Prompt 发给第三方 API。在这个过程中用户可能不小心贴进来任何东西手机号、身份证号、API Key、数据库密码、内部文档的敏感段落。你必须假设发给 LLM 的每一段文本都可能包含敏感信息然后在发送之前做脱敏处理。二、底层机制与原理深度剖析2.1 敏感信息的分类在 Prompt 场景下敏感信息可以分为三类个人身份信息PII手机号、身份证号、邮箱、银行卡号、家庭住址。识别难度低有明确的正则模式。凭证类信息API Key、Access Token、数据库密码、SSH 私钥。这些通常有特定的格式前缀如sk-、AKIA、ghp_而且有高熵特征。业务敏感信息公司内部项目代号、未发布的版本号、客户真实姓名。这类信息无法通过规则识别需要靠自定义字典或 NERNamed Entity Recognition模型。2.2 脱敏流水线的架构脱敏流水线采用串行与并行结合的处理方式整体流程从用户输入与检索文档的合并开始经过规则引擎筛选后依次完成检测、替换与还原三个阶段最终将结果返回给用户。整个流水线的核心逻辑是检测用多引擎并行检测文本中的敏感信息。规则引擎负责模式匹配正则高熵检测负责发现看起来不像正常文本的字符串如随机生成的 Token。替换检测到的敏感信息用占位符替换如[PHONE_01]、[APIKEY_01]。保留原始值到映射表中。还原LLM 返回结果后把占位符还原为原始值。这样用户看到的结果跟实际数据一致但 LLM 和 API 日志里只有占位符。关键是第三步还原必须在最终输出给用户之前完成否则用户看到的答案是您的电话是 [PHONE_01]而不是真实的138xxxx1234。2.3 高熵检测的原理有经验的工程师可能会绕过正则把 API Key 拆成两段、中间加空格、用 Base64 编码后再贴进来。正则匹配不到。这时候需要用信息熵来检测。正常英文文本的香农熵通常在 3.5-4.5 之间而随机生成的 Token如sk-proj-abc123def456...的熵通常超过 4.8。算法很简单对每个连续的非空白字符串计算字符频率分布的熵值。超过阈值的标记为疑似凭证。三、生产级代码实现import asyncio import re import math import logging from collections import Counter from dataclasses import dataclass, field from typing import Any logger logging.getLogger(__name__) dataclass class SensitiveMatch: 检测到的敏感信息。 original: str # 原始值 placeholder: str # 占位符 type: str # phone / email / apikey / pii / high_entropy start: int # 在原文本中的起始位置 end: int # 在原文本中的结束位置 class PromptSanitizer: Prompt 敏感信息脱敏器。 # ---- PII 正则模式 ---- _PII_PATTERNS: list[tuple[str, str]] [ (phone, r\b1[3-9]\d{9}\b), # 中国手机号 (idcard, r\b[1-9]\d{5}(19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]\b), # 身份证 (email, r\b[A-Za-z0-9._%-][A-Za-z0-9.-]\.[A-Za-z]{2,}\b), # 邮箱 (bankcard, r\b\d{16,19}\b), # 银行卡号 (ip_addr, r\b(?:[0-9]{1,3}\.){3}[0-9]{1,3}\b), # IP 地址 ] # ---- 凭证前缀模式 ---- _CREDENTIAL_PREFIXES: list[tuple[str, str]] [ (openai_key, rsk-[A-Za-z0-9]{20,}), # OpenAI (aws_key, rAKIA[A-Z0-9]{16}), # AWS Access Key (github_token, rgh[ps]_[A-Za-z0-9]{36,}), # GitHub Token (jwt, reyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}), # JWT (private_key, r-----BEGIN [A-Z ] PRIVATE KEY-----), # SSH/TLS 私钥 (connection_string, r(?:mongodb|mysql|postgresql|redis)://[^\s]), ] def __init__( self, entropy_threshold: float 4.5, custom_patterns: list[tuple[str, str]] | None None, custom_keywords: set[str] | None None, ): self._entropy_threshold entropy_threshold self._custom_keywords custom_keywords or set() # 编译所有正则 self._compiled [] for name, pattern in self._PII_PATTERNS self._CREDENTIAL_PREFIXES: try: self._compiled.append((name, re.compile(pattern, re.IGNORECASE))) except re.error as e: logger.error(正则编译失败 (%s): %s, name, e) if custom_patterns: for name, pattern in custom_patterns: try: self._compiled.append( (fcustom_{name}, re.compile(pattern, re.IGNORECASE)) ) except re.error as e: logger.error(自定义正则编译失败 (%s): %s, name, e) # ─── 公开接口 ─── async def sanitize( self, text: str, timeout: float 5.0 ) - tuple[str, dict[str, str]]: 对文本做脱敏处理。 返回: (脱敏后的文本, 占位符→原始值的映射) try: return await asyncio.wait_for( self._sanitize_impl(text), timeouttimeout ) except asyncio.TimeoutError: logger.error(脱敏处理超时返回原文未脱敏) return text, {} async def desanitize( self, text: str, mapping: dict[str, str] ) - str: 将占位符还原为原始值。 result text for placeholder, original in mapping.items(): result result.replace(placeholder, original) return result async def _sanitize_impl( self, text: str ) - tuple[str, dict[str, str]]: matches: list[SensitiveMatch] [] # 1) 正则匹配 for name, pattern in self._compiled: for m in pattern.finditer(text): matches.append(SensitiveMatch( originalm.group(), placeholder, typename, startm.start(), endm.end(), )) # 2) 高熵检测 high_entropy_matches self._detect_high_entropy(text) matches.extend(high_entropy_matches) # 3) 自定义关键词检测 keyword_matches self._detect_keywords(text) matches.extend(keyword_matches) if not matches: return text, {} # 4) 去重叠较长的匹配优先 matches.sort(keylambda m: (m.start, -(m.end - m.start))) deduped: list[SensitiveMatch] [] for m in matches: if not any( e.start m.start and e.end m.end for e in deduped ): deduped.append(m) # 5) 生成占位符并替换 mapping: dict[str, str] {} counts: dict[str, int] {} # 从后往前替换避免位置偏移 result_chars list(text) for m in sorted(deduped, keylambda x: x.start, reverseTrue): type_key m.type.upper() idx counts.get(type_key, 0) 1 counts[type_key] idx placeholder f[{type_key}_{idx:02d}] m.placeholder placeholder mapping[placeholder] m.original result_chars[m.start:m.end] list(placeholder) result .join(result_chars) logger.info( 脱敏完成: 发现 %d 处敏感信息 (%d 种类型), len(deduped), len(counts), ) return result, mapping # ─── 高熵检测 ─── def _detect_high_entropy(self, text: str) - list[SensitiveMatch]: 检测高熵字符串疑似随机生成的凭证。 results: list[SensitiveMatch] [] # 找出所有连续的非空白词元至少 10 个字符 for m in re.finditer(r\S{10,}, text): token m.group() entropy self._shannon_entropy(token) if entropy self._entropy_threshold: # 额外检查不匹配已知的普通词语 if not self._looks_like_normal_text(token): results.append(SensitiveMatch( originaltoken, placeholder, typehigh_entropy, startm.start(), endm.end(), )) return results staticmethod def _shannon_entropy(s: str) - float: 计算字符串的香农熵。 if not s: return 0.0 counter Counter(s) length len(s) return -sum( (count / length) * math.log2(count / length) for count in counter.values() ) staticmethod def _looks_like_normal_text(token: str) - bool: 判断是否像正常文本排除已知的模式。 # 如果包含常见英文单词模式元音辅音交替很可能是正常文本 word_pattern re.compile(r[aeiou][bcdfghjklmnpqrstvwxyz], re.IGNORECASE) vowel_ratio sum(1 for c in token.lower() if c in aeiou) / max(len(token), 1) # 正常英文单词元音比例通常在 0.2-0.5 if 0.25 vowel_ratio 0.55 and word_pattern.search(token): return True # URL 不是高熵凭证 if token.startswith((http://, https://)): return True return False # ─── 关键词检测 ─── def _detect_keywords(self, text: str) - list[SensitiveMatch]: 检测自定义敏感关键词。 if not self._custom_keywords: return [] results: list[SensitiveMatch] [] for kw in self._custom_keywords: start 0 while True: idx text.find(kw, start) if idx -1: break results.append(SensitiveMatch( originalkw, placeholder, typecustom_keyword, startidx, endidx len(kw), )) start idx 1 return results # ─── Agent 集成中间件 ─── class SanitizerMiddleware: 在 Agent 工具调用前插入脱敏处理。 def __init__(self, sanitizer: PromptSanitizer): self._sanitizer sanitizer self._last_mapping: dict[str, str] {} async def wrap_prompt(self, raw_prompt: str) - str: 在构造 Prompt 时调用返回脱敏后的文本。 clean, mapping await self._sanitizer.sanitize(raw_prompt) self._last_mapping mapping return clean async def unwrap_response(self, raw_response: str) - str: 在返回给用户前调用还原占位符。 return await self._sanitizer.desanitize( raw_response, self._last_mapping ) async def main(): sanitizer PromptSanitizer( entropy_threshold4.5, custom_keywords{ProjectPhoenix, 竞品分析_V2}, ) middleware SanitizerMiddleware(sanitizer) # 模拟用户输入 user_input ( 我的手机号是 13812345678请帮我把这个 API Key: sk-proj-abc123def456ghi789jkl 相关的项目部署到 AWS 上。 另外可以参考一下 ProjectPhoenix 的架构。 ) clean await middleware.wrap_prompt(user_input) print(f脱敏后: {clean}) # 模拟 LLM 返回 llm_response ( 已根据您的 [PHONE_01] 设置了通知方式。 使用 [OPENAI_KEY_01] 完成了部署。 参考了 [CUSTOM_KEYWORD_01] 的架构设计。 API Key: [OPENAI_KEY_01] ) final await middleware.unwrap_response(llm_response) print(f还原后: {final}) if __name__ __main__: asyncio.run(main())设计的几个关键考量从后往前替换如果从前往后替换每次替换后字符位置会偏移后续的start/end就对不上了。从后往前替换不需要重新计算位置。去重叠手机号可能同时匹配phone和bankcard如果手机号恰好 16 位较长的匹配优先级更高。高熵检测的假阳性过滤_looks_like_normal_text通过元音比例和辅音模式过滤掉正常的英文文本避免误杀。超时兜底sanitize外层包了asyncio.wait_for。脱敏不是核心业务路径超时后降级为不脱敏直接发送好过阻塞整个请求。中间件模式SanitizerMiddleware封装了脱敏→LLM→还原的完整流程对 Agent 的其他代码透明。四、边界分析与架构权衡4.1 脱敏的过度与不足正则匹配天然偏向宁可误杀不可漏过——银行卡号\b\d{16,19}\b会匹配任何 16-19 位纯数字包括一些正常的订单号或流水号。如果你的业务中订单号是敏感信息那误杀反而是好事。但如果需要保留某些长数字就需要在正则前加一层白名单_WHITELIST_PATTERNS [ r\b(?:ORDER|TXN|REF)\d\b, # 订单号、交易号等非敏感长数字 ]在白名单中的匹配不进行替换。4.2 LLM 对占位符的理解一个容易忽略的问题是LLM 看到[PHONE_01]后可能会猜测这是什么然后在回答中尝试还原。比如它会写您的电话号码 [PHONE_01] 对应的运营商是……——这种情况下LLM 实际上猜对了这个占位符的含义但它的猜测过程本身可能泄露了上下文信息。缓解方案使用更语义中性的占位符如[S_01]、[P_03]而不是[PHONE_01]。4.3 性能与延迟脱敏处理的延迟取决于文本长度和正则复杂度。对于典型 2000 字的 Prompt规则引擎处理时间在 1-5ms。高熵检测需要对每个连续词元做计算在 5000 字文本中约 3-10ms。总体延迟可控制在 15ms 以内。如果文本特别长如检索到 10 万字的上下文建议先做文本截断或只在可能包含敏感信息的段落上运行脱敏如代码块、用户输入部分。4.4 日志与监控脱敏后的文本仍然可能泄露模式信息如用户问了关于 [PHONE_01] 的问题。如果日志也被发送到第三方如日志服务需要考虑日志层面的脱敏。建议在日志输出前再做一层脱敏——但这会引入额外复杂度。务实做法是脱敏后的 Prompt 直接入日志只对高风险的日志接收方做额外处理。五、总结Prompt 脱敏不是可选的nice to have而是一个大模型应用的基础安全设施。你无法控制用户往对话框里贴什么但你可以控制什么内容被发送到第三方 LLM API。实现上建议从三层渐进式构建第一层必须正则匹配 PII 和常见凭证格式覆盖 90% 的场景。第二层推荐高熵检测补充规则引擎的盲区拦截非标准格式的凭证。第三层按需自定义关键词和 NER 模型覆盖业务特定的敏感信息。最关键的一点还原必须在服务端。永远不要在前端做脱敏——你是在防用户把敏感信息发给 LLM而不是帮用户保护他自己的隐私虽然客观上也有这个效果。安全领域有一条铁律不要信任用户的任何输入。在大模型时代这条铁律要加上后半句也不要让 LLM 看到用户的任何裸奔输入。下一篇预告RAG 服务的内存池设计复用 embedding 模型推理结果来减少重复计算。

相关新闻

Ptex核心原理深度解析:为什么它能彻底改变3D纹理工作流

Ptex核心原理深度解析:为什么它能彻底改变3D纹理工作流

Ptex核心原理深度解析:为什么它能彻底改变3D纹理工作流 【免费下载链接】ptex Per-Face Texture Mapping for Production Rendering https://wdas.github.io/ptex 项目地址: https://gitcode.com/gh_mirrors/pt/ptex Ptex作为Per-Face Texture Mapping&#…

2026/10/13 18:54:34 阅读更多 →
Redis 集群数据迁移:不停服的前提下把向量索引迁移到新集群的方案

Redis 集群数据迁移:不停服的前提下把向量索引迁移到新集群的方案

Redis 集群数据迁移:不停服的前提下把向量索引迁移到新集群的方案 一、深度引言与场景痛点 大家好,我是赵咕咕。 有一段时间我对"停服迁移"这件事产生了 PTSD。每次产品经理小心翼翼地问"这个数据迁移要停多久",我就知…

2026/10/12 21:25:44 阅读更多 →
告别复杂配置:OpCore-Simplify让黑苹果EFI创建变得简单快捷

告别复杂配置:OpCore-Simplify让黑苹果EFI创建变得简单快捷

告别复杂配置:OpCore-Simplify让黑苹果EFI创建变得简单快捷 【免费下载链接】OpCore-Simplify A tool designed to simplify the creation of OpenCore EFI 项目地址: https://gitcode.com/GitHub_Trending/op/OpCore-Simplify 还在为黑苹果的复杂配置而头疼…

2026/10/13 19:39:50 阅读更多 →

最新新闻

Nano Banana 2.1降价背后:API账单未减半的三大原因

Nano Banana 2.1降价背后:API账单未减半的三大原因

Nano Banana 2.1降价了,价格直接腰斩。按常理想,原来一个月花100元的API调用费,现在应该50元就够了。可我这几个月盯着账单数据,发现事情远没有这么简单——有些月份总花费只降了10%出头,甚至有一个周五的峰值账单比降…

2026/10/13 19:39:21 阅读更多 →
Git四部曲:add、commit、remote、push的本质与实操

Git四部曲:add、commit、remote、push的本质与实操

1. 项目概述:为什么“四部曲”这个说法在Git实操中如此根深蒂固你刚学Git时,是不是也听过这句话:“上传文件就四步——add、commit、remote、push”?我第一次带某高校开源社团做代码托管训练时,发现92%的新人把这四步当…

2026/10/13 19:39:20 阅读更多 →
数学建模B题穿越沙漠:Matlab动态规划源码拆解与调优

数学建模B题穿越沙漠:Matlab动态规划源码拆解与调优

简介:面向数学建模2020年B题的Matlab源码包,适合正在备战建模竞赛、需要参考完整赛题编程实现的学生和培训团队。资源围绕四关递进式求解展开,重点解决第二关双地图双村庄下的矿山与补给选择、第三四关天气不确定时的路线规划,并对…

2026/10/13 19:39:20 阅读更多 →
dlib 19.20.0 VS2019 预编译包:免编译配置与集成避坑指南

dlib 19.20.0 VS2019 预编译包:免编译配置与集成避坑指南

简介:这是一份针对Windows 64位平台的dlib 19.20.0预编译资源包,采用VS2019(msvc1928)编译的release版本,面向需要在C项目中快速集成dlib的开发者,适用于图像处理、机器学习和计算机视觉等场景。包内共1032…

2026/10/13 19:39:20 阅读更多 →
两堵墙之间:数据不出域与云端协同的六次门禁迁移

两堵墙之间:数据不出域与云端协同的六次门禁迁移

如果你也站在“数据不出域”和“模型效果跟不上”这两堵墙之间犹豫过,那这个标题应该能让你会心一笑。我估计不少同行都有过类似的阶段:本地推理服务器与云端协同,听起来像一句无比正确的废话,真落地起来才知道,单是“…

2026/10/13 19:39:20 阅读更多 →
Cursor额度重置教程:设备指纹清理实现合规续杯

Cursor额度重置教程:设备指纹清理实现合规续杯

简介:本资源是面向软件开发者的 Cursor 编辑器 11 月续杯实战源码包,聚焦解决免费用户模型调用受限、账号续期繁琐等实际痛点,适用于中初级开发者快速提升 AI 编程效率。压缩包共 3 个文件,含 .inscode(Cursor 插件配置…

2026/10/13 19:38:20 阅读更多 →

日新闻

物联网系统集成避坑指南:从选型到验收的关键要点

物联网系统集成避坑指南:从选型到验收的关键要点

做了这么多年物联网系统集成的项目,我最大的感受是,大部分坑不是出在技术前沿,而是出在一些看起来很平常的地方。设备选了便宜的、协议没问清楚、现场没摸透、验收标准写得含糊——这些细枝末节,最后都会变成延期、超支和扯皮的大…

2026/10/13 0:00:57 阅读更多 →
ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

如果你玩ESP32有一阵子了,很难不碰到舵机控制这道坎。做智能小车要转方向,做机械臂要抬关节,做云台要偏航俯仰,背后都是同一个东西在干活:舵机。而我见过太多人卡在“代码能编译、舵机就是不听话”这个阶段&#xff0c…

2026/10/13 0:00:57 阅读更多 →
CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

1. 为什么CAN总线至今仍是工业现场的“硬通货”?——从汽车底盘到智能温室的真实需求你有没有拆过一辆老款燃油车的中控台?或者调试过某高校实验室里那台跑着Modbus协议却频频丢帧的PLC控制柜?又或者,在某个农业物联网项目里&…

2026/10/13 0:00:57 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/13 2:54:56 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/13 16:22:04 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/13 1:58:08 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 21:05:09 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 14:57:39 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 18:11:17 阅读更多 →