Fala pessoal,

Pega o café, que esse é sobre um erro que quase passou por mim.

Em setembro eu testei quatro jeitos de apagar CPF e cartão SUS de 200 notas clínicas. Tudo sintético, tudo rodando local, no meu Mac. Um deles, o Phi-4, achou o CPF nas notas. E mesmo assim deixou número de documento em quatro delas.

A resposta curta, pra quem está com pressa: se você usa um modelo de linguagem pra achar dado pessoal e depois mascara o trecho que ele devolveu, confere se esse trecho existe igualzinho no texto original. Se não existe, a máscara não acha nada e o dado fica. Sem erro e sem aviso.

O resto é como eu cheguei nisso, os números, o código pra checar e onde o meu teste é fraco.

Por que eu fui testar isso?

O NursIA é um simulador de raciocínio clínico pra ensino de enfermagem que eu desenvolvo no mestrado (PPGINFOS/UFSC, com bolsa FAPESC). Texto com cara de prontuário passa por anonimização antes de chegar em qualquer modelo. Isso não é opcional quando a LGPD trata dado de saúde como dado sensível (art. 5º, II).

A pergunta era simples: pra texto em português, com CPF, cartão SUS, telefone e endereço do jeito que a gente escreve aqui, qual ferramenta dá conta?

Em vez de confiar em benchmark de fornecedor, montei um teste. Virou a demo 06.

Como foi o teste?

  • 200 notas clínicas sintéticas em português, geradas por template. Nome, CPF, cartão SUS (CNS), telefone, endereço e data. Tudo inventado.
  • Quatro configurações:
    • Presidio padrão (ferramenta open source de detecção e anonimização de dado pessoal)
    • Presidio com reconhecedores brasileiros (presidio-br)
    • Qwen3.8 27B rodando local
    • Phi-4 14B rodando local
  • Um Mac M4 Max com 36 GB. Modelos via Ollama, quantizados em 4 bits.

A pergunta que eu fiz a cada ferramenta: em quantas notas um CPF ou cartão SUS sobreviveu?

O que saiu?

ConfiguraçãoNotas em que CPF ou CNS sobreviveuF1 geral
Presidio padrão200 de 200—
Presidio + presidio-br0 de 200—
Qwen3.8 27B0 de 20099,9%
Phi-4 14B4 de 20098,9%

(—: deixei o F1 geral das duas configurações de regra fora da tabela. Nelas, o que conta é o acerto por tipo de dado, que vem mais abaixo.)

O Presidio padrão vazando tudo não me surpreendeu. Ele foi pensado primeiro pro inglês e não conhece o nosso CPF. Com o pacote brasileiro, zerou.

O que me pegou foi o Phi. 98,9% de F1. Olhando só esse número, eu teria passado direto. Confesso que quase passei.

Como um modelo acerta e o dado vaza?

Fui abrir as quatro notas, uma por uma.

Em três delas, o CPF estava escrito sem pontuação, tipo 81013315650 (sintético, gerado por algoritmo, calma). O modelo devolveu 810.133.156-50. Na quarta, o cartão SUS estava escrito com espaços e voltou sem.

Agora junta isso com o jeito que a maioria dos pipelines funciona:

  1. o modelo devolve a lista de trechos sensíveis;
  2. o código procura cada trecho no texto original;
  3. o que achar, troca por uma máscara.

A busca é exata. “810.133.156-50” não existe na nota. Nada é trocado. O CPF original continua lá, inteirinho.

O modelo fez o que um humano caprichoso faria: arrumou o número. (Gentileza que ninguém pediu.) Ele acertou a detecção. Quem errou foi a ponte entre o que ele devolveu e o texto de verdade. E essa ponte não aparece na métrica.

Quais testes eu faço agora?

Depois disso, passei a rodar três testes antes de confiar em qualquer ferramenta pra apagar dado pessoal.

  1. Formato brasileiro. CPF com e sem ponto. Cartão SUS com e sem espaço. Data por extenso. No meu teste, o Presidio padrão não reconheceu nenhum dos 133 CPFs. Com o presidio-br, reconheceu os 133. Ferramenta feita pro inglês primeiro não conhece documento brasileiro, e ninguém te avisa disso.
  2. Cópia exata. Todo trecho que a IA devolve tem que existir igualzinho no original. Se não existe, é alarme. Esse foi o teste que pegou o Phi.
  3. Acerto por tipo de dado, nunca só a média. O Presidio com presidio-br nunca perdeu um CPF ou um cartão SUS. Mas zerou endereço e perdeu metade das datas escritas por extenso. No CPF e no cartão SUS, parece perfeito. Por tipo, tem buraco.

Nenhum dos três precisa de GPU. Só de olhar o resultado com desconfiança.

Como fica o teste 2 em código?

Python puro, sem biblioteca nenhuma. A primeira função é o alarme. A segunda acha o trecho mesmo que a IA tenha mexido em ponto, traço ou espaço.

import re

def confere_trechos(original, trechos):
    """Teste 2: todo trecho devolvido pela IA existe igualzinho no original?"""
    return [t for t in trechos if t not in original]

def acha_no_original(original, trecho):
    """Acha o trecho mesmo que a IA tenha mudado ponto, traço ou espaço."""
    i = original.find(trecho)
    if i >= 0:
        return i, i + len(trecho)
    digitos = re.sub(r"\D", "", trecho)
    if len(digitos) < 8:
        return None
    padrao = r"(?<!\d)" + r"[.\-\s]?".join(digitos) + r"(?!\d)"
    m = re.search(padrao, original)
    return (m.start(), m.end()) if m else None

