在我们的提示注入数据集悄悄误导了我们之后,我们如何重建它
在我们将 zn 开源的前一天,我们发布了一个我们很满意的结果:在我们自己的 217 向量基准上,我们的融合注入门控表现可以与一个大得多的 Meta 模型相抗衡。之后我们回头训练模型头部的下一个版本,才发现我们的训练数据已经误导了我们好几个月。本文是事后复盘和重建过程的记录。
诊断
在 v13d 检查点,我们用当时的语料库训练了一个检测头。训练指标看起来正常,但在任何真实数据上的评估都不正常。真正的良性输入(普通用户提示、LLM 的拒绝回复、从未包含任何载荷的自动化攻击生成痕迹)的平均 logit 为 4.26。普通攻击的平均值则为 2.83。我们的良性类别竟然高于攻击类别。
这个检测头并没有学会"注入与良性"的区分。它学会的是一个合成语料库的边界:短小的模板化字符串与其他一切。每一段真实文本都处于分布之外,而分布之外的文本会被映射到更高的分数。真实文档比攻击本身更像"攻击",因为攻击与良性模板出自同一个模板工厂。
我们能否重新校准阈值然后继续前进?不能。在训练之前,我们就登记了一条停止规则:如果一组真实世界的外部提示与我们的样本池的 AUROC 低于 0.75,数据集即被视为已损坏,任何阈值调整都无法挽救。实测的可分性是 0.33,甚至低于 0.5,这意味着排序实际上是颠倒的。停止规则完全按设计触发了。
需要明确责任归属:这不是架构问题。没有任何损失函数、采样器或校准技巧,能修复一个将正类定义为"看起来像我们合成语料库"的标签空间。
我们构建了什么
我们不再依赖一堆模板,而是按照家族蓝图重建了语料库:
- 攻击家族:普通、红队、改写和多语言。
- 良性家族:短提示、长篇真实文本、文档、对话式记录和元文本。
- 设计规则:良性文本必须覆盖真实文本的分布,而不是我们生成器的分布。
元文本家族值得单独说明,因为它直接针对上述失败模式。这些文档讨论注入攻击(例如 RFC 中的安全考虑、文档中的安全章节),但不包含任何载荷。按照真值标注,它们是良性的,100% 来自自然来源,并且恰好位于旧检测头失效的区域:用平实的文字讨论攻击。如果一个分类器可以被说服去标记一份威胁模型,那它学到的并不是威胁,而是关键词。
改写刻意保持朴素。没有任何 LLM 参与改写。我们使用一张固定的 53 组同义词对表(它们在注入话术中很常见),配合由正则匹配的安全子句重排、从固定列表中选取的上下文前缀和后缀,以及受控的大小写与标点。处理只进行一遍,替换结果从不再次扫描,因此变换不会串联成非预期的内容。每条记录都以种子 id 加变换列表的形式记录来源,并冻结来源日期,使重新运行的结果逐字节一致。每个种子最多 3 个变体。我们直说:机械替换属于表层变化,而非语义多样性。它锚定了表层,但我们不声称更多。
多语言家族是 v15 的新内容,诚实起见,需要说明它是什么、不是什么。它包含 8 种语言(法语、西班牙语、葡萄牙语、意大利语、荷兰语、波兰语、德语、俄语)中的 1,812 条攻击,由 Amazon Bedrock 的 Qwen3-32B 对英文攻击种子进行机器翻译生成,总花费 0.07 美元,并通过一个确定性的冻结响应缓存,使重新运行的结果逐字节一致。每条记录都以种子 id、语言代码和模型 id 的形式记录来源。翻译后的注入仍然是门控必须拦截的注入,因此它是真实的跨语言能力锚点,但它属于表层覆盖:是机器翻译的英文,而非每种语言母语者原创的攻击模式。种子的哈希保持稳定,按组工作的切分器会把每个种子和它的翻译放在同一侧,因此翻译永远不会同时出现在训练集和测试集中。
数据切分通过 MinHash 近似重复分组(k=128)实现按组划分,因此一段文本及其近似孪生文本永远不会同时出现在训练集和测试集中。我们之所以如此执着,是因为我们审计旧语料库时,发现它与我们自己的评估集存在已记录的泄漏:基准中的 217 个向量中有 3 个,以及另一外部数据集中的 60 个条目中有 1 个,与训练数据重叠。在全部 23,699 条文本上运行的切分后审计结果为:0 处违规。
数字
以下仅列出可追溯到 evidence/v15/ 下产物的内容:
| 指标 | 数值 |
|---|---|
| 文本数 | 23,699 |
| 攻击家族 | 普通、红队、改写、多语言 |
| 改写数 | 2,500,每个种子最多 3 个变体 |
| 多语言攻击 | 8 种语言中的 1,812 条(机器翻译,已披露) |
| 元文本家族 | 966 条,100% 来自自然来源 |
| 切分审计 | 7 项检查通过,0 处违规 |
目前尚无准确率数字,因为本文的重点是数据,而非基于该数据训练的模型。
该语料库已公开:Hugging Face 上的 zn-prompt-injection-bench,采用 CC-BY-4.0 发布,各数据来源的许可证记录在数据集卡片中。
目前仍然缺失的部分
三个缺口,我们如实陈述,而非掩饰:
- 多语言覆盖是机器翻译的英文。 这八种语言包含的是翻译后的种子,而非各语言母语者原创的攻击。特定语言的措辞习惯和混淆模式代表性不足,因此我们将该家族视为跨语言能力的锚点,而非深度覆盖。
- 机械改写属于表层变化。 它无法替代语义上真正不同的攻击措辞,在解读结果时我们也据此对待它。
- 1,416 条合成数据保持私有。 它们源自我们内部的生产语料库(
sb4_mutants切片),因此我们保留而不再分发。它们的存在已在数据集卡片的局限性部分披露;数据本身不公开。
下一步
冻结语料库,在预先登记的门控 G1 至 G7 下运行 GPU 扫描,并公布无论结果如何的全部输出,包括不理想的结果。这是我们在 v13d 停止规则触发时与自己达成的约定。
如果你构建过注入语料库,并且有过类似的教训,我们希望得到你的评审。请审视这套方法论、各家族配额以及改写流水线。检测器本身以单一 Rust 二进制文件的形式发布,集成文档位于 /docs,完整 API 规范位于 /openapi.json,此前对比中的基准方法论则见于上一篇文章。请在我们再次基于此训练之前,找出我们做错了什么。