Anonymizing data before sending it to a neural network: a practical way

You do not have to send personal data to a model for it to process a document. How to replace it with labels, restore it in the answer and check that nothing leaked.

6 min · · автоматизация · бизнес

For a model to write a reply to a customer or summarize a contract, it does not need to know the customer's name and phone number. It needs the meaning of the document. So a sensible scheme for a neural network in document processing is this: first replace personal data with labels, then send the text, and in the received answer put the real values back. Below: what exactly to look for, how replacement and reverse substitution work, what code is needed, where the method breaks and how to verify the result before going live.

Anonymizing data before sending it to a neural network: why

Everything you pass in a request goes to the model provider's side. If a text contains names, passports, phones, emails or card numbers, that is personal data, and the rules for handling it are on you. Anonymization handles the practical part: it reduces the volume of sensitive material leaving your perimeter and limits the damage of any leak (logs, an access mistake, correspondence).

It is important to see the limits of the method soberly. Replacing direct identifiers with labels does not make data anonymous by definition: a person can sometimes be recognized from context, for example by a rare job title, unique circumstances or a combination of details. Legal questions (what counts as anonymization, on what basis data may be passed, where it may be stored) are not ours to settle; discuss them with the company lawyer. This article is about the technical side.

What data to look for

What to look forHow to find itReliability
Emailregular expressionhigh
Phoneregular expression covering +7 and 8high, but check your own formats
Bank card numbera digit chain plus a Luhn checkhigh
SNILSthe pattern 123-456-789 01high
INNdigits next to the word "ИНН"medium: without context 10–12 digits are easily confused with other numbers
Passport series and numbera pattern of 4 and 6 digitsmedium: false positives are possible
Names, organizations, addressesa dictionary from your CRM or named entity recognitiondepends on the data and the language
Contract and order numbersyour numbering patternhigh if the pattern is known

For names and organizations in Russian text there are named entity recognition libraries (for example Natasha), but check their quality on your own documents. The most reliable source of names is your own reference lists: customers, employees, counterparties. They are known in advance, and looking them up is easier than guessing.

The scheme: replace, send, restore

  1. Replace. Each found value gets a label like [PHONE_1], [EMAIL_1], [PERSON_1]. Identical values get identical labels so the model understands that "Ivanov" at the start and at the end of the document is one person.
  2. Send. Only the text with labels goes into the request. In the system prompt say: "do not change the labels in square brackets, do not try to reveal them and keep them in the answer".
  3. Restore. In the received answer the labels are replaced with the real values. The mapping table is kept only on your side and never goes to the model.

Python code

bash
import re
from collections import defaultdict


def luhn_ok(digits: str) -> bool:
    total = 0
    for i, ch in enumerate(reversed(digits)):
        d = int(ch)
        if i % 2 == 1:
            d *= 2
            if d > 9:
                d -= 9
        total += d
    return total % 10 == 0

How it works on an example:

bash
m = Masker()
m.add_known("PERSON", ["Иванов Пётр Сергеевич"])
src = ("Клиент Иванов Пётр Сергеевич (ivanov@example.com, +7 916 123-45-67) "
       "просит расторгнуть договор. Позвоните Иванову Петру Сергеевичу: +7 916 123-45-67.")
print(m.mask(src))

The result:

bash
Клиент [PERSON_1] ([EMAIL_1], [PHONE_1]) просит расторгнуть договор. Позвоните Иванову Петру Сергеевичу: [PHONE_1].

The phone and email are replaced, and the repeated phone got the same label. But "Иванову Петру Сергеевичу" (the dative form) remained: the dictionary knew only the nominative case. This is the main weakness of the method for Russian, and the next section covers it.

Sending the request and restoring values:

bash
import os

from openai import OpenAI

client = OpenAI(
    base_url="https://api.zukko.pro/v1",
    api_key=os.environ["ZUKKO_API_KEY"],
)

SYSTEM = (
    "You are a lawyer's assistant. Summarize the document briefly. Labels in square brackets "
    "such as [PERSON_1] or [PHONE_1] must not be changed or revealed and must be kept in the answer as is."
)

Where the method breaks

  • Cases and name forms. "Иванов", "Иванову", "Ивановым" are different strings. Add the main forms or word stems to the dictionary; a stem catches more but can hit words with the same root.
  • Context that gives a person away. "The director of the only plant in town" stays in the text. There are no direct identifiers, yet the person is visible. Look through typical documents by eye and add rules.
  • Data in images and scans. If you send an image of a document to a model, the text on it cannot be masked with regular expressions. First recognize the text, mask it, then send. This scheme is covered in the article how to recognize a PDF with a neural network.
  • Labels disappear or change. A model may rephrase "[PERSON_1]" as "the client" or distort the label number. The leftovers function catches extra labels, and lost ones show up when you compare the number of labels before and after.
  • The mapping table. It is the most sensitive thing you have: do not write it to logs and do not send it to a model. More on logs: LLM logging without leaking data.

How to verify

  1. A test set. Collect 30–50 typical documents, run them through mask and look at the result. Look especially for missed names and addresses.
  2. A second search on the masked output. Run the anonymized text through the same patterns again: nothing should remain.
  3. Canaries. Put obviously fictional data (an email, a phone, a name) into a test document and make sure it is replaced. Run this test after every rule change.
  4. Manual spot checks of what actually goes into the request, on the first few hundred documents.
  5. Metrics. Count replacements by type and watch for anomalies: a sharp drop in the number of phones in the stream means the format changed and the regular expression does not see it.

For extra insurance create a separate key for this task with a daily limit and a list of allowed models (more on that).

What it costs

Masking runs locally on your side and costs nothing; the labels add only a few tokens to the text. Model prices on October 18, 2026, per 1M tokens. Assume (a hypothetical example) 500 contracts of 6,000 tokens each, with 500-token answers. Input is 3,000,000, output 250,000.

  • claude-sonnet-5-5 (input $0.4, output $2): 3,000,000 × 0.4 / 1,000,000 + 250,000 × 2 / 1,000,000 = 1.20 + 0.50 = $1.70.
  • deepseek-v4.1-flash ($0.013 / $0.038): 3,000,000 × 0.013 / 1,000,000 + 250,000 × 0.038 / 1,000,000 = 0.039 + 0.0095 ≈ $0.05.

Fresh prices are always in the GET https://api.zukko.pro/v1/models response.

FAQ

Are regular expressions enough?

For formal data (emails, phones, cards, SNILS) usually yes, provided the patterns are tested on your formats. For names, organizations and addresses you need dictionaries from your reference lists or entity recognition, and a spot check is mandatory.

Can I ask the model itself to find personal data?

As a second line of checking, yes, but not as the only one: that would mean sending the text to a model in its original form, which is exactly what you wanted to avoid. Such a check makes sense to run locally, on a model inside your own perimeter.

What if the labels in the answer changed?

Do not substitute values blindly. Repeat the request with a stronger instruction or send the document to manual processing. The leftovers function in the example catches such cases.

How do I connect from Russia?

Access from Russia goes through the gateway, payment by SBP, a MasterCard card or crypto. The key is created in the Zukko dashboard.

zukko

Pay 25×
less

for Claude Code & ChatGPT

One fetch_* key and base URL api.zukko.pro — for Cursor, Claude Code, Codex, and any SDK.

Get API keyDocumentation

api.zukko.pro · fetch_*

Anonymizing data before sending it to a neural network · Zukko