十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Python实现本地文本清洗器:LLM提示词敏感信息脱敏指南

Python实现本地文本清洗器:LLM提示词敏感信息脱敏指南 在把代码、日志、配置文件粘贴进 LLM 对话框之前大多数开发者都想过同一个问题这段文本里有没有不该发出去的内容API Key、内网 IP、邮箱、绝对路径、连接字符串这些信息只要出现在提示词里就已经离开本机后续无论怎么删除聊天记录都无法撤回。本地文本清洗器local scrubber解决的就是这个环节在文本出网之前用本地程序完成敏感内容识别和替换只把清理后的版本交给 LLM。这篇文章会用 Python 从零实现一个可运行的本地 scrubber覆盖规则设计、代码实现、参数调优、排错路径和生产落地建议。1. 为什么需要在文本离开本机之前做清洗1.1 泄露往往发生在提示词的“夹带内容”里很多人以为只有主动填写密钥才会泄露实际项目中更常见的是夹带泄露。调试 LLM 应用时你会把报错堆栈贴进提示词而堆栈里可能带着环境变量展开后的值处理文档时你会把 Markdown 正文交给模型正文里可能藏着客户邮箱做代码审查时你会把函数片段贴进去函数里正好有一个写死的数据库口令。这些内容不是用户主动输入而是从项目环境、日志文件、文档正文里被一起带出来的。一旦这些内容进入提示词它们就成了请求的一部分。LLM 服务端能否看到取决于服务端对请求内容的处理策略而这不是本地代码能够控制的。最稳妥的做法只有一个让敏感内容在离开本机之前就不存在。1.2 本地 scrubber 和“让 LLM 自己脱敏”是两件事有人会问能不能在提示词里加一句“请忽略其中的密钥不要记录”这个思路有两个问题。第一提示词本身已经携带了密钥模型在推理时已经接触了原文服务端日志、请求链路、模型服务平台的调试工具都可能记录到请求原文。让模型“假装没看见”不代表系统真的没有收到。第二这依赖模型服务端的行为而模型供应商的策略、日志开关、数据保留周期都不在本机控制范围内。本地 scrubber 的思路是完全不同的它不依赖服务端做任何事而是在请求发出前由本地程序完成检测和替换。原始文本只存在于本机内存和缓冲区外发的只有清洗结果。这个边界不能模糊因为一旦原文出现在网络请求里清洗就已经晚了。1.3 先分类再设计你需要清洗哪些数据不同项目要清洗的数据类型差别很大但常见的可以归为几类数据类别典型示例为什么容易漏云厂商密钥sk-开头的 token、AKIA开头的访问密钥没有统一格式只有前缀规律私钥内容-----BEGIN PRIVATE KEY-----整块文本内部是随机字符靠正则块匹配身份联系方式邮箱、手机号、员工编号散落在正文中容易被当成普通文本内网网络信息10.x.x.x、172.16.x.x、192.168.x.x、内部域名开发者对内网地址不敏感本地路径/Users/用户名/...、/home/用户名/...路径本身携带用户名和目录结构连接字符串host... user... password...键值对形式容易被忽略高熵随机 token哈希、会话 ID、OAuth token没有语义只能靠熵判断设计清洗器之前先把自己项目里真实出现过的泄露样本找出来按上表分类再为每一类写规则。没有样本驱动的规则表无论写多少正则都只是看起来安全。2. 识别与替换分离scrubber 的三层设计2.1 第一层语义正则识别“看得见”的敏感格式最容易处理的敏感信息带有明确前缀或结构。OpenAI 类型密钥通常以sk-开头AWS 访问密钥以AKIA开头私钥由固定头尾包裹邮箱符合标准地址结构内网 IP 有固定网段。对这些内容正则是最直接的识别方式。正则层要格外注意两个原则。一是优先使用带语义前缀的规则不要写一个[A-Za-z0-9]{20,}去匹配所有长字符串否则正常英文单词和聊天内容也会被替换。二是要控制边界用\b或前后缀约束匹配范围避免一个邮箱地址中间被截断匹配。2.2 第二层香农熵兜住随机 token密钥、会话 ID、哈希值这类内容没有固定前缀但有一个共同特征字符分布非常均匀也就是信息熵很高。香农熵的计算公式是H(X) -Σ p(x) * log2(p(x))其中p(x)是字符x在 token 中出现的概率。一个完全随机的 Base64 风格 token每个字符的出现概率接近均匀熵值会接近 6而一段普通英文单词字符分布不均匀熵值通常在 3.5 到 4.0 之间。熵检测不是单独使用的而是作为正则层的补充。流程是先跑语义正则对上一条规则的内容整体替换再对剩余文本扫描长度大于阈值的 token计算熵值超过阈值就整体替换。这样既能识别格式明确的密钥也能兜住没有格式规律的随机 token。2.3 第三层allowlist 与自定义规则规则再完善也会误伤。测试邮箱testexample.com、文档里的示例密钥、公司内部公开的内网 IP这些内容可能本来就可以发给 LLM不需要替换。所以 scrubber 必须支持 allowlist也就是允许清单如果被命中的值在清单里就不替换。自定义规则是对正则层和熵层的补充。不同团队的泄露模式完全不同金融团队要处理卡号健康类项目要处理病历号游戏团队要处理玩家 ID。正确做法是配置文件里预置一套公共规则同时开放自定义规则入口让每个团队把自己特有的字段类型加进去。2.4 占位符的设计原则替换不是直接删除。把敏感值删掉会让文本结构断裂LLM 在分析日志时可能因为参数缺失而给出错误结论。更合理的做法是使用统一格式的占位符用REDACTED_类型代替原文。占位符要满足几个条件唯一标识是什么类型被清洗不干扰 Markdown 和代码块结构长度尽量稳定。不要把占位符设计成***因为 LLM 无法区分是密钥还是省略号。也不要让占位符长度差异太大文本里一会儿是 6 个字符的占位符一会儿是 40 个字符的占位符会干扰模型对结构长度的判断。占位符示例原内容占位符sk-abc123DEF456ghi789JKL012mnopREDACTED_OPENAI_KEYzhangsanexample.comREDACTED_EMAIL10.20.30.40REDACTED_INTERNAL_IP/Users/zhangsan/work/app/main.pyREDACTED_PATH任意高熵随机 tokenREDACTED_HIGH_ENTROPY3. 环境准备与项目结构3.1 运行环境与依赖本地 scrubber 设计成 Python 命令行工具运行环境要求如下依赖版本要求用途Python3.9 及以上标准库re、math、argparsePyYAML5.4 及以上读取 YAML 规则配置pytest可选编写自动化回归测试如果原始材料没有给出明确版本落地前要先确认本机 Python 版本。这里使用的类型标注语法list[str]在 Python 3.9 才可用低于 3.9 需要改成typing.List[str]。3.2 项目结构llm-text-scrubber/ ├── scrubber.py # 核心实现和命令行入口 ├── scrubber.yaml # 规则配置 ├── prompt.txt # 待清洗的样例文本 ├── prompt.scrubbed.txt # 清洗后的输出 └── test_scrubber.py # 自动化回归测试实际项目可以增加rules/目录存放团队自定义规则也可以把配置改为 JSON 格式以便接入配置中心。3.3 配置文件 scrubber.yaml规则配置用 YAML 管理好处是可读性强、支持注释、适合版本管理。下面的配置包含熵检测参数、允许清单和几条常见规则# scrubber.yaml entropy: threshold: 4.2 min_token_length: 16 token_pattern: [A-Za-z0-9_\-]{16,} allowlist: - testexample.com rules: openai_key: enabled: true pattern: sk-[A-Za-z0-9]{16,} placeholder: REDACTED_OPENAI_KEY aws_access_key: enabled: true pattern: AKIA[0-9A-Z]{16} placeholder: REDACTED_AWS_ACCESS_KEY private_key_block: enabled: true pattern: -----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY----- placeholder: REDACTED_PRIVATE_KEY email: enabled: true pattern: [A-Za-z0-9._%-][A-Za-z0-9.-]\.[A-Za-z]{2,} placeholder: REDACTED_EMAIL internal_ip: enabled: true pattern: \b(?:10\.\d{1,3}\.\d{1,3}\.\d{1,3}|172\.(?:1[6-9]|2[0-9]|3[01])\.\d{1,3}\.\d{1,3}|192\.168\.\d{1,3}\.\d{1,3})\b placeholder: REDACTED_INTERNAL_IP absolute_path: enabled: true pattern: (?:/Users/[A-Za-z0-9_\-]|/home/[A-Za-z0-9_\-])/[^\s]* placeholder: REDACTED_PATH注意 YAML 单引号字符串里的反斜杠不会被转义正则中的\b、\d、\-原样传给 Python 的re模块这是规则能正确匹配的前提。如果发现规则不生效先怀疑 YAML 里反斜杠是否被吃掉了。4. 核心代码实现4.1 Scrubber 类核心类是Scrubber构造函数接收规则字典和熵参数编译正则提供scrub()方法。每次清洗把命中记录保存到history中方便后续报告。# scrubber.py from __future__ import annotations import math import re from typing import Optional import yaml class Scrubber: Rule-based text scrubber with high-entropy token detection. def __init__( self, rules: dict, entropy_threshold: float 4.2, min_token_length: int 16, token_pattern: str r[A-Za-z0-9_\-]{16,}, allowlist: list[str] | None None, ) - None: self.entropy_threshold entropy_threshold self.min_token_length min_token_length self.allowlist set(allowlist or []) self.history: list[dict] [] self._patterns: list[tuple[str, re.Pattern, str]] [] for name, rule in rules.items(): if not rule.get(enabled, True): continue pattern re.compile(rule[pattern]) placeholder rule.get(placeholder, fREDACTED_{name.upper()}) self._patterns.append((name, pattern, placeholder)) self._token_pattern re.compile(token_pattern) classmethod def from_config(cls, config: dict) - Scrubber: entropy config.get(entropy, {}) return cls( rulesconfig.get(rules, {}), entropy_thresholdentropy.get(threshold, 4.2), min_token_lengthentropy.get(min_token_length, 16), token_patternentropy.get(token_pattern, r[A-Za-z0-9_\-]{16,}), allowlistconfig.get(allowlist), ) def scrub(self, text: str) - str: self.history [] result text for name, compiled, placeholder in self._patterns: def _replace(match: re.Match, namename, placeholderplaceholder) - str: value match.group(0) if value in self.allowlist: return value self.history.append({type: name, value: value}) return placeholder result compiled.sub(_replace, result) result self._scrub_high_entropy(result) return result def _scrub_high_entropy(self, text: str) - str: def _replace(match: re.Match) - str: token match.group(0) if len(token) self.min_token_length and self._shannon_entropy(token) self.entropy_threshold: if token in self.allowlist: return token self.history.append({type: high_entropy, value: token}) return REDACTED_HIGH_ENTROPY return token return self._token_pattern.sub(_replace, text) staticmethod def _shannon_entropy(token: str) - float: if not token: return 0.0 length len(token) freq {ch: token.count(ch) for ch in set(token)} return -sum((count / length) * math.log2(count / length) for count in freq.values())这里最关键的实现决策是正则替换闭包使用默认参数namename、placeholderplaceholder避免 Python 闭包捕获循环变量导致占位符错乱。sub()是同步执行的但写成默认参数更安全也方便后续改成多线程。4.2 命令行入口命令行入口需要支持三种典型用法读文件、读标准输入、写文件。--report参数用于输出清洗报告把命中的类型和原值打印到标准错误流方便调试。# scrubber.py 追加 import argparse import sys from pathlib import Path def main() - int: parser argparse.ArgumentParser(descriptionScrub sensitive text before sending to an LLM) parser.add_argument(--config, defaultscrubber.yaml, helppath to YAML config) parser.add_argument(--input, typePath, helpread text from file) parser.add_argument(--output, typePath, helpwrite scrubbed text to file) parser.add_argument(--stdin, actionstore_true, helpread text from stdin) parser.add_argument(--report, actionstore_true, helpprint redaction report to stderr) args parser.parse_args() config_path Path(args.config) if not config_path.exists(): print(fconfig not found: {config_path}, filesys.stderr) return 2 with config_path.open(encodingutf-8) as f: config yaml.safe_load(f) scrubber Scrubber.from_config(config) if args.input: try: with args.input.open(encodingutf-8) as f: content f.read() except OSError as exc: print(fread input failed: {exc}, filesys.stderr) return 2 elif args.stdin: content sys.stdin.read() else: parser.error(need --input FILE or --stdin) cleaned scrubber.scrub(content) if args.output: with args.output.open(w, encodingutf-8) as f: f.write(cleaned) else: sys.stdout.write(cleaned) if args.report: for item in scrubber.history: print(f[{item[type]}] {item[value]!r}, filesys.stderr) return 0 if __name__ __main__: sys.exit(main())文件读写都显式指定encodingutf-8。不做这一步Windows 平台下可能读取到默认 GBK 编码中文文本会乱码或直接抛UnicodeDecodeError。这是本地工具最容易踩的坑。4.3 关键实现点正则执行顺序是按配置文件里rules字典的声明顺序。建议把大块规则放前面比如private_key_block要在通用随机 token 扫描之前执行。如果先做熵扫描私钥内部的随机字符会被逐个替换成REDACTED_HIGH_ENTROPY整块私钥结构就被破坏了。history列表中保存的是原文值这在学习环境方便调试但生产环境中如果把history打印到日志等于把原文写入日志文件造成二次泄露。生产实践是只记录类型和占位符或者把原文做不可逆哈希后再记录。5. 运行验证从日志样例到清洗结果5.1 准备测试文本创建一个prompt.txt内容模拟一段典型的需要交给 LLM 分析的错误日志请分析这段日志 Traceback (most recent call last): File /Users/zhangsan/work/app/main.py, line 42, in module call_llm(api_keysk-abc123DEF456ghi789JKL012mnop) File /Users/zhangsan/work/app/net.py, line 10, in connect client.connect(10.20.30.40, timeout30) 日志里出现的内网地址和 key 请重点说明。联系邮箱 zhangsanexample.com。这个样本包含绝对路径、OpenAI 风格密钥、内网 IP 和邮箱覆盖了配置文件里的四条主要规则。5.2 执行清洗命令先安装 PyYAMLpip install pyyaml然后运行python scrubber.py --config scrubber.yaml --input prompt.txt --output prompt.scrubbed.txt --report查看标准错误流里的报告[openai_key] sk-abc123DEF456ghi789JKL012mnop [internal_ip] 10.20.30.40 [absolute_path] /Users/zhangsan/work/app/main.py [absolute_path] /Users/zhangsan/work/app/net.py [email] zhangsanexample.com注意--report打印原文只适合本地调试。接入生产流水线时应关闭原文输出或改为只输出命中类型和行号。5.3 检查输出prompt.scrubbed.txt的内容应该是请分析这段日志 Traceback (most recent call last): File REDACTED_PATH, line 42, in module call_llm(api_keyREDACTED_OPENAI_KEY) File REDACTED_PATH, line 10, in connect client.connect(REDACTED_INTERNAL_IP, timeout30) 日志里出现的内网地址和 key 请重点说明。联系邮箱 REDACTED_EMAIL。验证点有三个所有敏感值都被替换非敏感内容保持原样“api_key”、“host”等字段名没有被替换因为规则只匹配值不匹配键名。这样 LLM 仍然能理解日志上下文只是拿不到真实密钥。5.4 用自动化测试固定行为清洗规则只要一改动就可能引入误伤或漏报。建议写一组 pytest 用例把测试样本和期望输出固定下来# test_scrubber.py import yaml from scrubber import Scrubber def load_scrubber() - Scrubber: with open(scrubber.yaml, encodingutf-8) as f: config yaml.safe_load(f) return Scrubber.from_config(config) def test_openai_key_redacted(): scrubber load_scrubber() cleaned scrubber.scrub(keysk-abc123DEF456ghi789JKL012mnop) assert sk- not in cleaned assert REDACTED_OPENAI_KEY in cleaned def test_allowlist_keeps_test_email(): scrubber load_scrubber() cleaned scrubber.scrub(contact testexample.com please) assert testexample.com in cleaned def test_normal_text_unchanged(): scrubber load_scrubber() cleaned scrubber.scrub(请分析这段错误日志) assert cleaned 请分析这段错误日志 def test_report_records_redaction(): scrubber load_scrubber() scrubber.scrub(ip10.20.30.40) assert any(item[type] internal_ip for item in scrubber.history)运行pytest -q每次修改规则或参数后跑一遍测试用例会告诉你两件事该挡的有没有挡住不该动的有没有被误伤。没有这套测试规则表会随着时间慢慢腐烂。6. 参数调优减少误伤稳定漏报6.1 需要调优的参数参数默认值调大的影响调小的影响调优建议entropy.threshold4.2漏掉更多随机 token误报减少更多普通文本被替换误报增加先收集样本统计正负例的熵分布再定entropy.min_token_length16短随机串漏过普通短词也可能被扫保持 12 到 20结合业务 token 长度token_pattern[A-Za-z0-9_\-]{16,}覆盖面变小匹配范围变大如果密钥含特殊字符扩展字符类规则顺序配置声明顺序大块规则靠前更安全通用规则靠前会破坏结构私钥块、证书块永远放在最前6
返回列表