Сработал safety / prompt injection
В приложении с пользовательским вводом safety-инциденты — самый «тихий»
класс. Модель ответила I cannot help with that, content-filter Azure
вернул 400 content_filter, jailbreak вытащил системный промпт — обычно
вы узнаёте об этом через жалобу пользователя через неделю, когда уже поздно.
Слать push на каждый случай — много шума, но на первый случай в день / в час или на резкое усиление частоты — самое то.
1. Алёрт на отказ модели
Заголовок раздела «1. Алёрт на отказ модели»REFUSAL_MARKERS = ( "i cannot", "i can't", "i'm not able to", "я не могу", "я не имею", "as an ai", "this request violates",)
def looks_like_refusal(text: str) -> bool: t = text.lower()[:300] return any(m in t for m in REFUSAL_MARKERS)
resp = client.messages.create(...)text = resp.content[0].textif looks_like_refusal(text): notify("🛑 Safety: модель отказалась", f"Запрос:\n{user_input[:600]}\n\nОтвет:\n{text[:400]}", priority=7)Для прода вместо «слать каждый раз» используйте sliding-window: если за 10 минут отказов больше N — push с агрегатом.
2. Content-filter провайдера
Заголовок раздела «2. Content-filter провайдера»OpenAI / Azure / Vertex возвращают явные ошибки — их легко перехватить:
import openaitry: resp = client.chat.completions.create(...)except openai.BadRequestError as e: body = getattr(e, "body", {}) or {} code = body.get("code") or body.get("error", {}).get("code") if code in ("content_filter", "responsible_ai_policy_violation"): notify("🛑 Content-filter сработал", f"Provider: openai\nCode: {code}\n\nInput:\n{user_input[:800]}", priority=7) raiseДля Anthropic — поле stop_reason == "refusal" в ответе и/или
HTTP 400 с error.type == "invalid_request_error".
3. Эвристики на prompt injection
Заголовок раздела «3. Эвристики на prompt injection»Простой набор сигнальных строк, которые редко встречаются в нормальных запросах, но почти всегда — в jailbreak-попытках:
INJECTION_PATTERNS = ( "ignore (all )?previous instructions", "you are now", "act as", "pretend to be", "system prompt", "show your prompt", "забудь все инструкции", "представь, что ты", "jailbreak", "DAN mode",)
import reRX = re.compile("|".join(INJECTION_PATTERNS), re.I)
def check_injection(text: str): m = RX.search(text) if m: notify("🚨 Prompt-injection попытка", f"Совпадение: «{m.group(0)}»\n\nВвод:\n{text[:1000]}", priority=8) # дальше — отказать / пропустить через guard-модельЭвристики — это первое приближение. Серьёзный prod-стенд должен прогонять ввод через отдельную guard-модель (Llama Guard, Prompt Guard, ваш fine-tune). Notifly остаётся слоем алёртинга поверх неё.
4. Алёрт на «пользователь видит системный промпт»
Заголовок раздела «4. Алёрт на «пользователь видит системный промпт»»Если в ответе модели мелькают строки вашего системного промпта — почти гарантированно успешный jailbreak. Сравнивайте на лету:
SYSTEM_FRAGMENTS = [ "You are a helpful assistant for FooCorp", # уникальная фраза "Internal tool: search_internal",]
if any(f in answer for f in SYSTEM_FRAGMENTS): notify("🚨 LEAK: системный промпт в ответе", f"Запрос:\n{user_input[:600]}\n\nОтвет:\n{answer[:1000]}", priority=10)Алёрт с priority: 10 — самый громкий push, обычно именно сюда.
5. Дедупликация: не утопите канал в шуме
Заголовок раздела «5. Дедупликация: не утопите канал в шуме»Полезный шаблон — слать первый случай в окне и потом периодический агрегат:
import timeWINDOW_SEC = 600state = {"first_ts": 0, "count": 0}
def maybe_alert(kind, payload): now = time.time() if now - state["first_ts"] > WINDOW_SEC: state["first_ts"] = now state["count"] = 0 notify(f"🛑 {kind} (первый случай)", payload, priority=7) state["count"] += 1 if state["count"] in (10, 100): # ступенчатый агрегат notify(f"🛑 {kind}: {state['count']} за окно", "Что-то происходит — посмотрите логи.", priority=9)Что положить в текст алёрта
Заголовок раздела «Что положить в текст алёрта»- классификация (refusal / content_filter / injection / leak);
- усечённый ввод пользователя (без PII, если можно);
- ссылка на полный лог в S3/Sentry/CloudWatch;
- идентификатор сессии/чата, чтобы можно было быстро найти.