Rodando com uma nota inventada:

nota      = "Paciente Fulano de Tal, CPF 81013315650, CNS 898 0012 3456 7890, retorna em 7 dias."
devolvido = ["810.133.156-50", "898001234567890"]

faltando no original: ['810.133.156-50', '898001234567890']
810.133.156-50  -> '81013315650'
898001234567890 -> '898 0012 3456 7890'

Os dois trechos que a IA devolveu não existem na nota. A primeira função grita. A segunda acha onde eles estão de verdade, e aí sim dá pra mascarar.

Dois cuidados. O (?<!\d) e o (?!\d) impedem que o CPF case dentro de um número maior, tipo um protocolo. E isso só resolve número. Se a IA reescrever um nome (“Fulano” virou “FULANO”), o problema é outro, e o teste 2 continua sendo o seu alarme.

“Mas Rogério, não é só pedir no prompt pra copiar exatamente?” Ajuda. Não garante. Prompt é pedido. Teste é teste.

Onde o meu teste é fraco?

Vale tanto quanto o resto do artigo, então lê com atenção.

  • Nota de template é fácil. Sem erro de digitação, sem número quebrado em duas linhas, sem abreviação de plantão. Os 99,9% do Qwen são teto, não previsão.
  • Foram 200 notas, um gerador, uma quantização. Não testei o Phi-4 com outro prompt nem com outra quantização. Pode ser que ele melhore. Não sei ainda.
  • Prontuário de verdade é mais difícil. Um estudo publicado no JMIR em 2026 (Rahmeh e colegas, InCor/HCFMUSP) avaliou anonimização de notas clínicas em português com modelos de linguagem. O Llama-3.3-70B ficou em 0,726 de macro-F1 no corpus deles. Dado diferente, métrica diferente, não é comparação direta com a minha tabela. Mas dá a medida da distância entre template e o mundo real.
  • Tem o custo. O Qwen levou 12,4 segundos por nota. O Presidio, 0,01 segundo. Pra 100 mil notas, o Qwen seria umas 344 horas de máquina. Mais de duas semanas ligado.

Regra ou modelo: o que cada um ganha e perde?

Regra (Presidio com reconhecedores brasileiros):

  • A favor: rápido (0,01 s por nota), previsível, roda em qualquer máquina, 0 de 200 no CPF e no cartão SUS
  • Contra: só acha o que tem formato. Zerou endereço e perdeu metade das datas por extenso
  • O que dá trabalho: escrever e manter um reconhecedor por documento de cada país

Modelo de linguagem rodando local (Qwen3.8 27B, Phi-4 14B):

  • A favor: entende contexto, como nome no meio da frase. A promessa é pegar endereço escrito de qualquer jeito, mas isso eu ainda não testei (minhas notas são de template)
  • Contra: 12,4 s por nota no Qwen, e o Phi mostrou que acertar não garante mascarar
  • O que dá trabalho: máquina parruda, avaliação por tipo de dado e o teste 2 em toda resposta

E se o modelo rodar na nuvem, no Bedrock ou no Azure OpenAI? Não testei. Mas o problema do Phi está no que volta, não em onde roda. O teste 2 vale igual. E aí entra uma pergunta a mais: você pode mandar esse texto pra fora antes de anonimizar?

Então, o que eu recomendo?

Bala de prata não existe aqui. O que eu faria (o NursIA já faz a primeira parte: Presidio com reconhecedores brasileiros antes de qualquer modelo):

  • Documento com formato fixo (CPF, CNS, telefone) vai pra regra. Presidio com presidio-br. Rápido, determinístico, 0 de 200 no meu teste.
  • O modelo entra, em tese, no que a regra não pega: nome no meio da frase, endereço, contexto.
  • Tudo que o modelo devolver passa pelo teste 2 antes de virar máscara.
  • A avaliação é por tipo de dado. Média bonita esconde o tipo que zerou.

Quer rodar?

O código da demo 06 está no GitHub: nursia-research-lab, pasta demos/06-anonimizacao-pt. As notas são sintéticas por construção, então dá pra rodar sem medo.

Se você é dev: o que você usa hoje pra tirar dado pessoal antes de mandar texto pra um modelo, e já conferiu se o que volta existe no original?

Se você é de saúde ou de pesquisa: já testou anonimização em prontuário de verdade em português? Quanto deu?

Quero ler vocês nos comentários, aqui ou no LinkedIn.


Fontes

NursIA: pesquisa do PPGINFOS/UFSC, com bolsa FAPESC.


Publicado no blog em 5 de outubro de 2026. Versão original publicada como artigo no LinkedIn em 2 de outubro de 2026: A IA achou o CPF. E o CPF vazou mesmo assim. Changelog: 05/10/2026, versão do blog, com a tabela em HTML, os links do repositório da demo 06 e do presidio-br, e o resumo em inglês abaixo.

Summary in English. I ran four anonymizers on 200 synthetic Brazilian Portuguese clinical notes. Presidio default leaked a CPF or CNS in 200 of 200 notes; with Brazilian recognizers, 0. Qwen3.8 27B: 0. Phi-4 14B: 4, with 98.9% F1. In all four leaks the model had found the number but returned it reformatted (three CPFs came back with dots and a dash, like 81013315650 as 810.133.156-50; one CNS came back without spaces), so exact-string masking found nothing. If an LLM returns the spans you mask, check that each one exists verbatim in the source. Limits: template notes, n=200, 4-bit quantization. Code and data: nursia-research-lab, demos/06-anonimizacao-pt.


Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *