Редагування SSE: захист системних промптів LLM в архітектурах зі стрімінгом
Автор Better ISMS — лютий 2026
Автор Better ISMS — лютий 2026
Якщо ви будуєте продукт на базі LLM, ваш системний промпт — це ваша логіка продукту. Коли хтось його витягує, він отримує вашу логіку міркувань, ваші захисні механізми, вашу конкурентну перевагу — все. І якщо ви транслюєте відповіді через Server-Sent Events (а ви, ймовірно, це робите), захист від витоку стає складнішим, ніж здається.
У цьому дописі описано редагування SSE — техніку, яку ми розробили для ISMS Copilot, щоб виявляти та нейтралізувати витоки системних промптів під час стрімінгу. Ми ділимося архітектурою, щоб інші розробники LLM-продуктів могли реалізувати щось подібне.
Проблема
Більшість застосунків на базі LLM транслюють відповіді клієнту частинами за допомогою SSE. Кожна частина надсилається одразу після генерації. Немає етапу "перевірки повної відповіді перед відправкою" — це б звело нанівець саму ідею стрімінгу.
Це створює прогалину в безпеці: якщо джейлбрейк-промпт переконає модель вивести її системні інструкції, вміст вже надсилається клієнту, перш ніж ви зможете його зупинити. До моменту, коли ви зрозумієте, що сталося, користувач уже побачить сотні або тисячі символів вашого системного промпту.
Традиційна фільтрація виводу тут не працює. Ви не можете буферизувати всю відповідь (затримка погіршує користувацький досвід), і не можете перевіряти кожну крихітну частину окремо (фрагмент з 5 слів не схожий на системний промпт).
Для загальних стратегій запобігання джейлбрейкам див. Зменшення ризиків джейлбрейків та ін'єкцій промптів. Редагування SSE — це захід глибинного захисту на випадок, коли ці запобіжні заходи не спрацьовують.
Архітектура
Редагування SSE працює в чотири етапи.
Етап 1 — Створення відбитків. Перед початком будь-якої розмови ви витягуєте набір фраз-відбитків із вашого системного промпту. Це характерні рядки, які з'являються разом лише тоді, коли модель відтворює свої інструкції. Ви хочете, щоб фрази були розподілені по різних секціях промпту — визначення ролей, назви обмежень, правила поведінки. Кількість відбитків і поріг спрацьовування — це налаштовувані параметри, які ви тримаєте в секреті.
Етап 2 — Накопичення та періодична перевірка. У міру стрімінгу частин моделлю, охоронець накопичує повний текст відповіді. Через регулярні інтервали (вимірюються кількістю символів, а не частин), він перевіряє накопичений вміст на відповідність набору відбитків. Перевірка кожної частини була б марнотратною — відбиткам потрібен достатній контекст для змістовного співставлення.
Етап 3 — Поширення помилки. Коли охоронець виявляє достатню кількість співпадінь відбитків, він генерує типізовану помилку (у нашому випадку — SystemPromptLeakError). Саме тут криється нюанс. У архітектурі зі стрімінгом цикл обробки частин зазвичай має блок try/catch для обробки некоректних даних SSE (поганий JSON, неочікувані формати). Цей узагальнений блок catch поглине вашу помилку безпеки, якщо ви не будете обережні. Вам потрібен охоронний пункт, який повторно генерує ваш специфічний тип помилки до того, як спрацює узагальнений обробник:
catch (e) {
if (e instanceof Error && e.name === 'SystemPromptLeakError') throw e;
// узагальнена обробка помилок продовжується для всього іншого
}Це один рядок, але без нього вся система виявлення неактивна. Охоронець спрацьовує, фіксує виявлення в журналах, і стрім спокійно продовжує доставляти ваш системний промпт зловмиснику. Ми навчилися цьому на гіркому досвіді — наш охоронець ідеально виявляв витоки в журналах, абсолютно не зупиняючи їх.
Етап 4 — Редагування. Коли помилка поширюється до контролера стріму, він надсилає клієнту подію редагування SSE. Клієнт замінює вже відображений вміст на повідомлення про відмову. Сервер одночасно замінює збережений вміст у базі даних, щоб витік не зберігся.
Що бачить користувач
Зловмисник короткочасно бачить частковий стрімований вміст — можливо, кілька секунд — потім уся відповідь замінюється на узагальнене повідомлення про відмову. Користувацький досвід такий: текст з'являється, потім зникає і замінюється. Частковий вміст, який він побачив, неповний і змішаний зі звичайним текстом відповіді, що робить його ненадійним для вилучення.
Дізнайтеся більше про те, як працюють повідомлення про відмову, у статті Обробка відмов та обмежень контексту.
Проблема блоку catch
Це заслуговує на особливу увагу, оскільки це той тип багу, який проходить усі тести, але провалюється в продакшені.
Якщо ви використовуєте асинхронні генератори для стрімінгу, ваш цикл парсингу SSE, ймовірно, виглядає так:
for (const line of sseLines) {
try {
const data = JSON.parse(line);
const text = extractText(data);
await onChunkCallback(text); // <-- охоронець працює тут
yield text;
} catch (e) {
console.error('Error parsing chunk:', e);
// перехід до наступного рядка
}
}Зворотний виклик знаходиться всередині блоку try. Якщо охоронець генерує помилку, блок catch фіксує її як помилку парсингу і продовжує роботу. У нашому випадку охоронець коректно виявляв витік на кожній частині після досягнення порогу — журнали показували, що SystemPromptLeakError спрацьовував неодноразово — але стрім завершувався нормально, зберігав повний витік промпту в базі даних і надсилав його клієнту.
Додаткове ускладнення: ця поведінка залежить від середовища виконання. У Node.js помилки асинхронних генераторів з зворотних викликів можуть поширюватися інакше, ніж у Deno. Наші тести проходили в середовищі тестування Node.js, оскільки помилка випадково поширювалася. У продакшені Deno вона поглиналася. Якщо ви будуєте це, тестуйте у своєму реальному продакшен-середовищі, а не лише в тестовому раннері.
Варті уваги проектні рішення
Чому відбитки замість вбудовування подібності або точного співставлення? Відбитки працюють швидко (співставлення рядків), детерміновано (без викликів моделі) і стійкі до перефразування. Модель рідко перефразовує власний системний промпт під час витоку — вона відтворює його дослівно або майже дослівно. Вбудовування подібності додає затримку на кожну перевірку і створює ризик хибнопозитивних спрацьовувань на легітимному контенті відповідності. Точне співставлення підрядків надто крихке (відмінності у пробілах, форматуванні).
Чому перевірка періодична, а не на кожній частині? Частини маленькі (часто 3–10 символів). Одна частина не має значення для виявлення. Накопичення до мінімального порогу перед перевіркою зменшує обчислювальні витрати і забезпечує достатній контекст для надійного співставлення.
Чому не буферизувати всю відповідь? Буферизація вбиває стрімінговий UX. Користувачі очікують побачити текст у реальному часі. Буфер у 2 секунди помітний; буферизація повної відповіді на 4000+ символів неприйнятна. Редагування SSE зберігає стрімінг у реальному часі для 99.99% розмов і втручається лише під час активного витоку.
Чому замінювати також у базі даних? Якщо редагувати лише на клієнті, витік зберігається на стороні сервера. Будь-хто з доступом до бази даних, будь-яка функція експорту, будь-який ендпоінт історії розмов може його викрити.
Що це не вирішує
Редагування SSE — це захід глибинного захисту, а не срібна куля.
Воно не запобігає спробам витоку моделлю. Це забезпечують власні інструкції вашого системного промпту (явні інструкції щодо відмови, секції обмежень). Редагування SSE — це страхувальна сітка на випадок, коли ці інструкції не спрацьовують — а з достатньою креативністю джейлбрейки іноді таки спрацьовують.
Воно не запобігає витокам, коротшим за поріг виявлення. Якщо хтось змусить модель розкрити одне речення системного промпту, кількість відбитків не досягне порогу. Це зроблено свідомо — ви балансуєте між виявленням повних витоків (висока впевненість) і фіксацією часткових згадок (високий ризик хибнопозитивних спрацьовувань).
Зловмисник таки бачить частковий вміст до редагування. Протягом кількох секунд стрімований текст видимий. Це властивість архітектур зі стрімінгом. Частковий вміст неповний і позбавлений структури, але це не нульова експозиція.
Редагування SSE доповнює, але не замінює найкращі практики безпеки системних промптів. Див. Системні промпти та Захист робочого простору та користувацьких інструкцій для базових заходів безпеки.
Контрольний список реалізації
Якщо ви хочете реалізувати це для свого LLM-продукту:
- Витягніть фрази-відбитки з вашого системного промпту — виберіть характерні рядки, що охоплюють різні секції.
- Створіть охоронця, який накопичує стрімований вміст і періодично перевіряє його на відповідність відбиткам.
- Визначте клас типізованої помилки з характерною назвою для виявлення витоку.
- Перевірте кожен блок
catchу вашому стрімінговому конвеєрі — додайте охоронні повторні генерації помилок для вашого типу помилки. - У вашому контролері стріму обробіть помилку, надіславши подію редагування та замінивши збережений вміст.
- На клієнті обробіть подію редагування, замінивши відображений вміст на повідомлення про відмову.
- Тестуйте у своєму продакшен-середовищі, а не лише в тестовому раннері.
- Тримайте ваші відбитки, пороги та інтервали перевірок у секреті.
Заключні думки
Найскладнішою частиною цього була не алгоритмічна частина виявлення — це був однолінійний баг у блоці catch, який мовчки вимикав усю систему. Безпека в архітектурах зі стрімінгом ламається на рівні "сантехніки", а не алгоритмів. Якщо ви розробляєте функції безпеки для LLM, простежте повний шлях помилки від виявлення до дії, видимої користувачеві, і перевірте його у своєму реальному продакшен-середовищі.
Для ширшого погляду на практики безпеки ШІ в ISMS Copilot див. Огляд безпеки та відповідального використання ШІ.
Better ISMS розробляє інструменти відповідності для команд з інформаційної безпеки. ISMS Copilot — наш ШІ-асистент для ISO 27001, SOC 2, GDPR та пов'язаних фреймворків.