Cómo reconstruimos nuestro dataset de inyección de prompts después de que nos mintiera en silencio
El día antes de publicar zn en código abierto, publicamos un resultado que nos gustó: nuestra puerta fusionada de inyección aguantó frente a un modelo de Meta mucho más grande en nuestro propio benchmark de 217 vectores. Después volvimos a entrenar la siguiente revisión cabezal del modelo y descubrimos que nuestros datos de entrenamiento nos habían estado mintiendo durante meses. Este artículo es la autopsia y la reconstrucción.
El diagnóstico
En el checkpoint v13d entrenamos un cabezal de detección con el corpus que teníamos entonces. Las métricas de entrenamiento parecían bien. La evaluación contra cualquier cosa real no. Las entradas benignas genuinas (prompts de usuarios corrientes, rechazos de LLM, rastros de generación automática de ataques que nunca contienen una carga) obtuvieron un logit medio de 4,26. Los ataques simples promediaron 2,83. Nuestra clase benigna estaba por encima de nuestra clase de ataque.
El cabezal no había aprendido "inyección frente a benigno". Había aprendido la frontera de un corpus sintético: cadenas cortas basadas en plantillas frente a todo lo demás. Cada texto real estaba fuera de distribución, y lo que está fuera de distribución se desplazaba hacia arriba. Los documentos reales parecían más "ataque" que los propios ataques, porque los ataques salían de la misma fábrica de plantillas que los benignos.
¿Podíamos recalibrar los umbrales y seguir adelante? No. Antes de entrenar habíamos registrado una regla de parada: si un conjunto externo de prompts del mundo real se separaba de nuestro pool por debajo de AUROC 0,75, el dataset está roto y ningún ajuste de umbrales puede salvarlo. La separabilidad medida fue de 0,33, por debajo incluso de 0,5, lo que significa que el ranking estaba efectivamente invertido. La regla de parada se disparó exactamente como estaba diseñada.
Para ser precisos con las culpas: no era un problema de arquitectura. Ninguna función de pérdida, muestreador o truco de calibración arregla un espacio de etiquetas donde la clase positiva se define como "parece nuestro corpus sintético".
Lo que construimos
Reconstruimos el corpus a partir de un plano de familias en lugar de un montón de plantillas:
- Familias de ataque: simple, red-team, parafraseo y multilingüe.
- Familias benignas: prompts cortos, prosa real larga, documentación, transcripciones conversacionales y meta-texto.
- Regla de diseño: el texto benigno debe cubrir la distribución del texto real, no la distribución de nuestro generador.
La familia de meta-texto merece una mención aparte porque es una vacuna directa contra el modo de fallo descrito arriba. Son documentos que hablan de ataques de inyección (consideraciones de seguridad en RFC, secciones de seguridad de la documentación) sin contener ninguna carga. Son benignos según la verdad de referencia, provienen al 100 % de fuentes naturales y viven precisamente en la región donde murió el cabezal antiguo: texto que discute ataques en prosa llana. Si un clasificador puede ser convencido de marcar un modelo de amenazas, no ha aprendido amenazas; ha aprendido palabras clave.
Los parafraseos son deliberadamente sencillos. Ningún LLM los reescribe. Una tabla fija de 53 pares de sinónimos habituales en la redacción de inyecciones, reordenamiento seguro de cláusulas emparejado por expresiones regulares, prefijos y sufijos contextuales tomados de listas fijas, y mayúsculas y puntuación controladas: una sola pasada, sin reescanear los reemplazos, de modo que las transformaciones no se encadenen en algo no deseado. Cada elemento registra su procedencia como id de semilla más lista de transformaciones, con la fecha de procedencia congelada para que las re-ejecuciones sean idénticas byte a byte. Máximo 3 variantes por semilla. Lo diremos claramente: la sustitución mecánica es variación superficial, no diversidad semántica. Ancla la superficie, y no afirmamos más que eso.
La familia multilingüe es nueva en v15, y la honestidad exige decir qué es y qué no es. Son 1.812 ataques en 8 locales, en francés, español, portugués, italiano, neerlandés, polaco, alemán y ruso, producidos traduciendo automáticamente semillas de ataque en inglés con Qwen3-32B de Amazon Bedrock, por un gasto total de 0,07 $, detrás de una caché de respuestas congelada y determinista para que las re-ejecuciones sean idénticas byte a byte. Cada elemento registra su procedencia como id de semilla más locale más id del modelo. Una inyección traducida sigue siendo una inyección que la puerta debe detectar, así que es un ancla de capacidad interlingüística real, pero es cobertura a nivel superficial: inglés traducido automáticamente, no patrones de ataque escritos de forma nativa para cada locale. Las semillas mantienen hashes estables, y el particionador consciente de grupos coloca juntas cada semilla y sus traducciones, de modo que una traducción nunca puede cruzar entre entrenamiento y prueba.
Las particiones son conscientes de grupos mediante agrupación de casi duplicados con MinHash y k=128, de modo que un texto y sus casi gemelos nunca puedan cruzar entre entrenamiento y prueba. Nos obsesionamos con esto porque auditamos el corpus antiguo y encontramos fugas documentadas frente a nuestros propios conjuntos de evaluación: 3 de 217 vectores del benchmark y 1 de 60 elementos de otro conjunto externo se solapaban con datos de entrenamiento. La auditoría posterior a la partición se ejecutó sobre los 23.699 textos: 0 infracciones.
Los números
Solo lo que es trazable a artefactos bajo evidence/v15/:
| Métrica | Valor |
|---|---|
| Textos | 23.699 |
| Familias de ataque | simple, red-team, parafraseo, multilingüe |
| Parafraseos | 2.500, máximo 3 variantes por semilla |
| Ataques multilingües | 1.812 en 8 locales (traducidos automáticamente, declarado) |
| Familia de meta-texto | 966 elementos, 100 % fuentes naturales |
| Auditorías de partición | 7 comprobaciones superadas, 0 infracciones |
Todavía no hay cifras de exactitud, porque el objetivo de este artículo son los datos, no un modelo entrenado con ellos.
El corpus es público: zn-prompt-injection-bench en Hugging Face, publicado bajo CC-BY-4.0 con las licencias de cada fuente documentadas en la ficha del dataset.
Lo que todavía falta
Tres huecos, dichos en lugar de tapados:
- La cobertura multilingüe es inglés traducido automáticamente. Los ocho locales contienen semillas traducidas, no ataques escritos de forma nativa por hablantes de cada idioma. Los hábitos de redacción propios de cada locale y los patrones de ofuscación están poco representados, así que tratamos la familia como un ancla de capacidad interlingüística, no como profundidad.
- El parafraseo mecánico es variación superficial. No puede sustituir a formulaciones de ataque semánticamente distintas, y lo tratamos así al interpretar los resultados.
- 1.416 filas sintéticas siguen privadas. Derivan de nuestro corpus de producción interno (el segmento
sb4_mutants), así que las retenemos en lugar de redistribuirlas. Su existencia se declara en la sección de limitaciones de la ficha del dataset; los datos en sí no son públicos.
Siguiente paso
Congelar el corpus, ejecutar el barrido en GPU bajo las puertas preregistradas G1 a G7 y publicar lo que salga, incluso si es feo. Ese es el pacto que hicimos con nosotros mismos cuando se disparó la regla de parada de v13d.
Si has construido corpus de inyección y llevas las cicatrices, queremos tu revisión. Critica la metodología, las cuotas por familia y el pipeline de parafraseo. El detector en sí se distribuye como un único binario de Rust, la documentación de integración está en /docs, la especificación completa de la API en /openapi.json, y la metodología de benchmark de la comparación anterior está en el post anterior. Encuentra lo que hicimos mal antes de volver a entrenar con esto.