O que vive por baixo da cabeça: quatro botões de treino, três sementes que passam e o seq_len que fez todo o trabalho
Há dois posts, reconstruímos um corpus que nos andava a mentir. Há um post, contámos-lhe as dezassete execuções de treino falhadas antes de algo funcionar. Este é o capítulo três de nós a estarmos errados em público, com números anexados.
A versão curta: a nossa porta de produção, supA-v3, passou cinco das seis portas de lançamento pré-registadas à primeira. A sexta — falsos negativos por língua — falhava exclusivamente porque ataques em russo passavam. Podíamos tê-la lançado na mesma; nada no contrato de lançamento exige uma divisão perfeita entre línguas. Mas uma porta que prefere discretamente o inglês é uma responsabilidade para quem implementa agentes fora dos fóruns anglófonos, por isso corremos uma campanha de treino focada para descobrir porque é que acontecia.
A resposta acabou por ser embaraçosamente mecânica. O que se segue é tudo o que aprendemos, cada número que vimos e uma observação um pouco incómoda sobre como seria fácil ter deixado passar esta correção em particular.
Resumo
- A v3 passou a FPR (0,64% contra um teto de ≤1%), mas deixou passar 9,11% dos ataques em russo, estourando em 1,1 pontos percentuais o nosso orçamento de FNR por língua.
- O nosso primeiro instinto — aumentar o peso das amostras de ataque em russo na perda para um peso efetivo de x5 — piorou as coisas. O FNR em RU saltou de 9,11% para 31,72%. Mais peso mexeu nas pontuações sem melhorar o ranking.
- O AUROC manteve-se entre 0,99957 e 0,99989 nas dez execuções, o que sugere que não estávamos a corrigir o ranking. Estávamos a redistribuir os pontos de operação.
- O verdadeiro vencedor foi
seq_len=96, não qualquer botão de ponderação ou taxa de aprendizagem. Foi a única alteração que expandiu o que o codificador conseguia ver durante tempo suficiente para notar que os padrões de injeção multilingues eram mais longos do que a janela de codificação. - Três reexecuções independentes com sementes diferentes confirmaram a estabilidade: os limiares variaram (como a calibração exige que variem), mas os resultados das portas reproduziram-se de forma idêntica.
- Dez execuções de treino no total na campanha. A experiência mais barata ensinou-nos mais do que tudo o resto que fizemos neste trimestre.
- Em produção em
POST /v30/analyze— veja a documentação para programadores para autenticação, esquema de pedidos e exemplos de chamadas.
Onde ficámos
O corpus está público no Hugging Face, CC-BY-4.0, com a proveniência de cada linha. O pipeline de treino e as portas de lançamento estão pré-registados como JSON legível por máquina antes de qualquer modelo tocar nos dados. Cada execução de treino é registada automaticamente com a sua configuração, o hash do artefacto e o resultado da avaliação, pelo que a campanha se lê como uma série auditável e não como um vídeo de melhores momentos.
Para a campanha v3 treinámos com 10.554 linhas: 9.754 transitadas da v3 mais 800 perguntas benignas novas em russo, abrangendo temas de passatempo que nenhum item da partição de validação tocava. As verificações de contaminação saíram limpas. O limiar calibrado deu um FP praticamente zero. Um problema surgiu logo: as taxas de falsos negativos por língua contavam uma história feia.
Cinco línguas saíram praticamente de graça. A sexta não.
Primeira tentativa: carregar mais
A intuição óbvia quando uma fatia específica falha no recall é carregar mais nessa fatia durante o treino. Ponderar mais a perda para o que se quer apanhar com mais frequência. Se o x3 em casos limite já fazia parte da receita base, talvez os ataques em russo precisassem de um x15 no total.
A execução funcionou mecanicamente: o treino terminou, o artefacto apareceu, a contaminação estava limpa. Depois a avaliação chegou e deixou-nos em silêncio durante um segundo:
O FNR em RU passou de 9,11% para 31,72%.
O AUROC mexeu-se pouco (0,99957 → 0,99969). Essa discrepância importa. Um modelo cujo ranking não melhora não pode ser corrigido mudando os pesos da perda: só se está a deslocar o ponto de corte ao longo de uma curva fixa. E foi exatamente isso que aconteceu: o limiar calibrado acabou empurrado para 0,99, o que significa que quase todas as decisões ficaram muito mais conservadoras, e o colapso do recall seguiu-se mecanicamente.
A varredura de limiares mostrou o quadro real. Em cada ponto da grelha onde a FPR descia abaixo do nosso teto de 1%, o FNR em RU ficava acima da porta de 8%, e vice-versa. As duas restrições não se cruzam para essa configuração. Nenhuma quantidade de procura de limiares encontra um valor que não existe.
Segunda tentativa: largar os pesos
Se empurrar o peso da perda não funciona, a hipótese razoável seguinte é arquitetural. Em particular, o russo era tokenizado em mais tokens do que as outras cinco línguas com o tokenizador do MiniLM — cerca de 30 a 40% mais tokens em média para conteúdo semanticamente equivalente. Se as sequências estivessem a ser cortadas aos 64 tokens, a cauda de um ataque poderia ser invisível.
Testámos duas variantes:
- seq_len = 96, tudo o resto por defeito
- seq_len = 128 + lr_head subida para 2e-3
Ambas passaram. Limpamente. Todas as portas. Em sequência.
Esse padrão — as duas experiências de comprimento de sequência a passar independentemente dos ajustes de taxa de aprendizagem, enquanto todas as variantes que não mexiam no comprimento continuavam a falhar — é exatamente o que parece uma história de janela de tokenização quando aparece nos seus dados.
Quatro configurações independentes atingiram TODAS AS PORTAS PASSAM:
Três outros candidatos falharam uma ou mais portas, sobretudo variantes de sequência curta em que uma taxa de aprendizagem mais alta ainda era insuficiente.
O lado direito desse gráfico é aborrecido por desenho. As duas configurações que passam estão na mesma vizinhança, quer tenhamos duplicado a taxa de aprendizagem quer não. O comprimento da sequência é que faz o trabalho pesado.
Confirmar que não foi sorte
Quatro aprovações num varrimento não provam causalidade. Para verificar que estávamos perante uma receita robusta e não uma combinação afortunada de hiperparâmetros, voltámos a treinar exatamente a configuração vencedora duas vezes, com sementes aleatórias diferentes. Tudo o resto igual.
O limiar calibrado mexeu-se (0,50 → 0,91 → 0,80), como esperado — as distribuições de probabilidade deslocam-se ligeiramente por semente. Os resultados das portas não se mexeram. Quando inicializações aleatórias diferentes produzem posicionamentos de limiar independentes mas conclusões idênticas nas portas duras, tem-se um achado e não uma anedota.
Combinando isto com os varrimentos de tipo validação cruzada acima, temos agora sete configurações distintas a passar a suíte pré-registada completa, todas remontando à mesma escolha estrutural subjacente.
Como é de facto a execução vencedora
Na partição de retenção de regressão congelada, com o limiar calibrado da execução que implementamos:
- Taxa de falsos positivos: 0,64% medidos — 1 em cada 156 prompts negativos limpos disparou
- Falsos negativos por língua: RU 0,56% — IT 0,00% — PT 0,00% — ES 0,00% — DE 0,12% — EN 0,00%
- AUROC sobre bench-core: 0,99986
- Taxa de captura nas falhas do PromptGuard: 98,8%
- Latência de inferência em CPU (int8 ONNX, batch=1): p50 ≈ 27 ms, p95 ≈ 45 ms
O que isto NÃO resolve
Ser honesto sobre os limites é precisamente o objetivo de publicar estes números em público.
Testámos a capacidade de cada modelo para apanhar parafrasear coisas que o motor de regras já conhece, mais ataques sintéticos genuinamente novos gerados contra esta família de corpus. O comportamento real do adversário muda mais depressa do que qualquer dataset offline consegue acompanhar. Algo acabará por contornar tanto as regex como o classificador de formas que as nossas retenções nunca anteciparam. O objetivo de medir com rigor é saber com o que se está a lidar, não fingir que a deteção ficou resolvida para sempre.
Segundo: a configuração vencedora usa sequências de 96 tokens em vez de 64. O custo de inferência em CPU sobe moderadamente (cerca de 45% mais lento do que seq64 no nosso hardware). Também não há razão para que comprimentos de sequência maiores continuem a ajudar indefinidamente; o modo de falha em causa é especificamente o corte de frases russas mais longas, e para além dessa janela está a pagar computação por um benefício marginal.
Terceiro: as exclusões das partições canário e bench-private-holdout são mantidas por asserção de IDs. Essa disciplina de contaminação aguenta auditorias, mas é frágil face a fontes de corpus novas que ainda não ingerimos.
Quarto: nove configurações desta campanha falharam uma ou mais portas antes de alguma ter sucesso. Publicar apenas histórias de aprovação não ensina nada sobre como construir o pipeline que as apanha. Cada falha — incluindo as catastróficas, como o colapso de x5 no peso para 31,72% — fica preservada no registo de commits exatamente como correu.
O que se segue
Implementação em modo sombra junto do motor de regras determinista em POST /v30/analyze, em modo OR combinado: uma correspondência de regex ou uma pontuação do modelo acima do limiar resulta em bloqueio; cada uma por si não resulta necessariamente em permissão. O motor de regras trata do que a cabeça deixar passar; a rede neuronal apanha tudo o que as regex não conseguem ver. O empilhamento de latência é intencional: o caminho de regex é leve e funciona como recurso se o runtime ONNX se portar mal.
Quer experimentar? Vá a a documentação para programadores: o contrato completo de /v30/analyze, a configuração de autenticação e o esquema de pedidos estão lá documentados, juntamente com /openapi.json quando as métricas em sombra estabilizarem. As notas de integração de cliente chegarão ao changelog depois do período de teste em staging.
Atualizado a 14 de outubro de 2026: redação alterada para apresentar resultados medidos.