Практика · LLM Observability
Редакция чувствительных данных в LLM-трассировках без потери диагностической ценности
Полный промпт ускоряет расследование сбоев, но одновременно превращает систему наблюдаемости в хранилище персональных данных, секретов и коммерческой информации. Ниже — воспроизводимый конвейер, который уменьшает этот риск и оставляет достаточно сигналов для отладки.
Что мы построим
LLM-трассировка — это связанная последовательность событий одного обращения к модели: входные сообщения, вызовы инструментов, ответы, ошибки, задержки и служебные атрибуты. Сохранять её целиком удобно, но безопаснее считать содержимое недоверенным уже в момент поступления.
Итоговый конвейер будет выполнять пять операций:
- оставлять только разрешённые поля события;
- редактировать известные чувствительные поля независимо от их значения;
- псевдонимизировать повторяющиеся идентификаторы;
- искать опасные фрагменты внутри свободного текста;
- отбрасывать или сокращать данные, не прошедшие проверку.
Главный принцип: диагностическая ценность хранится не только в исходном тексте. Для расследований часто достаточно типа события, модели, длительности, размера входа, кода ошибки, позиции отредактированного фрагмента и стабильной псевдонимной метки.
Шаг 1. Зафиксируйте политику до написания регулярных выражений
Разделите данные на четыре класса. Это пример политики, а не универсальная классификация: границы необходимо согласовать с требованиями вашей организации.
| Класс | Примеры | Действие |
|---|---|---|
| Разрешённые метаданные | Тип события, длительность, имя модели, HTTP-статус | Хранить с ограниченным сроком |
| Связующие идентификаторы | User ID, session ID, tenant ID | Псевдонимизировать |
| Чувствительное содержимое | Email, телефон, внутренний номер договора | Заменять типизированным маркером |
| Секреты | Пароль, токен, приватный ключ | Удалять; событие при необходимости помещать в карантин |
Для каждого поля заранее задайте владельца, цель хранения, допустимые потребители и срок удаления. Если назначение поля невозможно объяснить, не включайте его в журнал.
Шаг 2. Создайте контролируемый набор
Не проверяйте редакцию на реальных пользовательских данных. Следующий файл содержит только явно вымышленные маркеры. Они нужны, чтобы увидеть пропуски и избыточные замены.
{
"trace_id": "trace-demo-001",
"event": "model.request",
"model": "example-model",
"latency_ms": 842,
"user_id": "demo-user-42",
"session_id": "demo-session-7",
"messages": [
{
"role": "user",
"content": "Напишите на test.person@example.invalid. Телефон +7 000 000-00-00. Код проекта ALJ-DEMO-17."
},
{
"role": "system",
"content": "api_key=DEMO_SECRET_DO_NOT_USE_123456"
}
],
"headers": {
"authorization": "Bearer DEMO_TOKEN_DO_NOT_USE_123456",
"content-type": "application/json"
},
"error": null
}
Домен .invalid зарезервирован для примеров и не должен разрешаться в рабочий адрес. Остальные строки также помечены как демонстрационные и не являются действующими реквизитами.
Шаг 3. Реализуйте многоступенчатую редакцию
Ниже — автономный пример на Python без сторонних пакетов. Сохраните его как redact_trace.py. Ключ псевдонимизации передаётся только через окружение и не записывается в код или журнал.
import hashlib
import hmac
import json
import os
import re
import sys
ALLOWED_TOP_LEVEL = {
"trace_id", "event", "model", "latency_ms",
"user_id", "session_id", "messages", "headers", "error"
}
DROP_FIELDS = {
"authorization", "cookie", "set-cookie",
"password", "passwd", "secret", "api_key", "access_token"
}
PSEUDONYM_FIELDS = {"user_id", "session_id", "tenant_id"}
PATTERNS = [
(
"EMAIL",
re.compile(
r"\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b",
re.IGNORECASE
),
),
(
"PHONE",
re.compile(r"(?<!\w)\+?\d[\d ()-]{7,}\d(?!\w)"),
),
(
"BEARER",
re.compile(r"\bBearer\s+[A-Za-z0-9._~+/=-]{12,}", re.IGNORECASE),
),
(
"ASSIGNMENT_SECRET",
re.compile(
r"\b(api[_-]?key|token|password|secret)\s*[:=]\s*[^\s,;]{8,}",
re.IGNORECASE
),
),
]
MAX_TEXT_LENGTH = 4000
MAX_DEPTH = 12
def pseudonym(value, key):
digest = hmac.new(
key.encode("utf-8"),
str(value).encode("utf-8"),
hashlib.sha256,
).hexdigest()[:16]
return "pseudonym:" + digest
def redact_text(value):
original_length = len(value)
text = value[:MAX_TEXT_LENGTH]
findings = []
for label, pattern in PATTERNS:
count = 0
def replace(match):
nonlocal count
count += 1
return f"[REDACTED:{label}]"
text = pattern.sub(replace, text)
if count:
findings.append({"type": label, "count": count})
return {
"text": text,
"original_length": original_length,
"truncated": original_length > MAX_TEXT_LENGTH,
"findings": findings,
}
def walk(value, key, field=None, depth=0):
if depth > MAX_DEPTH:
return "[DROPPED:MAX_DEPTH]"
if field and field.lower() in DROP_FIELDS:
return "[REDACTED:FIELD]"
if field and field.lower() in PSEUDONYM_FIELDS:
return pseudonym(value, key)
if isinstance(value, dict):
result = {}
for child_key, child_value in value.items():
result[child_key] = walk(
child_value, key, child_key, depth + 1
)
return result
if isinstance(value, list):
return [
walk(item, key, field, depth + 1)
for item in value[:100]
]
if isinstance(value, str):
redacted = redact_text(value)
if redacted["findings"] or redacted["truncated"]:
return redacted
return value
return value
def redact_event(event, key):
filtered = {
name: event[name]
for name in ALLOWED_TOP_LEVEL
if name in event
}
return walk(filtered, key)
def main():
key = os.environ.get("TRACE_PSEUDONYM_KEY")
if not key or len(key) < 32:
raise SystemExit(
"TRACE_PSEUDONYM_KEY должен содержать не менее 32 символов"
)
event = json.load(sys.stdin)
result = redact_event(event, key)
json.dump(result, sys.stdout, ensure_ascii=False, indent=2)
sys.stdout.write("\n")
if __name__ == "__main__":
main()
Очередность имеет значение. Сначала действует список разрешённых верхнеуровневых полей, затем правила по имени поля, после них — обработка свободного текста. Благодаря этому секрет в authorization удаляется даже тогда, когда его формат неизвестен регулярным выражениям.
Шаг 4. Запустите безопасно
Сохраните демонстрационное событие в fixture.json, а программу — в redact_trace.py. Для локальной проверки задайте временный демонстрационный ключ. Не используйте этот ключ в рабочей среде.
export TRACE_PSEUDONYM_KEY='demo-only-key-32-characters-minimum'
python3 redact_trace.py < fixture.json > redacted.json
python3 -m json.tool redacted.json
В производственной системе ключ следует получать из штатного хранилища секретов непосредственно при запуске процесса. Разделяйте ключи по средам, ограничивайте доступ к ним и планируйте ротацию: смена ключа изменит псевдонимы и разорвёт сопоставление между периодами.
Шаг 5. Проверьте результат автоматически
В корректно обработанном файле должны сохраниться event, model, latency_ms, роли сообщений и код проекта. Email, телефон, демонстрационный секрет и bearer-токен не должны остаться в открытом виде. Идентификаторы пользователя и сессии должны превратиться в стабильные HMAC-псевдонимы.
Создайте verify_redaction.py:
import json
import sys
with open("redacted.json", encoding="utf-8") as source:
result = json.load(source)
serialized = json.dumps(result, ensure_ascii=False)
for forbidden in [
"test.person@example.invalid",
"+7 000 000-00-00",
"DEMO_SECRET_DO_NOT_USE_123456",
"DEMO_TOKEN_DO_NOT_USE_123456",
"demo-user-42",
"demo-session-7",
]:
if forbidden in serialized:
raise SystemExit(f"Утечка контролируемого маркера: {forbidden}")
assert result["event"] == "model.request"
assert result["model"] == "example-model"
assert result["latency_ms"] == 842
assert "ALJ-DEMO-17" in serialized
assert result["user_id"].startswith("pseudonym:")
assert result["session_id"].startswith("pseudonym:")
print("Проверка пройдена: маркеры удалены, диагностические поля сохранены")
Запуск:
python3 verify_redaction.py
Это проверка конкретного контролируемого набора, а не доказательство отсутствия утечек во всех входах. Расширяйте набор случаями с Unicode, вложенными объектами, длинными строками, необычными разделителями и секретами, характерными для вашей инфраструктуры.
Как встроить конвейер в рабочую трассировку
Редактор должен работать до экспорта события в коллектор, очередь или SaaS-систему наблюдаемости. Редакция только на стороне интерфейса скрывает данные от оператора, но не удаляет их из хранилища.
- Сформируйте событие в памяти приложения.
- Примените структурный список разрешённых полей.
- Удалите секреты и псевдонимизируйте идентификаторы.
- Просканируйте свободный текст и ограничьте объём.
- Проверьте итоговую схему и только затем экспортируйте.
Полезно вести отдельные счётчики без исходных значений: число замен по типам, число отброшенных событий, долю усечённых сообщений, версию политики и задержку редактора. Резкий рост REDACTED:FIELD может указывать на изменение входной схемы, а рост карантина — на новую форму секрета или ошибку интеграции.
Если организации действительно нужен доступ к исходному событию для редкого расследования, храните его в отдельном зашифрованном контуре с коротким сроком жизни, строгим аудитом доступа и процедурой одобрения. Такой контур не должен быть обычным источником для поиска и аналитики.
Типовые ошибки
Полагаться только на регулярные выражения
Они пропускают нестандартные форматы и могут ошибочно скрывать безопасный текст. Структурное правило для поля password надёжнее попытки распознать значение пароля.
Редактировать после записи
За время между записью и очисткой данные уже могут попасть в резервную копию, поисковый индекс, выгрузку или уведомление. Обрабатывайте событие до первой внешней границы.
Использовать обычный хеш для идентификаторов
Предсказуемые значения можно перебрать. HMAC с отдельным секретным ключом снижает этот риск и сохраняет возможность сопоставлять события одного субъекта внутри выбранного периода.
Заменять всё одним маркером
[REDACTED] не сообщает, что именно было найдено. Типизированные маркеры и счётчики сохраняют диагностическую ценность без сохранения исходного значения.
Записывать ошибку редактора вместе с исходным событием
Обработчик исключений нередко возвращает опасный вход в журнал. При сбое сохраняйте только код ошибки редакции, идентификатор трассы, версию политики и безопасные метаданные.
Не версионировать правила
Без версии политики трудно понять, почему два похожих события обработаны по-разному. Добавляйте к результату идентификатор набора правил, но не помещайте туда ключ или чувствительную конфигурацию.
Ограничения
Такой конвейер уменьшает поверхность утечки, но не гарантирует абсолютную анонимность. Чувствительный смысл может сохраняться в редких формулировках, комбинациях метаданных, изображениях, вложениях, tool-вызовах или необычной кодировке. Регулярные выражения не понимают контекст, а статистические детекторы дают ложные срабатывания и пропуски.
Псевдонимизация также не равна анонимизации: стабильные метки позволяют связывать события. Поэтому к отредактированным трассам всё равно нужны контроль доступа, ограниченный срок хранения, аудит выгрузок и минимизация числа копий.
Не отправляйте сырой текст во внешнюю модель «для проверки на PII»: это создаёт ещё одну передачу чувствительных данных. Если семантический детектор необходим, оцените его локальное развёртывание, модель угроз и поведение при отказе. Безопасный режим отказа — не экспортировать сомнительное событие.
Контрольный список перед включением
- События проходят редакцию до очереди, коллектора и диска.
- Верхнеуровневые поля разрешены явно, а не принимаются по умолчанию.
- Известные секретные поля удаляются независимо от формата значения.
- Связующие идентификаторы защищены HMAC, а ключ не попадает в логи.
- Длина строк, размер списков и глубина вложенности ограничены.
- Контролируемые маркеры проверяются автоматически при изменении правил.
- Ошибка редактора не приводит к экспорту исходного события.
- У политики есть версия, владелец и процедура пересмотра.
- Для отредактированных логов заданы доступ и срок удаления.
Что дальше
Встройте проверку контролируемых маркеров в CI, а затем добавьте случаи из собственных схем — только синтетические или специально обезличенные. Для проектирования остальных частей наблюдаемости используйте практические руководства, а определения терминов сверяйте с глоссарием Agent Lab Journal.