PII в промпте или ответе
Пользователь вставил в чат номер карты, модель процитировала чью-то почту в ответе, в лог утёк телефон — это персональные данные там, где их быть не должно, и в проде с реальными пользователями это прямой compliance-инцидент. Детектор должен ловить PII на входе и выходе, слать push с высоким приоритетом и — важно — не тащить сами данные в текст алёрта.
1. Быстрый regex-детектор
Заголовок раздела «1. Быстрый regex-детектор»Дешёвый первый слой поверх любого текста — почта, телефон, карта. В самом алёрте показываем только маску, никогда не полное значение:
import re, os, requests
PATTERNS = { "email": r"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}", "phone": r"(?:\+?\d[\d\s\-()]{9,}\d)", "card": r"\b(?:\d[ -]*?){13,16}\b",}RX = {k: re.compile(v) for k, v in PATTERNS.items()}
def mask(value: str) -> str: v = re.sub(r"\s|-", "", value) return v[:2] + "…" + v[-2:] if len(v) > 4 else "…"
def scan_pii(text: str, where: str): found = {} for kind, rx in RX.items(): if kind == "card": hits = [m.group(0) for m in rx.finditer(text or "") if luhn(m.group(0))] else: hits = [m.group(0) for m in rx.finditer(text or "")] if hits: found[kind] = hits
if found: summary = "; ".join(f"{k}×{len(v)} ({mask(v[0])})" for k, v in found.items()) notify(f"🔒 PII в {where}", f"Найдено: {summary}\n\n" "Замаскируйте перед записью в лог и проверьте источник.", priority=10) # громкий — это compliance return found
def luhn(number: str) -> bool: digits = [int(d) for d in re.sub(r"\D", "", number)] if not 13 <= len(digits) <= 16: return False chk = 0 for i, d in enumerate(reversed(digits)): if i % 2 == 1: d *= 2 if d > 9: d -= 9 chk += d return chk % 10 == 0
def notify(title, message, priority): requests.post(f"{os.environ['NOTIFLY_URL']}/message", params={"token": os.environ["NOTIFLY_TOKEN"]}, json={"title": title, "message": message, "priority": priority}, timeout=5)Проверка luhn() отсекает ложные срабатывания на длинных числах, которые не
являются картами.
2. Точнее — через Presidio
Заголовок раздела «2. Точнее — через Presidio»Regex ловит форматы, но не имена и адреса. Для серьёзной модерации возьмите Presidio: он даёт тип сущности и confidence, а результат можно так же замаскировать в алёрте:
from presidio_analyzer import AnalyzerEngine
analyzer = AnalyzerEngine()
def scan_pii_presidio(text: str, where: str, min_score=0.6): results = analyzer.analyze(text=text, language="en") strong = [r for r in results if r.score >= min_score] if strong: by_type = {} for r in strong: by_type.setdefault(r.entity_type, 0) by_type[r.entity_type] += 1 summary = ", ".join(f"{t}×{c}" for t, c in by_type.items()) notify(f"🔒 PII в {where}", f"Presidio нашёл: {summary}. Данные в push не вкладываются.", priority=10) return strong3. Куда встроить
Заголовок раздела «3. Куда встроить»- На входе — до отправки в LLM: если нашли PII, где его быть не должно, отклоняйте запрос или редактируйте перед отправкой.
- На выходе — до записи в лог и до показа пользователю: маскируйте найденное и алёртите.
- Помеченные ответы отправляйте в очередь модерации на ручной разбор.
Что положить в текст алёрта
Заголовок раздела «Что положить в текст алёрта»- типы и количество найденных сущностей, но не значения;
- маску (
52…19), а не полный номер; - где нашли: prompt, ответ, лог;
- id запроса, чтобы найти оригинал в защищённом хранилище.
Связанные рецепты
Заголовок раздела «Связанные рецепты»- Секреты в логах LLM — тот же приём для ключей и токенов.
- Очередь модерации — куда уходят помеченные ответы.
- Сработал safety / prompt injection — соседний слой безопасности.