Перейти к содержимому

Сработал safety / prompt injection

В приложении с пользовательским вводом safety-инциденты — самый «тихий» класс. Модель ответила I cannot help with that, content-filter Azure вернул 400 content_filter, jailbreak вытащил системный промпт — обычно вы узнаёте об этом через жалобу пользователя через неделю, когда уже поздно.

Слать push на каждый случай — много шума, но на первый случай в день / в час или на резкое усиление частоты — самое то.

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].text
if looks_like_refusal(text):
notify("🛑 Safety: модель отказалась",
f"Запрос:\n{user_input[:600]}\n\nОтвет:\n{text[:400]}",
priority=7)

Для прода вместо «слать каждый раз» используйте sliding-window: если за 10 минут отказов больше N — push с агрегатом.

OpenAI / Azure / Vertex возвращают явные ошибки — их легко перехватить:

import openai
try:
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".

Простой набор сигнальных строк, которые редко встречаются в нормальных запросах, но почти всегда — в jailbreak-попытках:

INJECTION_PATTERNS = (
"ignore (all )?previous instructions",
"you are now", "act as", "pretend to be",
"system prompt", "show your prompt",
"забудь все инструкции", "представь, что ты",
"jailbreak", "DAN mode",
)
import re
RX = 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, обычно именно сюда.

Полезный шаблон — слать первый случай в окне и потом периодический агрегат:

import time
WINDOW_SEC = 600
state = {"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;
  • идентификатор сессии/чата, чтобы можно было быстро найти.