Redakcja SSE: Ochrona promptów systemowych LLM w architekturach strumieniowych
Autor Better ISMS — luty 2026
Autor Better ISMS — luty 2026
Jeśli budujesz produkt oparty na LLM, Twój prompt systemowy to logika Twojego produktu. Gdy ktoś go wydobędzie, uzyskuje Twój sposób rozumowania, Twoje zabezpieczenia, Twoją przewagę konkurencyjną — wszystko. A jeśli strumieniujesz odpowiedzi za pomocą Server-Sent Events (co prawdopodobnie robisz), obrona przed wydobyciem jest trudniejsza, niż myślisz.
Ten wpis opisuje redakcję SSE, technikę, którą stworzyliśmy dla ISMS Copilot, aby wykrywać i neutralizować wycieki promptów systemowych w trakcie strumieniowania. Dzielimy się architekturą, aby inni budujący produkty oparte na LLM mogli wdrożyć coś podobnego.
Problem
Większość aplikacji LLM strumieniuje odpowiedzi do klienta fragment po fragmencie za pomocą SSE. Każdy fragment jest wysyłany w momencie jego wygenerowania. Nie ma kroku „przejrzyj pełną odpowiedź przed wysłaniem” — to zniweczyłoby cel strumieniowania.
To tworzy lukę w zabezpieczeniach: jeśli prompt jailbreak przekona model do ujawnienia jego instrukcji systemowych, treść jest już wysyłana do klienta, zanim zdążysz ją zatrzymać. Zanim zorientujesz się, co się stało, użytkownik zobaczył już setki lub tysiące znaków Twojego promptu systemowego.
Tradycyjne filtrowanie wyjścia nie działa tutaj. Nie możesz buforować całej odpowiedzi (opóźnienie zabija UX), a sprawdzanie każdego małego fragmentu w izolacji nie ma sensu (5-słowowy fragment nie wygląda jak prompt systemowy).
Aby poznać ogólne strategie zapobiegania jailbreakom, zobacz Zmniejszanie ryzyka jailbreaków i wstrzykiwania promptów. Redakcja SSE to środek obrony w głąb na wypadek, gdyby te zabezpieczenia zawiodły.
Architektura
Redakcja SSE działa w czterech etapach.
Etap 1 — Tworzenie odcisków palców. Przed rozpoczęciem jakiejkolwiek rozmowy, wyodrębniasz zestaw fraz odcisków palców z Twojego promptu systemowego. Są to charakterystyczne ciągi znaków, które pojawiają się razem tylko wtedy, gdy model odtwarza swoje instrukcje. Chcesz, aby frazy były rozproszone w różnych sekcjach promptu — definicje ról, nazwy ograniczeń, reguły zachowania. Liczba odcisków palców i próg dopasowania to parametry, które trzymasz w tajemnicy.
Etap 2 — Akumulacja i okresowe sprawdzanie. Gdy model strumieniuje fragmenty, strażnik akumuluje pełny tekst odpowiedzi. W regularnych odstępach (mierzonych liczbą znaków, a nie fragmentów), sprawdza zgromadzoną treść pod kątem zestawu odcisków palców. Sprawdzanie każdego fragmentu byłoby marnotrawstwem — odciski palców potrzebują wystarczającego kontekstu, aby dopasowanie miało sens.
Etap 3 — Propagacja błędu. Gdy strażnik wykryje wystarczającą liczbę dopasowań odcisków palców, rzuca typowany błąd (w naszym przypadku SystemPromptLeakError). To tutaj kryje się subtelność. W architekturze strumieniowej pętla przetwarzająca fragmenty zazwyczaj ma blok try/catch do obsługi nieprawidłowych danych SSE (zły JSON, nieoczekiwane formaty). Ten generyczny blok catch pochłonie Twój błąd zabezpieczeń, jeśli nie będziesz ostrożny. Potrzebujesz klauzuli strażnika, która ponownie rzuci Twój specyficzny typ błędu przed uruchomieniem generycznego handlera:
catch (e) {
if (e instanceof Error && e.name === 'SystemPromptLeakError') throw e;
// generyczna obsługa błędów kontynuowana dla wszystkiego innego
}To jedna linijka, ale bez niej cały system wykrywania jest bezużyteczny. Strażnik uruchamia się, rejestruje wykrycie, a strumień kontynuuje szczęśliwie dostarczanie Twojego promptu systemowego atakującemu. Nauczyliśmy się tego na własnej skórze — nasz strażnik wykrywał wycieki idealnie w logach, nie robiąc absolutnie nic, aby je zatrzymać.
Etap 4 — Redakcja. Gdy błąd zostanie przekazany do kontrolera strumienia, wysyła on zdarzenie redact SSE do klienta. Klient zastępuje wszystko, co zostało już wyrenderowane, komunikatem odmowy. Serwer jednocześnie zastępuje przechowywaną treść w bazie danych, aby wyciek nie utrwalił się.
Co widzi użytkownik
Atakujący przez krótką chwilę widzi częściową treść strumieniowaną — może kilka sekund — a następnie cała odpowiedź zostaje zastąpiona generyczną odmową. Doświadczenie jest takie: tekst się pojawia, a następnie znika i zostaje zastąpiony. Częściowa treść, którą zobaczył, jest niekompletna i wymieszana z normalnym tekstem odpowiedzi, co czyni ją niewiarygodną do wydobycia.
Dowiedz się więcej o tym, jak działają komunikaty odmowy w Obsługa odmów i ograniczeń zakresu.
Problem z blokiem catch
To zasługuje na podkreślenie, ponieważ jest to rodzaj błędu, który przechodzi wszystkie testy, ale zawodzi w produkcji.
Jeśli używasz asynchronicznych generatorów do strumieniowania, Twoja pętla parsowania SSE prawdopodobnie wygląda tak:
for (const line of sseLines) {
try {
const data = JSON.parse(line);
const text = extractText(data);
await onChunkCallback(text); // <-- strażnik działa tutaj
yield text;
} catch (e) {
console.error('Error parsing chunk:', e);
// przechodzi do następnej linii
}
}Wywołanie zwrotne znajduje się wewnątrz bloku try. Jeśli strażnik rzuci błąd, catch rejestruje go jako błąd parsowania i przechodzi dalej. W naszym przypadku strażnik poprawnie wykrywał wyciek na każdym fragmencie po przekroczeniu progu — logi pokazywały, że SystemPromptLeakError był wielokrotnie uruchamiany — podczas gdy strumień kończył się normalnie, zapisując pełny wyciekły prompt w bazie danych i wysyłając go do klienta.
Dodatkowa komplikacja: to zachowanie zależy od środowiska wykonawczego. W Node.js błędy asynchronicznych generatorów z wywołań zwrotnych mogą propagować się inaczej niż w Deno. Nasze testy przeszły w środowisku testowym Node.js, ponieważ błąd przypadkowo się propagował. W produkcji Deno był pochłaniany. Jeśli budujesz to, testuj w swoim rzeczywistym środowisku produkcyjnym, a nie tylko w środowisku testowym.
Ważne decyzje projektowe
Dlaczego odciski palców zamiast podobieństwa osadzania lub dokładnego dopasowania? Odciski palców są szybkie (dopasowywanie ciągów znaków), deterministyczne (bez wywołań modelu) i odporne na parafrazowanie. Model rzadko parafrazuje swój własny prompt systemowy podczas wycieku — odtwarza go dosłownie lub prawie dosłownie. Podobieństwo osadzania dodaje opóźnienie na każde sprawdzenie i wprowadza ryzyko fałszywych pozytywów w przypadku legalnej treści zgodnościowej. Dokładne dopasowywanie podciągów jest zbyt kruche (różnice w białych znakach, formatowaniu).
Dlaczego sprawdzanie okresowe zamiast każdego fragmentu? Fragmenty są małe (często 3–10 znaków). Pojedynczy fragment jest bez znaczenia dla wykrywania. Akumulowanie do minimalnego progu przed sprawdzeniem zmniejsza obliczenia i zapewnia wystarczający kontekst dla niezawodnego dopasowania.
Dlaczego nie buforować całej odpowiedzi? Buforowanie zabija strumieniowe UX. Użytkownicy oczekują, że tekst pojawi się w czasie rzeczywistym. Dwusekundowy bufor jest zauważalny; buforowanie pełnej odpowiedzi o długości 4000+ znaków jest niedopuszczalne. Redakcja SSE zachowuje strumieniowanie w czasie rzeczywistym dla 99,99% rozmów i interweniuje tylko podczas aktywnego wycieku.
Dlaczego zastępować również w bazie danych? Jeśli zredagujesz tylko po stronie klienta, wyciekła treść pozostaje po stronie serwera. Każdy, kto ma dostęp do bazy danych, każda funkcja eksportu, każdy endpoint historii rozmów może ją ujawnić.
Czego to nie rozwiązuje
Redakcja SSE to środek obrony w głąb, a nie złoty środek.
Nie zapobiega próbie wycieku przez model. To obsługują same instrukcje Twojego promptu systemowego (jawne instrukcje odmowy, sekcje ograniczeń). Redakcja SSE to siatka bezpieczeństwa na wypadek, gdyby te instrukcje zawiodły — a przy wystarczającej kreatywności, jailbreaky czasami się udają.
Nie zapobiega wyciekom krótszym niż próg wykrywania. Jeśli ktoś namówi model do ujawnienia pojedynczego zdania promptu systemowego, liczba odcisków palców nie osiągnie progu. To jest celowe — równoważysz między wykrywaniem pełnych ekstrakcji (wysoka pewność) a oznaczanie częściowych wzmianek (wysokie ryzyko fałszywych pozytywów).
Atakujący widzi częściową treść przed redakcją. Przez kilka sekund strumieniowany tekst jest widoczny. To jest nieodłączne dla architektur strumieniowych. Częściowa treść jest niekompletna i pozbawiona struktury, ale nie jest to zerowa ekspozycja.
Redakcja SSE uzupełnia, ale nie zastępuje najlepszych praktyk bezpieczeństwa promptów systemowych. Zobacz Prompty systemowe i Ochrona przestrzeni roboczej i niestandardowych instrukcji w celu zapoznania się z podstawowymi środkami bezpieczeństwa.
Lista kontrolna implementacji
Jeśli chcesz zbudować to dla swojego własnego produktu LLM:
- Wyodrębnij frazy odcisków palców z Twojego promptu systemowego — wybierz charakterystyczne, obejmujące różne sekcje ciągi znaków.
- Zbuduj strażnika, który akumuluje strumieniowaną treść i okresowo sprawdza ją pod kątem odcisków palców.
- Zdefiniuj klasę błędu typowanego z charakterystyczną nazwą dla wykrywania wycieków.
- Przeprowadź audyt każdego bloku catch w Twoim potoku strumieniowania — dodaj strażniki ponownego rzucania dla Twojego typu błędu.
- W kontrolerze strumienia obsłuż błąd, wysyłając zdarzenie redact i zastępując przechowywaną treść.
- Po stronie klienta obsłuż zdarzenie redact, zastępując wyrenderowaną treść komunikatem odmowy.
- Testuj w swoim środowisku produkcyjnym, a nie tylko w środowisku testowym.
- Trzymaj swoje odciski palców, progi i interwały sprawdzania w tajemnicy.
Końcowa myśl
Najtrudniejszą częścią tego nie był algorytm wykrywania — to był jednolinijkowy błąd w bloku catch, który cicho wyłączał cały system. Bezpieczeństwo w architekturach strumieniowych zawodzi na poziomie instalacji, a nie algorytmu. Jeśli budujesz funkcje bezpieczeństwa LLM, prześledź pełną ścieżkę błędu od wykrycia do działania widocznego dla użytkownika i zweryfikuj ją w swoim rzeczywistym środowisku produkcyjnym.
Aby uzyskać szerszy obraz praktyk bezpieczeństwa AI w ISMS Copilot, zobacz Przegląd bezpieczeństwa AI i odpowiedzialnego użytkowania.
Better ISMS tworzy narzędzia zgodności dla zespołów ds. bezpieczeństwa informacji. ISMS Copilot to nasz asystent AI dla ISO 27001, SOC 2, RODO i powiązanych ram.