Cuando un sistema de salud o financiero llama a un modelo de lenguaje externo, cada prompt puede llevar nombre, CPF (el identificador fiscal de Brasil), diagnóstico o montos. Si eso sale de tu infraestructura sin tratamiento, el problema deja de ser técnico y se convierte en un riesgo jurídico y de confianza.
La idea de una capa de privacidad es simple: antes de la llamada, identificar las entidades sensibles, cambiar cada una por un marcador y guardar el mapa de vuelta dentro de tu perímetro. El modelo razona sobre los marcadores, y la respuesta se rehidrata solo de tu lado.
entrada: Maria Souza, CPF 123.456.789-09, internada el 12/03
modelo: [PERSONA_1], CPF [CPF_1], internada el [FECHA_1]Hasta aquí, todos de acuerdo. El problema es que la versión ingenua de esto protege mucho menos de lo que parece, y los agujeros están en cuatro lugares.
Un token estable es seudonimización, no anonimización
La tentación es hacer que el mismo CPF se convierta siempre en el mismo marcador, en todas las llamadas, para que el modelo “se acuerde” de la persona. Eso crea vinculabilidad: quien ve las peticiones puede juntar todo lo que pasó bajo el mismo marcador y armar un perfil sin ver nunca el CPF. Y el mapa de vuelta se convierte en el activo más sensible del sistema, porque es lo único que separa un montón de marcadores de un montón de pacientes.
Para la LGPD, la ley brasileña de protección de datos, igual que para el RGPD, un dato seudonimizado sigue siendo un dato personal. Solo deja de serlo cuando la anonimización no puede revertirse, y un mapa de vuelta existe justamente para revertirla. Así que el marcador no te saca del alcance de la ley; solo reduce lo que se filtra si interceptan la llamada.
Tampoco sirve cambiar el mapa por un hash. El CPF tiene unos 10⁹ valores posibles, y calcular el SHA-256 de todos cabe en una tarde en un portátil. Un hash sin clave de un identificador con un espacio pequeño es solo un CPF escrito de otra forma.
Lo mínimo que considero razonable:
- Alcance por conversación o por tenant, nunca global. Dentro de una conversación, bastan marcadores secuenciales (
CPF_1,CPF_2). - Cuando necesites consistencia entre llamadas, como en un documento procesado por partes, usa HMAC con una clave por alcance, y rota la clave.
- El mapa tratado como una bóveda: cifrado, con acceso restringido, retención corta y su propio registro de accesos.
token = "CPF_" + base32(HMAC(clave[tenant][conversacion], cpf))[:8]Mandar PII para detectar PII
Un consejo habitual es empezar por lo determinista, con regex y validadores para documentos, y dejar un LLM para los casos que la regla no alcanza. El consejo es correcto, con una condición que suele quedar fuera: ese modelo tiene que correr dentro del perímetro. Si el detector es una API externa, mandas el texto crudo afuera justamente para descubrir lo que no podías mandar afuera.
En la práctica, la detección queda toda de tu lado: regex y validadores (dígito verificador del CPF, número de la tarjeta de salud CNS, teléfono, e-mail, fechas), listas de entidades conocidas, un NER como el de Presidio con un modelo en portugués y, si hace falta, un modelo pequeño corriendo en local. El modelo externo solo ve texto que ya pasó por todo eso.
texto
-> regex + validadores (CPF, CNS, teléfono, e-mail, fechas)
-> NER / modelo local, dentro del perímetro
-> generalización de cuasi-identificadores
-> marcadores con alcance + mapa en la bóveda
-> LLM externo
-> validación de los marcadores en la respuesta
-> rehidrataciónEn salud, la filtración vive en los cuasi-identificadores
Nombre y CPF son la parte fácil. Lo que de verdad reidentifica a un paciente es la combinación de atributos que, por separado, parecen inofensivos. Latanya Sweeney mostró en 2000 que el código postal, la fecha de nacimiento y el sexo bastaban para identificar de forma única a cerca del 87% de la población de Estados Unidos. En una historia clínica en texto libre la situación es peor, porque esos atributos vienen mezclados con el resto.
Ninguna regex atrapa “paciente de 34 años con VIH atendido en Sorocaba”. No tiene nombre, no tiene documento, y en la base de una aseguradora de salud regional puede ser una sola persona. Aquí es donde vive el problema de verdad.
Sustituir no lo resuelve, porque el modelo necesita el contenido clínico: en un resumen de caso, el diagnóstico es el tema. La herramienta correcta es generalizar, decidiendo por tarea lo que el modelo realmente necesita saber:
| Atributo | Original | Generalizado |
|---|---|---|
| Edad | 34 años | 30 a 39 años |
| Fecha | internado el 12/03 | [FECHA_1], o “D+3” relativo al ingreso |
| Ciudad | Sorocaba | interior del estado de São Paulo |
| Diagnóstico | VIH | se mantiene, si la tarea lo necesita |
Y se puede medir. Si, después de generalizar, solo un paciente de la base coincide con “30 a 39 años, interior de São Paulo, VIH, internado en marzo”, la generalización no fue suficiente. Es la intuición del k-anonimato aplicada a lo que queda en el texto: cada combinación que llega al modelo debería corresponder a varias personas, no a una.
La rehidratación también falla
La vuelta es donde nadie mira. El modelo puede cambiar el formato del marcador ([CPF_1] se convierte en CPF 1, [cpf_1] o CPF_01), inventar uno que no existe ([PERSONA_3] cuando solo había dos personas) o cambiar los marcadores de lugar. Un find-and-replace ingenuo deja marcadores rotos en pantalla o, peor, pone el nombre del paciente equivocado en la respuesta.
- Marcadores con un formato rígido y difícil de parafrasear, y un reconocedor que encuentre las variaciones que el modelo suele producir.
- Todo marcador de la respuesta tiene que existir en el mapa de esa conversación. Un marcador desconocido significa rechazar o regenerar, nunca adivinar.
- Un marcador de otra conversación o de otro tenant nunca se resuelve, aunque exista en la bóveda.
- Después de rehidratar, no puede quedar ningún marcador en el texto.
for token in find_tokens(respuesta):
if token not in mapa_de_la_conversacion:
raise TokenDesconocido(token)Mide lo que se escapa
Un enmascaramiento que nadie prueba es solo una sensación de seguridad. Mantén un conjunto de casos sintéticos que se parezcan a los reales, incluidos cuasi-identificadores en texto libre, mide el recall del detector por tipo de entidad y cuenta lo que llega al modelo externo. La métrica que importa es lo que se filtró, no lo que se detectó.
Esto se complementa con El prompt no es una frontera de seguridad: una capa se ocupa de lo que el modelo ve, la otra de lo que puede hacer.