Personenbezogene Daten pseudonymisieren, bevor sie an das LLM gehen. Originale in der Antwort wiederherstellen. Eine schlanke Middleware zwischen App und Anthropic / OpenAI / Gemini / Bedrock.
Entwickelt für deutsche B2B-Anwendungsfälle, bei denen Kundennamen, Telefonnummern und E-Mails die EU nicht verlassen sollen — auch wenn der LLM-Anbieter es tut.
v0.1.0. Aus produktivem Einsatz unter netzhandwerker.de extrahiert. Für regulierte Produktion (Medizin, Recht, Behörden) ohne eigene Prüfung nicht empfohlen.
- Sechs Kategorien werden standardmäßig erkannt: Personennamen, E-Mails, Telefonnummern, Postadressen, IBAN, Kreditkartennummern
- Bidirektionale Pipeline:
redact()ersetzt durch Token,restore()setzt Originale zurück - Deutsche und internationale Telefon-Formate
- IBAN mit ISO-13616-Checksum-Prüfung
- Kreditkarten mit Luhn-Validierung
- Optional spaCy-NER für höhere Namens-Genauigkeit
- Konfigurierbare Ausschluss-Wörter gegen False Positives
- Namens-Erkennung per Heuristik ist nicht perfekt — False Positives bei „Hello World", Ländernamen, Produktnamen
- Geburtsdaten werden nicht erkannt (zu nah an normalen Daten — bräuchte Kontext-Analyse)
- Gesundheitsdaten (Diagnosen, Medikamente) werden nicht erkannt — Schicht ist keine medizinische Pseudonymisierung
- KFZ-Kennzeichen-Pattern existiert, aber selten nützlich
- Freitext-Adressen ohne PLZ werden nicht erkannt
- Pseudonymisierung ist reversibel (Mapping vorhanden) — nicht Anonymisierung im DSGVO-Sinn
┌────────────────┐ ┌──────────────────┐ ┌─────────────┐
│ Eingabe │──▶│ redact() │──▶│ LLM │
│ "Maria Schmidt │ │ "[PERSON_1] aus │ │ (beliebig) │
│ aus München" │ │ [ADDRESS_1]" │ │ │
└────────────────┘ └──────────────────┘ └──────┬──────┘
│
▼
┌────────────────┐ ┌──────────────────┐ ┌─────────────┐
│ Endausgabe │◀──│ restore() │◀──│ LLM-Antwort│
│ "Hallo Maria, │ │ Token-Lookup, │ │ "Hallo │
│ ich prüfe das │ │ Originale │ │ [PERSON_1]" │
│ in München..."│ │ zurücksetzen │ │ │
└────────────────┘ └──────────────────┘ └─────────────┘
Sechs Kategorien werden standardmäßig erkannt:
| Kategorie | Token-Format | Detektor |
|---|---|---|
| Personennamen | [PERSON_N] |
Großschreibungs-Pattern für deutsche und englische Namen, optional spaCy NER |
| E-Mail-Adressen | [EMAIL_N] |
RFC-5322-orientierte Regex |
| Telefonnummern | [PHONE_N] |
Deutsche und internationale Formate |
| Postadressen | [ADDRESS_N] |
Deutsche und EU-Postleitzahl-Patterns |
| IBAN | [IBAN_N] |
ISO 13616 mit Checksum-Validierung |
| Kreditkarte | [CARD_N] |
Luhn-validiert |
Drei reale Szenarien, die diese Schicht motiviert haben:
- DSGVO + LLM-Anbieter in den USA. Eine Support-Anfrage enthält den Namen einer Person aus Deutschland. Direkt an OpenAI geschickt ist das eine Übermittlung personenbezogener Daten in ein Drittland. Pseudonymisierung davor macht den LLM-Aufruf zu Nicht-personenbezogenen-Daten — DSGVO Art. 4 Abs. 5.
- Telefon-Agent-Transkripte. Das Transkript eines Voice Bots enthält Anrufernummer und Name. Rohe Transkripte an ein Summarization-LLM zu schicken erzeugt unnötige Datenoffenlegung, wenn der Speicher nicht strikt lokal bleibt.
- Chatbots mit Kundenkontakt. Wenn ein Nutzer seine Bestelldaten einfügt („Mein Name ist X, Bestellung #Y"), soll der Bot mit dem Original-X antworten — nicht mit dem Token.
Diese Middleware löst alle drei mit einem Wrapper.
pip install pii-redaction-layerfrom pii_redaction import Redactor
redactor = Redactor(locales=["de_DE", "en_US"])
# 1. Vor dem LLM-Aufruf pseudonymisieren
redacted, mapping = redactor.redact(
"Maria Schmidt aus 80331 München hat unter +49 89 1234567 angerufen. "
"E-Mail maria.schmidt@example.de"
)
print(redacted)
# "[PERSON_1] aus [ADDRESS_1] hat unter [PHONE_1] angerufen. E-Mail [EMAIL_1]"
# 2. `redacted` an dein LLM senden
llm_reply = call_llm(redacted)
# "Verstanden, ich rufe [PERSON_1] unter [PHONE_1] zurück."
# 3. Token in der Antwort wiederherstellen
final = redactor.restore(llm_reply, mapping)
print(final)
# "Verstanden, ich rufe Maria Schmidt unter +49 89 1234567 zurück."from multi_llm_router import Router
from pii_redaction import Redactor
router = Router()
redactor = Redactor()
async def chat_safely(user_message: str) -> str:
redacted, mapping = redactor.redact(user_message)
response = await router.chat([{"role": "user", "content": redacted}])
return redactor.restore(response.text, mapping)Standard-Detektor nutzt Heuristiken für deutsche B2B:
- Großgeschriebene Wörter nach „Herr", „Frau", „Hr.", „Fr.", „Mr.", „Ms.", „Dr."
- Zwei großgeschriebene Wörter zusammen (wahrscheinlich Vor- und Nachname)
- Anrede-Pattern am Anfang einer E-Mail
Für höhere Genauigkeit spaCy mit deutschem Modell installieren:
pip install pii-redaction-layer[spacy]
python -m spacy download de_core_news_smNamen werden dann über NER erkannt statt per Heuristik.
Deutsche Formate:
+49 89 1234567,+49 (0)89 1234567030 / 12345678,030 123456780151 12345678(Mobil)
Internationale Formate: +1 555 123 4567, +44 20 7946 0958 usw.
Standard-Regex mit Checksum-Validierung, wo anwendbar.
Deutsch: 12345 Stadt und Straße N, 12345 Stadt.
EU: vergleichbare Patterns für AT, CH, NL, FR, IT (Basis).
Heuristische Namens-Erkennung erwischt manchmal:
- „Hello World" (zwei großgeschriebene Wörter)
- „Germany France" (Ländernamen)
- „iPhone Pro Max" (großgeschriebene Produktnamen)
Gegenmaßnahmen:
exclude_words=["iPhone", "Pro", "Max"]min_name_word_length=4, um kurze Wörter zu überspringen- spaCy NER für Produktion verwenden (deutlich genauer)
- Kein Ersatz für ordentliche Daten-Klassifizierung. Sensible Kontexte (Medizin, Recht) brauchen Experten-Prüfung.
- Keine DSGVO-Konformität für sich allein. Pseudonymisierung ist ein Werkzeug — Rechtsgrundlage, Aufbewahrungsfrist und Verträge sind weiterhin nötig.
- Keine Anonymisierung. Das Mapping ist reversibel. Erst wenn das Mapping nach der Antwort gelöscht wird, ist der an das LLM gesendete Payload anonymisiert. Mit Mapping ist es weiterhin personenbezogen nach DSGVO Art. 4 Abs. 5.
pii_redaction/
├── __init__.py — exportiert Redactor
├── detectors/
│ ├── names.py — Heuristik + optional spaCy NER
│ ├── email.py — RFC 5322-orientiert
│ ├── phone.py — international + deutsche Formate
│ ├── address.py — Postleitzahl-Pattern je Locale
│ ├── iban.py — ISO 13616 mit Mod-97-Prüfung
│ └── card.py — Luhn-validiert
├── redactor.py — zentrale Redactor-Klasse
└── locales.py — locale-spezifische Regex-Bundles
- v0.2: Provider-spezifische Exception-Klassen statt String-Heuristik bei der Detektion, mehr deutsche False-Positive-Filter
- v0.3: Geburtsdaten-Erkennung mit Kontext-Analyse, KFZ-Kennzeichen-Toggle
- v0.4: Benchmark-Corpus mit deutschen Texten + Recall/Precision-Messung pro Detektor
MIT.
Gebaut von Daniel Wesseling bei Die Netzhandwerker. Teil der Architektur hinter Robert, einem KI-Sprachbegleiter mit Datenschutz-Fokus (in Entwicklung).