PII en agentes de IA: detectar y redactar datos sensibles en entrada y salida
Resumen
Cómo evitar que tu agente de IA filtre datos personales: detecta emails, teléfonos, tarjetas e IBAN con regex y validación Luhn, redacta con máscaras o bóveda de tokens con TTL, y aplica el filtro en ambas direcciones —antes del LLM y antes de logs y memoria. Incluye patrones listos, código TypeScript mínimo, tabla de decisiones, checklist y FAQ.

Qué resuelve
Esta pieza se queda en la decisión práctica: qué instalar, qué riesgo agrega y cómo aplicarlo sin romper operación.
La guía de cómo evitar fugas de datos dice qué acotar: permisos, memoria, herramientas. Esta guía dice cómo limpiar el texto que fluye entre esas piezas. Porque el agente no filtra datos por malicia: los ve por diseño. Lee correos para resumirlos, recibe DPI y tarjetas en formularios, devuelve historiales en sus respuestas y escribe todo eso en logs y memoria. Cada uno de esos puntos necesita el mismo tratamiento: detectar identificadores personales y redactarlos antes de que lleguen a donde no deben.
Dónde vive el PII en un agente
El PII (información de identificación personal) no entra por un solo lugar. Mapea las cinco superficies y decide el filtro para cada una:
| Superficie | Ejemplo real | Riesgo si no filtras |
|---|---|---|
| Entrada del usuario | "Mi DPI es 1234 56789 0101, actualiza mi ficha" | El número viaja al vendor del LLM y queda en sus logs |
| Salida de herramientas | El CRM devuelve la ficha completa con teléfono y email | El modelo los repite en su respuesta al usuario |
| Memoria / RAG | Fragmentos con datos de clientes indexados sin limpiar | Otro usuario los recupera con una pregunta inocente |
| Logs y trazas | Prompt completo guardado para debug | Quien lee logs lee los datos de tus usuarios |
| Mensajes de error | "Falló el cargo a la tarjeta 4111…" visible en el chat | Fuga directa en la superficie más visible |
Regla de una línea: ningún texto con PII llega al LLM, a un log, a memoria ni a otro usuario sin pasar por el filtro. El filtro corre en dos direcciones: a la entrada (lo que el usuario y las herramientas aportan) y a la salida (lo que el modelo genera y lo que se persiste).
Capa 1: patrones regex + Luhn
El 90 % del PII estructurado cae con media docena de patrones. Esta es la tabla base; ajústala a tu país (el ejemplo de teléfono usa el formato de Guatemala, +502):
| Tipo | Patrón | Nota |
|---|---|---|
[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,} | Case-insensitive; valida el TLD con lista corta si quieres precisión | |
| Teléfono GT | \+502\s?\d{4}-?\d{4} | 8 dígitos; acepta con o sin guion ni espacio |
| Teléfono genérico | \+?\d[\d\s.-]{7,}\d | Ruidoso a propósito: úsalo solo como red amplia con revisión |
| Tarjeta | (?:\d[ -]?){13,19} + Luhn | Sin Luhn genera falsos positivos con cualquier número largo |
| IBAN | [A-Z]{2}\d{2}[A-Z0-9]{11,30} | Incluye cuentas GT (GT82…) y europeas |
| API keys / tokens | (sk-[A-Za-z0-9]{8,}|ghp_[A-Za-z0-9]{8,}|xox[bap]-[A-Za-z0-9-]+) | Secretos de OpenAI, GitHub y Slack más comunes |
El truco que separa un filtro serio de un match ingenuo es Luhn para tarjetas: un número de 16 dígitos solo se redacta si pasa la suma de verificación. Así no censuras IDs de pedido ni timestamps:
function luhnOk(digits: string): boolean {
const d = digits.replace(/\D/g, "");
let sum = 0, dbl = false;
for (let i = d.length - 1; i >= 0; i--) {
let n = d.charCodeAt(i) - 48;
if (dbl) { n *= 2; if (n > 9) n -= 9; }
sum += n; dbl = !dbl;
}
return d.length >= 13 && sum % 10 === 0;
}
Corre cada candidato de (?:\d[ -]?){13,19} por luhnOk y redacta solo los que pasen. Precisión alta, cero dependencias.

