IA na saúde brasileira: como anonimizar prontuário eletrônico antes de mandar pra LLM (LGPD + CFM)
Hospital de SP usa pipeline de anonimização + GPT-4o-mini via Tokia pra resumir evolução médica. Código real de regex + NER, custo por consulta e o que CFM Resolução 2.227/2018 exige.
Caso real (sob NDA): clínica multi-especialidade SP, 22 médicos, ~4.000 consultas/mês. Antes: médico digitava evolução de 8-15min depois de cada consulta (tempo "perdido" entre paciente sair e próximo entrar). Depois: gravação áudio → Whisper → anonimização → GPT-4o-mini resume em SOAP estruturado. Médico ganhou 12min/consulta de volta. 4.000 consultas × 12min = 800h/mês de produtividade médica recuperada.
Esse post mostra como fazer certo — porque o erro mais comum (mandar prontuário cru pra OpenAI) viola LGPD, CFM e expõe a clínica a R$ 50M de multa potencial.
O problema regulatório (que 90% dos devs ignoram)
3 normas BR que se aplicam antes de qualquer prompt:
- LGPD Art. 11: dados de saúde são sensíveis. Tratamento exige base legal específica (consentimento ou tutela da saúde).
- CFM Resolução 2.227/2018: prontuário eletrônico precisa garantir confidencialidade, integridade e disponibilidade. Mandar pra cloud gringa sem cuidado é falta ética do médico responsável.
- Marco Civil + LGPD Art. 33: transferência internacional de dados pessoais exige garantias contratuais. OpenAI/Anthropic têm DPA padrão mas leitor de DPO precisa avaliar.
Conclusão: você precisa garantir que o prompt que chega na LLM não contém PII identificável. Anonimização é pré-processamento obrigatório, não opcional.
A arquitetura segura
Áudio consulta → Whisper (Tokia)
↓
Texto cru com PII (paciente, médico, CPF, telefone)
↓
Pipeline anonimização (regex + NER local)
↓
Texto com PLACEHOLDERS ([PACIENTE_001], [CPF])
↓
GPT-4o-mini (Tokia) → SOAP estruturado
↓
Reverse-mapping placeholders → texto final pro prontuário
Princípio: LLM nunca vê dado real. Só placeholders + contexto clínico.
O pipeline de anonimização (Python + spaCy)
import re
import spacy
from typing import Dict, Tuple
# spaCy modelo PT-BR pra Named Entity Recognition local (não vai pra cloud)
nlp = spacy.load("pt_core_news_lg")
PATTERNS = {
"CPF": re.compile(r"\b\d{3}\.?\d{3}\.?\d{3}-?\d{2}\b"),
"CNS": re.compile(r"\b\d{15}\b"), # cartão SUS
"TELEFONE": re.compile(r"\b(?:\+?55\s?)?(?:\(?\d{2}\)?\s?)?\d{4,5}-?\d{4}\b"),
"EMAIL": re.compile(r"\b[\w.-]+@[\w.-]+\.\w+\b"),
"DATA_NASC": re.compile(r"\b\d{2}/\d{2}/(?:19|20)\d{2}\b"),
"CEP": re.compile(r"\b\d{5}-?\d{3}\b"),
}
def anonymize(text: str) -> Tuple[str, Dict[str, str]]:
"""
Retorna (texto_anonimizado, mapping_pra_reverter).
Mapping NUNCA vai pra LLM — fica em memória local pra reconstruir
o output depois.
"""
mapping: Dict[str, str] = {}
counter = {"PACIENTE": 0, "MEDICO": 0, "LUGAR": 0}
# 1. Regex pra padrões estruturados
for label, pat in PATTERNS.items():
for match in pat.finditer(text):
original = match.group()
placeholder = f"[{label}]"
mapping[placeholder] = original
text = text.replace(original, placeholder)
# 2. NER local pra nomes + lugares
doc = nlp(text)
for ent in doc.ents:
if ent.label_ == "PER": # pessoa
counter["PACIENTE"] += 1
placeholder = f"[PACIENTE_{counter['PACIENTE']:03d}]"
mapping[placeholder] = ent.text
text = text.replace(ent.text, placeholder)
elif ent.label_ == "LOC": # local
counter["LUGAR"] += 1
placeholder = f"[LUGAR_{counter['LUGAR']:03d}]"
mapping[placeholder] = ent.text
text = text.replace(ent.text, placeholder)
return text, mapping
def restore(text: str, mapping: Dict[str, str]) -> str:
"""Reverte placeholders → originais após a LLM processar."""
for placeholder, original in mapping.items():
text = text.replace(placeholder, original)
return text
O prompt SOAP (Subjective, Objective, Assessment, Plan)
SYSTEM = """Você é assistente de transcrição médica para clínica brasileira.
Dado a transcrição da consulta (texto bruto), estruture em formato SOAP:
S (Subjetivo): queixa principal e histórico contado pelo paciente
O (Objetivo): exame físico, sinais vitais, achados
A (Avaliação): hipóteses diagnósticas, raciocínio clínico
P (Plano): conduta, prescrições, retornos, exames
REGRAS:
- Use APENAS informação presente na transcrição. Não invente sintomas.
- Mantenha PLACEHOLDERS literais (ex: [PACIENTE_001], [CPF]).
- Linguagem clínica formal PT-BR.
- Retorne JSON: {"subjetivo": "", "objetivo": "", "avaliacao": "", "plano": ""}
"""
def gerar_soap(transcricao_anonimizada: str) -> dict:
completion = client.chat.completions.create(
model="gpt-4o-mini",
response_format={"type": "json_object"},
temperature=0.2,
messages=[
{"role": "system", "content": SYSTEM},
{"role": "user", "content": transcricao_anonimizada},
],
)
return json.loads(completion.choices[0].message.content)
Custo real por consulta
| Item | Valor | Detalhe | |---|---|---| | Whisper áudio 15min | $0.09 | $0.006/min × 15 | | Anonimização local | $0.00 | spaCy roda no servidor da clínica | | GPT-4o-mini SOAP | $0.003 | ~2k input + 800 output tokens | | Total USD | $0.093 | | | Total BRL | R$ 0,79 | fx 5.10 + markup Tokia 1.8x | | 4.000 consultas/mês | R$ 3.160 | |
Comparação: 1 escriba médico CLT custa R$ 5.500/mês com encargos. Bot Tokia economiza R$ 2.340/mês e opera 24/7.
Por que GPT-4o-mini e não DeepSeek?
DeepSeek é mais barato em LLM puro, mas:
- Terminologia médica PT-BR: GPT-4o-mini foi treinado com mais texto médico BR (artigos PubMed traduzidos). DeepSeek erra termos como "azitromicina" vs "azitromicina-hidrato".
- Compliance HIPAA: OpenAI tem DPA padrão. DeepSeek é empresa chinesa — DPO conservador rejeita.
- Latência consistente: GPT-4o-mini responde em menos de 2s p95. DeepSeek varia 1-8s dependendo da hora (horário pico China).
Para casos não-clínicos (resumo de exame laboratorial sem PII por exemplo), DeepSeek serve. Use Tokia pra orquestrar ambos.
Os 5 erros que vimos em outras clínicas
1. Anonimizar só com regex, sem NER
Regex pega CPF/CNS/email, mas perde "o Sr. João da Silva veio com queixa…".
NER local resolve. Use spaCy pt_core_news_lg (gratuito) ou Stanza.
2. Logging do prompt cru
Quem usa print(prompt) ou Sentry com payload completo pra debug vira o
problema. Antes de mandar pra LLM, logue só hash + length. Tokia
não loga conteúdo por padrão.
3. Não validar o reverse-mapping
Se LLM gerar texto NOVO que coincide com um placeholder ("paciente_002"
em texto natural), o restore vai errar. Mitigação: use placeholders bem
distintos ([[PACIENTE_001]] com duplo bracket).
4. Mandar áudio direto pra Whisper sem consent
CFM exige autorização explícita do paciente pra gravação. Modelo de termo: "Autorizo a gravação desta consulta exclusivamente para fins de elaboração do prontuário eletrônico, ciente que o áudio será processado por sistema de IA e descartado em até 24h." — papel ou checkbox no sistema antes de iniciar.
5. Não ter human-in-the-loop
Médico precisa revisar e assinar o SOAP antes de virar prontuário oficial. UI deve mostrar diff visual com áudio reproduzível, não "aceitar tudo".
Checklist compliance antes de subir em produção
- [ ] DPO da clínica revisou pipeline e aprovou
- [ ] Termo de consentimento atualizado (paciente assina antes da gravação)
- [ ] Áudio descartado em menos de 24h (não armazenar em S3 indefinidamente)
- [ ] Anonimização testada com 100 prontuários reais (manual review)
- [ ] Médico assina cada SOAP (audit trail em prontuário)
- [ ] Backup do mapping placeholders → original em servidor LOCAL da clínica
- [ ] Plano de contingência se Tokia/OpenAI cair (downgrade pra escriba humano)
Quando NÃO usar isso
- Psiquiatria/psicologia: sigilo aumentado (Resolução CFP 11/2018). Risco de exposição inaceitável mesmo com anonimização.
- Pediatria com menores: precisa autorização dos pais por escrito, ECA Art. 17. Mais barato fazer manual.
- Casos forenses/perícia: prontuário pode virar prova judicial. Qualquer transformação automatizada vira questionamento.
Próximo passo
Cria sua key gratuita no /dashboard, testa o prompt no /playground com transcrição fake e mede latência + qualidade do JSON. Se viável, próximo passo é discutir DPA específico com DPO da clínica — contato@usetokia.com.
Links
Quer testar Tokia com R$ 10 via PIX?
Criar conta grátis →