Input-Side PII Anonymization and Reversible Mapping
How to mask PII in user input before it reaches an external LLM, and restore the original values in the response.
Contents
Input-side defense masks PII before it goes to an external LLM, then restores only the values that need restoring in the answer that comes back.
Why the input is the first line of defense
The moment a request goes to an external LLM API, the text in the body crosses over to the vendor's infrastructure. That text may contain PII (Personally Identifiable Information), such as a member's name or phone number. If it does, PII arrives in plaintext somewhere outside your control.
Masking PII at the input is the earliest line of defense. Exposure removed here also reduces risk on everything downstream: RAG, tool calls and observability logs.
Input-side defense splits into detection and substitution. Detection finds which tokens are PII, and substitution replaces those tokens with fake values before sending them to the LLM.
Keeping the two stages separate matters. It is what lets you tune detection accuracy and substitution naturalness independently.
Microsoft Presidio
Microsoft Presidio is the common choice for detection. Internally it separates AnalyzerEngine (detection) from AnonymizerEngine (substitution).
The detectors combine Named Entity Recognition (NER) based on spaCy, Stanza or Transformers with regular expressions, checksum validation and custom recognizers.
The default entities for English include PERSON, EMAIL_ADDRESS, PHONE_NUMBER, CREDIT_CARD, IP_ADDRESS, LOCATION, US_SSN and DATE_TIME.
On English text it catches a substantial share right after installation. Korean requires separate reinforcement for names, addresses, resident registration numbers and account numbers.
Reinforcing Korean
Use a Korean NER model to find names, place names and organizations, and register Korea-specific identifiers separately with a PatternRecognizer.
- Reinforce PERSON, LOC and ORG with
klue/bert-base-nerorKPF-bert-ner - Cover resident registration numbers, account numbers and business registration numbers with a
PatternRecognizercombining a regular expression and a checksum - Inject a Korean spaCy model (
ko_core_news_lg) or a transformers backend intonlp_engine_provider
Presidio's accuracy depends on the Korean training distribution of the NER model. When the domain changes, as in healthcare, finance or HR, the false negative patterns change with it.
In production, collect the misses into a golden set and review it every quarter.
Reversible anonymization
Detection alone is not enough. If the substituted values remain in the LLM response, the user sees a fake name instead of their own.
LangChain's PresidioReversibleAnonymizer retains the substitution mapping. Once the response comes back, that mapping restores the values that need to be original again.
from langchain_experimental.data_anonymizer import PresidioReversibleAnonymizer
anonymizer = PresidioReversibleAnonymizer(
analyzed_fields=["PERSON", "PHONE_NUMBER", "EMAIL_ADDRESS", "KR_RRN"],
faker_seed=42,
add_default_faker_operators=True,
)
safe_prompt = anonymizer.anonymize("My name is Hong Gildong and my number is 010-1234-5678.")
answer = llm.invoke(safe_prompt)
final = anonymizer.deanonymize(answer)In the code above, anonymize finds the entities registered in analyzed_fields and replaces them with fake values. The substitution mapping is stored in deanonymizer_mapping.
When the LLM response returns, deanonymize consults that mapping and restores the fake values to the originals. KR_RRN is an identifier registered by hand for the Korean resident registration number.
One rule is absolute here. deanonymizer_mapping lives only in session-scoped or request-scoped memory, and is never persisted.
Writing the mapping to disk or a database turns that store into a shadow database. It holds plaintext PII paired one-to-one with the fake values.
In a LangGraph setup, keep the mapping only in PrivateState (in-memory). Only masked text flows into PublicState (which can be checkpointed).
Preempt-style routing
By default PresidioReversibleAnonymizer generates random fake values with Faker. That works well for filling in natural-looking names and addresses, but it breaks down when the same member appears several times.
If the value changes on every substitution, referential integrity is lost. In a query like "the top 5 transactions for this member", a member ID replaced with a different value on each call leaves the LLM unable to recognize the two as the same person.
The Preempt paper (arXiv:2504.05147) proposed a dual-strategy sanitizer that mixes two mechanisms according to the role of the token.
| Token kind | Mechanism | Reversal key |
|---|---|---|
| Card, account, phone, member ID | FPE (FF3-1) plus a Key Management Service (KMS) key | Held by the client |
| Numeric values such as age, amount, distance | mDP exponential mechanism | Irreversible (epsilon guarantee) |
| Name, address | Faker or a LOPSIDED pseudonym | Session memory |
Format-Preserving Encryption (FPE) is a deterministic cipher that keeps the format intact. The same input always maps to the same output, which suits tokens like card numbers where the format carries meaning.
Metric Differential Privacy (mDP) injects noise that assigns nearby values to a numeric field with higher probability. The exact value is hidden while the LLM can still use the fact that the magnitude is similar.
In LangGraph this can be implemented as one additional node that routes by token kind. Moving from plain Faker to deterministic FPE is the cheapest upgrade available.
SLM cascade
Presidio, regular expressions and NER are good at catching direct PII. What they can miss is the indirect identifier that only becomes identifying in combination.
"A department head in his fifties who works near Gangnam Station had lung surgery last year" contains no direct PII. Combine the company location, age, job title and illness, though, and it can single out one person.
To catch that residue, add a small language model (SLM) to the cascade. The candidates are as follows.
- SEAL (Self-Refining Anonymizer, arXiv:2506.01420) — an 8B SLM trained with adversarial distillation. It reports privacy-utility on par with a GPT-4 anonymizer and runs on a single GPU (L4/A10) under vLLM.
- LOPSIDED (arXiv:2510.27016) — semantically consistent pseudonym reassignment. It assigns pseudonyms while keeping job title, age and relationships coherent.
- Anonymizer-SLM 0.6B/1.7B/4B series — the lightweight option.
The cascade also lowers cost. Sending only what the first-stage regular expressions and NER let through to the SLM avoids running a large model on every input.
The full shape of the input-side cascade
Putting these tools together, the input-side pipeline takes the following shape.
Each stage works as a cascade. A later stage covers what an earlier one missed, and nothing depends on a single sanitizer.
Summary
Design the input side as a cascade with detection, substitution and reversible mapping kept separate. Run first-pass detection with Presidio and Korean recognizers, then split across Faker, FPE, mDP and LOPSIDED according to the nature of the token.
Keep the reversible mapping in session-scoped memory only and never persist it. Filter indirect identifiers once more through an SLM cascade, and where plain Faker is in use, adding deterministic FPE routing is the realistic upgrade.
The next post covers the second path: defense at the RAG, tool and MCP stages.