Capa 2: NER cuando el regex no alcanza
Nombres de personas, direcciones y empresas no tienen formato fijo: ahí entra el reconocimiento de entidades (NER). Presidio es el estándar open source: detectores por entidad con umbral de confianza configurable. La receta práctica:
- Entidades deterministas (email, tarjeta, IBAN) con umbral alto (0.9+): redacta directo.
- Entidades difusas (PERSON, LOCATION, PHONE_NUMBER) con umbral medio (0.6–0.8): redacta en entradas, marca para revisión en salidas.
- Todo lo que caiga bajo el umbral pero matchee un regex de capa 1: redacta igual. Las capas se suman, no se votan.
No necesitas self-hostearlo el día uno: empieza con la capa 1 en código (30 líneas) y añade NER cuando tus logs muestren PII no estructurado escapando. El OWASP Top 10 para LLM clasifica la divulgación de información sensible como riesgo principal: el orden de implementación es regex hoy, NER esta semana, bóveda este mes.
Redactar: máscara o bóveda con TTL
Detectar es la mitad; decidir con qué reemplazar es la otra. Dos estrategias, una tabla:
Máscara ([EMAIL_1], +502 ••••1234) | Bóveda de tokens con TTL | |
|---|---|---|
| Cuándo | El flujo no necesita el valor real después | El flujo sí lo necesita (cobrar, llamar, enviar) |
| Reversible | No | Sí, solo con permiso y dentro del TTL |
| Coste | Cero estado | Un almacén con expiración |
| Riesgo típico | Romper formato que el tool esperaba | Olvidar el TTL y crear una base de PII eterna |
La bóveda mínima cabe en pocas líneas: guarda el valor real fuera del contexto del modelo, entrega un token opaco al agente y rehidrata solo en el adaptador de la herramienta que de verdad lo necesita:
// ponytail: Map en memoria, vale para un proceso; usa Redis/Dynamo con TTL nativo si escalas a varios workers
const vault = new Map<string, { value: string; exp: number }>();
const TTL_MS = 15 * 60 * 1000; // 15 minutos, suficiente para completar el flujo
function seal(value: string): string {
const token = "pii_" + Math.random().toString(36).slice(2, 10);
vault.set(token, { value, exp: Date.now() + TTL_MS });
return token;
}
function unseal(token: string): string | null {
const e = vault.get(token);
if (!e || e.exp < Date.now()) { vault.delete(token); return null; }
return e.value;
}
Tres reglas no negociables de la bóveda: TTL corto y por defecto (minutos, no días), unseal solo dentro del adaptador de la herramienta autorizada (nunca en el prompt, nunca en un log), y barrido periódico de expirados aunque el Map los ignore.

Las dos direcciones, sin excepción
El error clásico es filtrar solo la entrada del usuario. El pipeline completo tiene cuatro compuertas:
- Pre-LLM: redacta el prompt del usuario + el contexto recuperado de memoria/RAG. El modelo nunca ve PII crudo salvo que el flujo lo exija (y entonces viaja como token de bóveda).
- Post-tool: redacta la salida de cada herramienta antes de inyectarla al contexto. El CRM devuelve la ficha completa; el agente solo necesita los campos de la tarea.
- Pre-log: lo que se escribe a disco (trazas, evals, debug) pasa por el mismo redactor. Un log con el prompt completo es una base de PII sin control de acceso.
- Pre-memoria: lo que se persiste para futuras sesiones se guarda ya redactado o como token con TTL. La memoria es el lugar donde una fuga se vuelve permanente y consultable por otros usuarios.
Si implementas una sola compuerta hoy, que sea la 3: los logs son la fuga que nadie mira hasta la auditoría.
Lo que nunca va con PII
- PII crudo en el system prompt (se replica en cada llamada y en cada traza).
- PII en nombres de archivo, IDs de traza o subjects de email generados por el agente.
- Datasets de evaluación con datos reales sin anonimizar: anonimiza los casos dorados o tus evals se convierten en el leak.
- Enviar PII a un vendor sin acuerdo de tratamiento de datos: revisa retención y opt-out de entrenamiento en la documentación de herramientas de tu proveedor antes de conectar el flujo.
- Confiar en que "el modelo no lo repetirá": los guardrails acotan comportamiento, no sustituyen redacción determinista.
Checklist
- Regex de email, teléfono, tarjeta (+Luhn), IBAN y API keys corriendo en pre-LLM
- Salidas de herramientas redactadas antes de entrar al contexto
- Logs y trazas con redacción aplicada (verifica con una búsqueda de
@en tus logs de hoy) - Memoria persiste solo texto redactado o tokens con TTL corto
- Bóveda con TTL,
unsealrestringido al adaptador autorizado y barrido de expirados - Casos de eval anonimizados
- NER (Presidio o equivalente) en roadmap cuando aparezca PII no estructurado en revisiones
FAQ
¿No basta con pedirle al modelo que no muestre datos personales? No. Es control probabilístico sobre un riesgo determinista. La instrucción ayuda como segunda capa, pero la redacción con regex y Luhn es la que garantiza el resultado. Capas, no promesas.
¿La redacción rompe la funcionalidad del agente? Solo si redactas a ciegas. Por eso existen dos estrategias: máscara donde el valor no se necesita, bóveda con TTL donde sí. El adaptador de la herramienta rehidrata; el modelo opera con tokens.
¿Esto es lo mismo que la guía de fugas de datos? Complementario. Evitar fugas cubre permisos, allowlists y sandboxes (el qué). Esta guía cubre detección y redacción del texto (el cómo). Implementa ambas; ninguna sustituye a la otra.
¿Qué hago con secretos y API keys que ya están en el repo o en variables? Eso vive en la guía de secretos para agentes: bóveda del proveedor, rotación y cero secretos en el contexto. El detector de esta guía es tu red de seguridad para cuando un secreto aparezca donde no debe, no tu almacén primario.
El siguiente paso natural después de limpiar el texto es ordenar quién puede hacer qué con él: permisos por herramienta y human-in-the-loop para lo irreversible viven en el hub de seguridad, coste y operación. Y si estás montando tu primer agente, el curso de instalación te deja el esqueleto listo para enchufarle este filtro desde el día uno.
Lecturas relacionadas
Sigue explorando Seguridad de Agentes y otras piezas para builders.

Prompt injection en agentes: defensas que sí cortan el daño

Seguridad en MCP: permisos, autenticación y tool poisoning

Cómo evitar fugas de datos en agentes de IA: permisos, memoria y herramientas
