Инструкция по интеграции: PII Proxy Wrapper
Что это
PII Proxy — тонкая программная прослойка между вашим приложением и LLM-провайдером, которая автоматически обнаруживает и обезличивает персональные данные в запросах перед отправкой в нейросеть, а затем восстанавливает их в ответе.
Ваше приложение общается с прокси как с обычным OpenAI-совместимым API — формат запросов не меняется. Прокси берёт на себя всю логику PII-защиты.

Проблема
При передаче пользовательских данных в публичные LLM вы теряете контроль над чувствительной информацией: данные уходят за периметр вашей инфраструктуры. PII Proxy решает эту проблему — модель видит только обезличенный текст. Персональные данные проходят через прослойку транзитно, но сама нейросеть их не получает.
Два сценария
Обезличивание и генерация — это два независимых шага, и каждый из них может выполняться как на стороне AIaaS, так и на вашей. Отсюда два рабочих сценария.
Сценарий 1. Обезличивание через AIaaS, модель — ваша
Прокси разворачивается на вашей стороне, а поиск и маскирование персональных данных выполняются вызовами к AIaaS: /ner, /mask и /unmask. Обезличенный текст вы отправляете в ту модель, которую выбрали сами — собственную, развёрнутую у вас, или любого внешнего провайдера.
В AIaaS при этом попадает только текст, передаваемый на обезличивание. Сам запрос к модели через наш контур не идёт.
Сценарий подходит, когда модель уже выбрана и менять её не планируется, а закрыть нужно именно риск утечки ПДн.
Сценарий 2. Обезличивание у вас, модель — в AIaaS
Вы разворачиваете у себя собственный NER-движок — например, ту же GLiNER — и логику маскирования и восстановления. В контур AIaaS приходит уже обезличенный запрос.
Персональные данные в этом случае не покидают ваш периметр вообще: ни в модель, ни в сервисы обезличивания. AIaaS видит только текст с плейсхолдерами.
Сценарий подходит организациям, которым нужен доступ к широкому каталогу моделей, но регламент запрещает передавать ПДн наружу даже на обработку.
Что выбрать
| Сценарий 1 | Сценарий 2 | |
|---|---|---|
| Где обезличивание | В AIaaS | У вас |
| Где модель | У вас или у стороннего провайдера | В AIaaS |
| Что попадает в AIaaS | Текст с ПДн — на обезличивание | Только обезличенный текст |
| Что нужно развернуть | Прокси-обёртку | Прокси-обёртку и NER-движок |
| Правила детекции | Общие для контура | Полностью ваши |
| Доступ к каталогу моделей AIaaS | Не используется | Используется полностью |
Есть и третий вариант, при котором ничего разворачивать не нужно: protected-маршруты — обезличивание и генерация выполняются в AIaaS, а от вас требуется только сменить значение поля model.
Как это работает
Ниже описан сценарий 1 — обезличивание через AIaaS с отправкой в вашу модель.
| # | Шаг | Куда | Что происходит |
|---|---|---|---|
| 1 | NER | POST /v1/ner | Обнаружение ПДн в тексте: ФИО, телефон, email, адрес, паспорт, даты |
| 2 | Mask | POST /v1/mask | Замена найденных ПДн на плейсхолдеры вида ***N_LABEL***, получение mapping-словаря |
| 3 | LLM | Ваш LLM-провайдер | Обезличенный текст уходит в нейросеть. LLM не получает ПДн |
| 4 | Unmask | POST /v1/unmask | Плейсхолдеры в ответе LLM заменяются на исходные значения из mapping |
Интеграция: полный код обёртки
import json
import requests
class PIIProxy:
"""
Прозрачная прокси-обёртка: ваш код → PII-очистка → LLM → PII-восстановление.
Интерфейс совместим с OpenAI /chat/completions.
"""
AINERGY_URL = "https://<адрес, выданный при подключении>"
def __init__(self, ainergy_token: str, llm_base_url: str, llm_api_key: str):
self.ainergy_headers = {
"Authorization": f"Bearer {ainergy_token}",
"X-AIN-USERNAME": "<ваш-username>",
"X-AIN-SOURCEID": "<ваш-source-id>",
"Content-Type": "application/json",
}
self.llm_base_url = llm_base_url.rstrip("/")
self.llm_headers = {
"Authorization": f"Bearer {llm_api_key}",
"Content-Type": "application/json",
}
# плейсхолдер → исходное значение, накапливается за сессию
self.mapping = {}
def chat_completion(self, openai_request: dict) -> dict:
user_text = self._user_text(openai_request["messages"])
if not user_text:
return self._call_llm(openai_request)
# 1. Найти персональные данные
ner = self._ainergy("/ner", {
"model": "itg.pii-detection.ner",
"text": user_text,
})
if not ner.get("entities"):
# ПДн не найдены — отправляем запрос как есть
return self._call_llm(openai_request)
# 2. Обезличить. entities передаётся из ответа /ner без изменений
mask = self._ainergy("/mask", {
"model": "itg.pii-detection.mask",
"text": user_text,
"entities": ner["entities"],
})
self.mapping.update(mask["mapping"])
# 3. В модель уходит только обезличенный текст
response = self._call_llm(
self._replace_user_text(openai_request, mask["text"])
)
# 4. Восстановить значения в ответе
content = response["choices"][0]["message"].get("content")
if content and self.mapping:
restored = self._ainergy("/unmask", {
"model": "itg.pii-detection.unmask",
"text": content,
"mapping": self.mapping,
})
response["choices"][0]["message"]["content"] = restored["text"]
return response
def _ainergy(self, path: str, payload: dict) -> dict:
resp = requests.post(f"{self.AINERGY_URL}/v1{path}",
headers=self.ainergy_headers,
json=payload, timeout=60)
resp.raise_for_status()
return resp.json()
def _call_llm(self, payload: dict) -> dict:
resp = requests.post(f"{self.llm_base_url}/chat/completions",
headers=self.llm_headers,
json=payload, timeout=120)
resp.raise_for_status()
return resp.json()
@staticmethod
def _user_text(messages: list):
"""Последнее текстовое сообщение пользователя."""
for message in reversed(messages):
if message.get("role") == "user" and isinstance(message.get("content"), str):
return message["content"]
return None
@staticmethod
def _replace_user_text(request: dict, text: str) -> dict:
"""Копия запроса, в которой текст пользователя заменён обезличенным."""
patched = json.loads(json.dumps(request))
for message in reversed(patched["messages"]):
if message.get("role") == "user" and isinstance(message.get("content"), str):
message["content"] = text
break
return patched
Обезличиваются только сообщения с ролью user. Системный промпт и история ассистента передаются без изменений. Словарь mapping накапливается за сессию, поэтому восстановление работает и тогда, когда модель упоминает данные из предыдущих сообщений.
Быстрый старт
proxy = PIIProxy(
ainergy_token="<ваш-AInergy-токен>",
llm_base_url="https://<адрес-вашего-LLM-провайдера>",
llm_api_key="<ваш-ключ-LLM>",
)
response = proxy.chat_completion({
"model": "openai/gpt-4.1",
"messages": [{
"role": "user",
"content": "Меня зовут Степан Сергеевич, email: stepa@example.com"
}]
})
print(response["choices"][0]["message"]["content"])
# Вывод: ответ LLM с восстановленными ФИО и email
Пример работы
Входные данные
Паспортная анкета из 17 полей: ФИО, дата рождения, паспорт, адрес, СНИЛС, ИНН, телефон, email, место работы, автомобиль и т.д.
Этап 1: NER — обнаружено 17 сущностей
PHONE_NUMBER +7 (900) 000-00-00 score=0.986
PHONE_NUMBER +7 (900) 111-11-11 score=0.993
EMAIL demo.person@example.com score=0.975
SURNAME Тестов score=0.459
CITY г. Демоград score=0.863
CITY г. Демоград score=0.936
CITY г. Демоград score=0.964
COUNTRY РФ score=0.973
STREET ул. Примерная / проспект Тестовый score=0.637 / 0.449
PASSPORT_NUMBER 000000 score=0.589
SNILS 000-000-000 00 score=0.487
JOB_TITLE специалист по тестовым данным score=0.896
JOB отдел проверки форматов score=0.636
DATE 01.01.2020 score=0.614
YEAR 2022 score=0.940
Этап 2: Mask — текст обезличен
До: ФИО: Иван Александрович Тестов, тел: +7 (900) 000-00-00, email: demo.person@example.com...
После: ФИО: ***NAME*** ***0_SURNAME***, тел: ***1_PHONE_NUMBER***, email: ***2_EMAIL***...
Этап 3: LLM — GPT-4.1 обрабатывает обезличенный текст
LLM видит: ФИО: ***0_SURNAME***... и не имеет доступа к реальным данным.
Этап 4: Unmask — ПДн восстановлены
Ответ LLM после восстановления:
ФИО: Иван Александрович Тестов
Дата рождения: 14.03.1991
Паспорт: серия 0000 номер 000000
Телефон: +7 (900) 000-00-00
E-mail: demo.person@example.com
...
Метрики цикла
| Параметр | Значение |
|---|---|
| Объём текста | 818 символов, 17 полей ПДн |
| Токенов на NER (raw) | 1 180 |
| Найдено сущностей | 17 |
| Mapping (плейсхолдеров) | 14 |
| Токенов LLM (prompt+completion) | 733 |
| End-to-end latency | ~5 секунд |
Особенности эксплуатации
Типы обнаруживаемых сущностей
Модель надёжно определяет следующие категории ПДн:
- EMAIL — адреса электронной почты (точность 97–99%)
- PHONE_NUMBER — телефонные номера в любом формате (точность 97–99%)
- CITY — названия городов (точность 86–96%)
- COUNTRY — названия стран (точность 97%)
- DATE — даты (точность ~60%)
- YEAR — года (точность 94%)
- JOB_TITLE — должности (точность 89%)
- JOB — места работы (точность ~64%)
- PERSON_NAME — имена и фамилии (точность 46–89%)
Дополнительная валидация
Для гарантированного покрытия полей со строгим форматом (ИНН, СНИЛС, паспортные серии) рекомендуется добавить правила в конфигурацию сервиса — файл configs/rules.yml. Это позволяет комбинировать AI-детекцию с детерминированными regex-паттернами без изменения клиентского кода.
Формат восстановленных данных
/unmask возвращает исходные значения как есть, без дополнительной разметки: ***0_SURNAME*** → Тестов. Обрамление восстановленных значений настраивается в конфигурации сервиса — если оно включено, значение вернётся в квадратных скобках. Проверить фактическое поведение можно одним вызовом /unmask с известным mapping.
Тарификация
Услуга PII-очистки тарифицируется в А-токенах по формуле:
А-токены = input_tokens × coefficient
| Объём текста | Raw-токены | ~А-токенов (coeff=0.001) |
|---|---|---|
| 1 предложение (ФИО + email + телефон) | 130 | 0.13 |
| Паспортная анкета (17 полей, 818 символов) | 1 180 | 1.18 |
| Развёрнутый текст (~5 000 символов) | ~5 000 | ~5 |
Рекомендации по внедрению
Streaming (SSE)
При использовании потокового режима (stream=true) накапливайте чанки LLM-ответа, затем выполните один вызов unmask на полном тексте. Либо — unmask каждого чанка отдельно (работает, если mapping уже полный).
Tool calling / Function calling
Аргументы, которые LLM передаёт в вызовы функций (tool_calls), могут содержать ПДн. Прокси поддерживает их маскирование и восстановление.
RAG / Embeddings
Если вы векторизуете пользовательские запросы для RAG — обезличивайте их перед эмбеддингом и сохраняйте mapping для обратной замены в извлечённых чанках.
Конфиденциальность
Все вызовы PII API (NER, Mask, Unmask) выполняются внутри контура ITGLOBAL.COM. ПДн не покидают инфраструктуру облачного провайдера.