Agent 训练数据市场 / 数据脱敏
实做记录 · 2026-08
专题二 · 供给侧

数据脱敏相关调研

一套真正跑过 5.4 GB 真实 coding-agent 会话的凭证清除方案——不是纸面方案。它解决了一个很具体的问题,也明确没有解决另外两个更大的问题。两部分同样重要。

18,380
条完整对话
129 万
条消息
5.4 GB
原始数据量
4.3%
的记录里混着活体凭证
9,837
次替换 · 复扫零残留
背景

数据来自 API 中转层录制的真实 coding-agent 会话。真实流量里不可避免地混着活体凭证——API key、云平台密钥、私钥文件内容——而且它们会出现在任何位置:消息正文、工具调用参数、工具返回、用户随手粘贴的代码块。

目标很明确:清除所有可用凭证,同时对数据做字节级的最小改动。训练数据的价值在于真实性,所以消息文本、代码、文件路径、项目名一律保持原样,只有凭证子串被换成带类型的占位符。工具是一个约 130 行、无第三方依赖的 Python 脚本。

在字节层替换,不解析 JSON

脚本逐行处理 JSONL,直接对原始字符串做正则替换,不做「反序列化 → 改 → 重新序列化」。这个选择贯穿了后面所有设计,理由有三个:

01
无死角
凭证可能藏在任何嵌套深度——消息文本里、工具参数里、工具返回里、粘贴的代码里。在字符串层面替换,无论它在结构的哪个位置都会被命中,不需要预先知道数据长什么样。
02
不动结构
5 GB 数据不用重排。替换点之外的每一个字节都与原始录制完全一致——这对「保真」这个目标很关键,重新序列化会悄悄改变键序、空白和数字格式。
03
可验证
处理后逐行 json.loads 校验,任何一次替换只要破坏了 JSON 结构就立即中止,杜绝静默损坏。宁可整批失败,不要悄悄产出坏数据。

代价是:正则必须自己尊重 JSON 的转义规则。这个代价在第二层里变成了一个真实的坑,见下文的护栏。

八条类型化正则

按顺序应用,更特殊的模式必须排在前面——sk-ant- 要先于 sk-,否则 Anthropic 的 key 会被误标成 OpenAI 的。

