jongkwan.dev
Development · Essay №082

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.

Jongkwan Lee2026년 5월 2일6 min read
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-ner or KPF-bert-ner
  • Cover resident registration numbers, account numbers and business registration numbers with a PatternRecognizer combining a regular expression and a checksum
  • Inject a Korean spaCy model (ko_core_news_lg) or a transformers backend into nlp_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.

python
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 kindMechanismReversal key
Card, account, phone, member IDFPE (FF3-1) plus a Key Management Service (KMS) keyHeld by the client
Numeric values such as age, amount, distancemDP exponential mechanismIrreversible (epsilon guarantee)
Name, addressFaker or a LOPSIDED pseudonymSession 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.