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 for | How to find it | Reliability |
|---|---|---|
| regular expression | high | |
| Phone | regular expression covering +7 and 8 | high, but check your own formats |
| Bank card number | a digit chain plus a Luhn check | high |
| SNILS | the pattern 123-456-789 01 | high |
| INN | digits next to the word "ИНН" | medium: without context 10–12 digits are easily confused with other numbers |
| Passport series and number | a pattern of 4 and 6 digits | medium: false positives are possible |
| Names, organizations, addresses | a dictionary from your CRM or named entity recognition | depends on the data and the language |
| Contract and order numbers | your numbering pattern | high 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
- 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. - 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".
- 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
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 == 0How it works on an example:
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:
Клиент [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:
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
leftoversfunction 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
- A test set. Collect 30–50 typical documents, run them through
maskand look at the result. Look especially for missed names and addresses. - A second search on the masked output. Run the anonymized text through the same patterns again: nothing should remain.
- 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.
- Manual spot checks of what actually goes into the request, on the first few hundred documents.
- 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.
Related articles
- AI for testers: test cases, checklists and bug reportsHow a QA engineer can use a model for test cases, checklists and bug reports, check coverage, and avoid requirements the model made up.
- A neural network for HR: job posts, candidate emails and resume reviewA model can draft job posts and emails and do a first-pass resume review. What to hand over, how to treat candidates' personal data and how to prevent biased decisions.
- Generating an FAQ with a neural network from tickets and a knowledge baseA good FAQ is built on what customers really ask. A pipeline: group the tickets, answer from the knowledge base with a source reference, and verify the result.
- Website copy with AI: homepage, services and FAQ without fillerAI writes smoothly but does not know your business. How to build a brief with facts, get page structure, edit the result before publishing, and what it costs.