类别匹配要点占位符
private_key-----BEGIN … PRIVATE KEY----- 起始,吞掉后续 base64 体;END 标记可选<REDACTED_PRIVATE_KEY>
anthropic_keysk-ant- + 20 位以上<REDACTED_ANTHROPIC_KEY>
openai_keysk-(负向前瞻排除 sk-ant-)+ 20 位以上<REDACTED_OPENAI_KEY>
github_tokenghp_ / gho_ / ghu_ / ghs_ / ghr_github_pat_<REDACTED_GITHUB_TOKEN>
aws_keyAKIA + 16 位<REDACTED_AWS_KEY>
gcp_keyAIza + 35 位<REDACTED_GCP_KEY>
slack_tokenxox[baprs]-<REDACTED_SLACK_TOKEN>
bearerBearer 后 30 位以上的 token(保留 Bearer 字样Bearer <REDACTED_TOKEN>
一个容易忽略的细节

私钥规则刻意做成「从头吃到 base64 体」,而不是「从头匹配到尾标记」。因为对话里出现的私钥经常是粘贴过来的半截——没有 -----END-----——但残体依然是可用的密钥材料,必须照删。要求匹配到结束标记的规则会把它们全部漏掉。

被源码「打散」的私钥

最麻烦的情况是私钥以代码形态出现:被宿主语言包装,比如字符串拼接、转义的换行、heredoc。此时私钥被切成许多短 base64 片段,任何「整块匹配」的正则都必然漏

// 真实数据里长这样 —— 密钥被 " + " 切成了碎片
// 整块正则匹配不到任何一段足够长的 base64

const PRIVATE_KEY =
  "-----BEGIN RSA PRIVATE KEY-----\n" +   ← 只有头是完整的
  "MIIEowIBAAKCAQEAxK9fQ2" +                  ← 22 字符,太短
  "vN8mR4tYuI0pAsDfGhJkLz" +                  ← 22 字符,太短
  "XcVbNmQwErTyUiOpAsDfGh\n" +                ← 22 字符,太短
  "-----END RSA PRIVATE KEY-----";

对策是放弃匹配「块的形状」,改为锚定加清扫

锚定
找到每个 BEGIN 头
不管后面被切得多碎,头几乎总是完整的——因为它是一个整体的字符串字面量。以它作为定位锚点。
求证
在头之后 6,000 字符的窗口内,找一段 ≥40 字符的连续 base64
找到了,才认定这里真的是密钥区。这一步是为了避免误伤:如果只是散文里提了一句「PEM 格式」,窗口里不会有大段 base64,内容原样不动。
清扫
确认后,把窗口内所有 ≥12 字符的 base64 片段全部替换掉
阈值从 40 降到 12——拼接打散的短片段由此一网打尽。降阈值之所以安全,正是因为「求证」那一步已经确认了这块区域的性质。
⚠️ 护栏:字节层替换的代价在这里现形

base64 的匹配带一个负向后顾断言 (?<!\\)禁止从 JSON 转义序列的字母上起步

因为 \n\uXXXX 里的 nu 本身就是合法的 base64 字符。不加这道护栏,正则会从转义序列的字母开始吃,吃掉那个字母、留下一个悬空的反斜杠,整条 JSON 记录就此损坏

替换统计与验证

类别替换次数说明
openai_key9,358占了绝大多数
key_material369第二层「锚定–求证–清扫」的战果
bearer45
github_token41
aws_key20
gcp_key2
anthropic_key1
private_key1整块完整的私钥只有一个——其余全是被打散的
slack_token0

计数按出现次数:同一把密钥每出现一次记一次,所以次数远大于不同密钥的数量。796 条记录(4.3%)含凭证

事后独立验证

① 对输出文件重新扫描全部凭证模式,零残留;② 全部 18,380 条记录仍能通过 JSON 解析(逐行校验在替换时同步执行,坏一条即中止导出)。

另有一道与脚本无关的前置防线:中转层导出时从不携带调用方元数据(IP、token 哈希等),它们根本不进数据集。

正则迭代了三轮才收敛

两个坑都在私钥上,而且都指向同一个方向:

对「密钥长什么样」建模是脆弱的;
对「密钥出现在哪个语境」建模——header 锚点 + 邻近窗口——更稳。
— 这套方案最有迁移价值的一条经验

它没有解决什么

原文档第 6 节把边界写得很清楚:脱的是各类活体凭证,无论出现在文本、代码还是工具输出里;不脱的是文件路径、项目名、用户粘贴的其他内容——全部原样保留

这个诚实的边界声明,恰好点出了一个更大的问题:日常说的「脱敏」其实混着三件完全不同的事,而这套方案只覆盖第一件。

凭证不泄露 已解决
这是安全问题。上面整套机制解决的就是它,而且解得很扎实:零残留、结构无损、有独立复验。
来源不可追溯 未解决
这是重识别问题。文件路径、项目名、内部服务名全部原样保留——而代码风格、依赖组合、目录结构本身就是指纹。去掉密钥不等于抹掉来源。
有权转售 脱敏解决不了
这是权利问题,而且它本质上不是技术问题。雇主的代码、客户信息、第三方网页内容、闭源模型的输出——都不能默认贡献者有权出售。再干净的脱敏也变不出一份授权。
⚠️ 这三层为什么必须分开讲

因为对外说「我们做了脱敏」时,对方听到的往往是第三层。而买方真正会追问的也是第三层——他们关心的是授权链,不是密钥。

把三件事混成一句「已脱敏」,会让「安全上没问题」被误读成「法律上可以卖了」。这两件事之间隔着的东西,一行正则也补不上

市场上买方怎么描述同一件事

把这套方案和买方地图里 Mercor Data 的公开说法放在一起,差距很说明问题。

维度本方案Mercor Data 的公开描述
目标清除活体凭证不可逆遮蔽 PII、PHI、BII(个人 / 健康 / 商业身份信息)
手段类型化正则 + 语境锚定窗口「确定性模式匹配 + 生成式 AI」
对原文的改动字节级最小改动,替换点之外完全一致未公开
在流程中的位置数据集导出后的后处理产品流程的内建一环,且前置了只读 OAuth 授权与企业签约
授权链不在处理范围内由企业签约与 OAuth 授权在抽取之前就解决
⚠️ 推论

差别不在正则写得好不好——本方案在纯技术层面做得更细致,也更透明。

差别在顺序:Mercor 把授权放在采集之前,所以它的匿名化只需要解决安全问题;而对一份已经录好的存量数据做后处理,无论做得多干净,都补不回一个当初不存在的授权。这也正是买方地图里那条链的含义——市场付费的形态是「先拿到同意,再采集」,不是「先采集,再脱敏」。

← 返回研究索引