Обновить

Запустил собственный PII-фильтр перед облачными моделями и отказался от идеи селф-хостед LLM

Уровень сложностиПростой
Время на прочтение4 мин
Охват и читатели10K
Всего голосов 3: ↑2 и ↓1+1
Комментарии4

Комментарии 4

Отличная новость!!!

Ничего не понял, зачем для этого было использовать локальные Gemma4 и Qwen3.8 если есть специализированные Qwen3Guard-Gen и Qwen3Guard-Stream (первая проверяет и маскирует вход, вторая выход)? Размеры моделей 0.6B, 4B, 8B и никакой RTX 6000 PRO 96 GB для них не нужно.

Ну и не будем преувеличивать, локальные модели пока до топовых облачных - все же не доятгивают.

Ну если не выбирать специализированные решения под задачи или не адаптировать под них то конечно, фронтир модели общего назначения это все что остается.

Спасибо за комментарий. Внутри самого фильтра нет ни Gemma, ни Qwen, вообще никакой ML-модели: по описанию проекта это словари, регулярки и проверочные суммы (ИНН и т.п.). Gemma и Qwen у меня отдельно, как локальная модель для данных, которым в облако нельзя совсем. А RTX 6000 PRO - это то, что я смотрел до этого для полного селф-хоста, под фильтр она не нужна.

Про Qwen3Guard: насколько я вижу, это классификатор безопасности. Он выдаёт вердикт (Safe/Unsafe/Controversial) и категорию, среди которых есть PII, но текст не подменяет, а Stream-версия размечает токены ответа. Мне нужно было именно заменить значения плейсхолдерами в запросе в сторону провайдеры и вернуть стримом настоящие в ответ, который читаю я. Как второй слой, чтобы поймать то, что регулярки пропустили, он вполне может подойти, на своих данных не пробовал.

Про Qwen3Guard: насколько я вижу, это классификатор безопасности

Да, он быстро ловит то, что нужно маскировать (PII категория). Если ПДн не найдены то не нужно тратить время на маскирование.

Сама маскировка производится, например, Qwen 3 7B Instruct. Простой пример логики, о которой я пишу:

import json
from vllm import LLM, SamplingParams

llm = LLM(model="Qwen/Qwen3-7B-Instruct", tensor_parallel_size=1)

json_schema = {
    "type": "object",
    "properties": {
        "entities": {
            "type": "array",
            "items": {
                "type": "object",
                "properties": {
                    "text": {"type": "string"},
                    "category": {
                        "type": "string",
                        "enum": [
                            "FIO",
                            "PASSPORT",
                            "PHONE",
                            "EMAIL",
                            "ADDRESS",
                            "SNILS",
                            "INN",
                        ],
                    },
                },
                "required": ["text", "category"],
            },
        }
    },
    "required": ["entities"],
}

system_prompt = """Ты — специализированная модель классификации персональных данных (152-ФЗ РФ).
Найди в тексте все упоминания ПДн и верни их в формате JSON.
Категории: FIO (ФИО), PASSPORT (серия/номер паспорта), PHONE (телефон), EMAIL, ADDRESS (адрес), SNILS, INN."""

user_text = "Клиент Сидоров Алексей Владимирович (паспорт 4510 987654) подал заявление по адресу г. Москва, ул. Арбат, д. 12."

messages = [
    {"role": "system", "content": system_prompt},
    {"role": "user", "content": user_text},
]

prompt = llm.get_tokenizer().apply_chat_template(
    messages, tokenize=False, add_generation_prompt=True
)

sampling_params = SamplingParams(
    temperature=0.0,
    max_tokens=512,
    guided_json=json_schema,
)

outputs = llm.generate([prompt], sampling_params)
result_json = json.loads(outputs[0].outputs[0].text)

def apply_masking(original_text: str, entities_data: dict) -> str:
  masked = original_text
  for item in entities_data.get("entities", []):
    val = item["text"]
    cat = item["category"]
    masked = masked.replace(val, f"[{cat}]")
  return masked

print(apply_masking(user_text, result_json))
# Вывод: Клиент [FIO] (паспорт [PASSPORT]) подал заявление по адресу [ADDRESS].

По хорошему, инструкт сеть еще файнтюнят для максимальной точности, но должно работать и так. Суть в использовании ансамбля специализированных моделей небольшого размера 4B-8B, т. к. чисто детерминированный подход (regex и тд) много пропускает при реальной работе.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации