Към съдържанието
wannacode

Предпазители: проверки преди и след модела

Накратко: Предпазителите (guardrails) са проверки в кода преди и след модела: входът се чисти, изходът се сверява с правилата, а при нарушение отива безопасен отговор и чернова за човек.

Проблем

Асистентът обеща 250 €

Клиентът е ядосан и пише номера на картата си направо в чата. Асистентът праща съобщението на модела и показва отговора:

FakeModel model = new FakeModel();
String message = "1187 дойде счупена. Платих с карта 4111 1111 1111 1111, искам си парите!";
System.out.println("клиент: " + message);
String answer = model.create(SYSTEM,
List.of(new Message("user", message)));
System.out.println("отговор: " + answer);
Изход
клиент: 1187 дойде счупена. Платих с карта 4111 1111 1111 1111, искам си парите!
отговор: Съжаляваме! Ще ви върнем 250 € още днес.

Две грешки, без нито един инструмент:

  • Номерът на картата е отишъл при модела и е останал в записа на разговора. Записът се пази, копира и чете от колеги.
  • Поръчка 1187 е за 89 €, а асистентът обещава 250 €, „още днес“. Пари връща само служител, а клиентът вече има обещанието писмено.

Моделът не е злонамерен. Той пише правдоподобен и учтив текст, а ядосан клиент, който иска парите си, звучи като „върнете му ги“.

Идеята

Проверки около модела

Предпазителите (guardrails) са код преди и след модела. Те не вярват на модела, както програмата не вярва на вход отвън:

  1. Проверка на входа, преди модела. Махаш това, което моделът не трябва да вижда: номер на карта, лични данни. Тук отказваш и прекалено дълги съобщения, които само струват токени.
  2. Проверка на изхода, след модела. Черновата се мери с правилата на магазина: без обещания за пари, само поръчки на този клиент, без сума над стойността на поръчката.
  3. Човек в цикъла. Ако правило не мине, клиентът получава безопасен отговор, а черновата отива при служител.
  4. Лог. Всяко решение се записва: какво е скрито, кое правило е спряло отговора. Без самите лични данни.

При съмнение програмата спира отговора, а не го пуска. Това е отказ при грешка (fail closed): по-добре клиентът да чака служител ден, отколкото да получи обещание, което магазинът не може да спази. Същото правило е в урока за недоверения вход.

Предпазителите не заменят prompt injection защитите, а ги допълват. Никой промпт не спира модела напълно, затова кодът около него ограничава щетите.

Виж го

Проверката на входа

Номерът на картата става **** 1111, а в лога пише защо:

// Преди модела: картата не стига до модела и до лога.
static String checkInput(String raw, List<String> log) {
String text = CARD.matcher(raw).replaceAll("**** $1");
if (text.equals(raw)) {
log.add("вход: ок");
} else {
log.add("вход: скрит номер на карта");
}
return text;
}
$1 в replaceAll е първата група в скобите: последните четири цифри.

Данните на магазина и правилата са в началото на програмата:

static final String SYSTEM = "Ти си асистентът на поддръжката на онлайн магазин. Отговаряй кратко и само за поръчките на клиента.";
static final String CUSTOMER = "C-17";
static final String FALLBACK = "Предадох въпроса на служител. Ще ви пише до един работен ден.";
static final Map<String, Order> ORDERS = Map.of(
"1042", new Order("C-17", 45),
"1187", new Order("C-17", 89),
"2210", new Order("C-23", 120));
static final List<String> MONEY = List.of("ще ви върнем", "ще ви възстановим");
static final Pattern CARD = Pattern.compile("\\d{4} \\d{4} \\d{4} (\\d{4})");
static final Pattern ORDER_ID = Pattern.compile("(?<!\\d)\\d{4}(?!\\d)");
static final Pattern AMOUNT = Pattern.compile("(\\d+) €");
static final List<String> REVIEW = new ArrayList<>();

Виж го

Проверката на изхода

Черновата на модела срещу правилата. Всяко нарушение е ред в списък, а празен списък значи „мина“:

static boolean isOwn(String id) {
Order order = ORDERS.get(id);
return order != null && order.customer().equals(CUSTOMER);
}
// След модела: черновата срещу правилата на магазина.
static List<String> checkOutput(String draft, Order order) {
List<String> problems = new ArrayList<>();
String lower = draft.toLowerCase();
for (String phrase : MONEY) {
if (lower.contains(phrase)) {
problems.add("обещава пари");
}
}
Matcher ids = ORDER_ID.matcher(draft);
while (ids.find()) {
String id = ids.group();
if (!isOwn(id)) {
problems.add(id + " не е ваша поръчка");
}
}
int limit = order == null ? 0 : order.total();
Matcher sums = AMOUNT.matcher(draft);
while (sums.find()) {
int sum = Integer.parseInt(sums.group(1));
if (sum > limit) {
problems.add(sum + " € > " + limit + " €");
}
}
return problems;
}

Сумата се мери с поръчката, за която пита клиентът. Нея я намира orderIn, и то само ако е негова:

// Първата поръчка на клиента в текста (или никоя).
static Order orderIn(String text) {
Matcher ids = ORDER_ID.matcher(text);
while (ids.find()) {
if (isOwn(ids.group())) {
return ORDERS.get(ids.group());
}
}
return null;
}

Виж го

Целият път

static void handle(FakeModel model, String message) {
List<String> log = new ArrayList<>();
String text = checkInput(message, log);
System.out.println("клиент: " + text);
String draft = model.create(SYSTEM,
List.of(new Message("user", text)));
List<String> problems =
checkOutput(draft, orderIn(text));
String answer = draft;
if (problems.isEmpty()) {
log.add("изход: ок");
} else {
log.add("изход: спрян — "
+ String.join("; ", problems));
REVIEW.add(draft);
answer = FALLBACK;
}
for (String line : log) {
System.out.println(" лог: " + line);
}
System.out.println(" отговор: " + answer);
}
Стъпка по стъпка, с думи
  1. Клиентът пише, а в съобщението има номер на карта.
  2. Проверката на входа е преди модела: картата става **** 1111, а логът казва защо.
  3. Моделът получава само скрития текст. Черновата му: „Съжаляваме! Ще ви върнем 250 € още днес.“
  4. Проверката на изхода мери черновата с правилата на магазина: обещава пари; 250 € > 89 €.
  5. Правило не мина: клиентът получава безопасния отговор, а черновата отива при служител.
  6. Логът пази всяко решение: какво е скрито и кое правило спря отговора. Без картата.
static void handle(FakeModel model, String message) {
List<String> log = new ArrayList<>();
String text = checkInput(message, log);
System.out.println("клиент: " + text);
String draft = model.create(SYSTEM,
List.of(new Message("user", text)));
List<String> problems =
checkOutput(draft, orderIn(text));
String answer = draft;
if (problems.isEmpty()) {
log.add("изход: ок");
} else {
log.add("изход: спрян — "
+ String.join("; ", problems));
REVIEW.add(draft);
answer = FALLBACK;
}
for (String line : log) {
System.out.println(" лог: " + line);
}
System.out.println(" отговор: " + answer);
}
FakeModel model = new FakeModel();
handle(model, "Кога пристига 1042?");
handle(model, "1187 дойде счупена. Платих с карта 4111 1111 1111 1111, искам си парите!");
handle(model, "Къде е поръчка 2210?");
System.out.println("за служител:");
for (String draft : REVIEW) {
System.out.println(" " + draft);
}
Изход
клиент: Кога пристига 1042?
лог: вход: ок
лог: изход: ок
отговор: Поръчка 1042 е изпратена и пристига на 03.11.
клиент: 1187 дойде счупена. Платих с карта **** 1111, искам си парите!
лог: вход: скрит номер на карта
лог: изход: спрян — обещава пари; 250 € > 89 €
отговор: Предадох въпроса на служител. Ще ви пише до един работен ден.
клиент: Къде е поръчка 2210?
лог: вход: ок
лог: изход: спрян — 2210 не е ваша поръчка
отговор: Предадох въпроса на служител. Ще ви пише до един работен ден.
за служител:
Съжаляваме! Ще ви върнем 250 € още днес.
Поръчка 2210 е доставена до адреса в Пловдив.

Първото съобщение минава през двете проверки без промяна. Второто губи картата преди модела и черновата му е спряна след него. В третото моделът казва нещо за чужда поръчка: такъв текст е теч на чужди данни, дори да е верен.

Моделът тук е фалшив клиент като в предишните уроци: по ключова дума в съобщението връща записан отговор. Отговорите са в „По-точно“.

Пробвай

Попълни

Правило не мина. Какво отива при служителя?

REVIEW.add();
answer = FALLBACK;

Пробвай

Предвиди изхода

Клиентът пита за отстъпка. Какво печата handle?

FakeModel model = new FakeModel();
handle(model, "Обещахте ми отстъпка за 1042. Колко е?");
Отговор

Идеята

Правилата хващат само описаното

Предпазителят е списък от известни опасности. Новата грешка минава, докато някой не я опише. Затова предпазителите са един слой от няколко:

  • Инструменти без опасни права. Агентът от урока за цикъла не може да върне пари, а само отваря заявка, която чака служител. Каквото и да напише моделът, парите не мърдат.
  • Структуриран изход. Отговор като JSON по схема (урокът за него) се проверява по-точно от свободен текст: полето refund се вижда, а изречението „ще ви върнем“ има хиляда форми.
  • Тестов набор. Всяка грешка, хваната в лога, става случай в тестовия набор, а новото правило се пуска срещу него.
  • Когато моделът не отговаря. Изтекло време или грешка от доставчика водят до същия безопасен отговор. Чакането и повторните опити са в урока за устойчивостта.

Пробвай

Намери грешката

Колега пише handle. Проверките работят, а в записа на разговора пак се вижда целият номер на картата. На кой ред е грешката?

Избери реда с грешката.

Идеята

Кога вреди

  • Прекалено строги правила. Правило „без суми“ спира и „доставката е 4,99 €“. Клиентите получават „ще ви пише служител“ за всичко, а служителите се давят. Мери колко отговора спира всяко правило и кои от тях са били верни.
  • Предпазител вместо права. Проверката на изхода не прави опасен инструмент безопасен. Ако агентът може да върне пари, текстова проверка няма да го спре навреме. Правата се режат в инструмента.
  • Лог с лични данни. Логът, който пази целия разговор, е втора база с карти и адреси. Записвай решенията и причините, а личното скривай преди лога, както преди модела.
  • Проверка с друг модел за всичко. Втори модел, който оценява отговора, хваща повече, но струва още едно извикване и закъснение на всяко съобщение. Пусни първо проверките с код, а модела само за това, което те не могат.
  • Никой не чете лога. Предпазителите се поправят от хората, които виждат спрените отговори. Наблюдението на живо приложение (табла, аларми) е част от ZeroToOps.
По-точно, за любопитните

Номерът на карта отвътре. Изразът тук хваща само четири групи по четири цифри с интервали. Истинските номера са от 13 до 19 цифри, с тирета или слети, а проверката на Лун (Luhn) различава номер на карта от случайно число. Тайни като ключове и пароли изобщо не влизат в разговора: те са в средата, не в кода (урокът за тайните).

Обещанията. Списъкът MONEY е две фрази. Моделът може да напише „ще получите парите си“ и да мине. Затова на практика проверката на обещания често е класификатор (малък модел или втори модел със схема), а правилата с код остават за това, което се мери точно: суми, номера на поръчки, дати.

Готови предпазители. Доставчиците и библиотеките предлагат проверки за обидно съдържание, лични данни и injection. Те са същият слой: преди и след модела, с лог. Бизнес правилата на магазина (кой клиент, коя поръчка, колко €) обаче знае само твоят код.

Фалшивият модел отвътре. Връща първия записан отговор, чиято ключова дума е в съобщението, иначе „Ще проверя и ще ви пиша.“.

// Стои на мястото на истинския модел: записани отговори,
// без мрежа и всеки път едни и същи.
class FakeModel {
static final String[][] ANSWERS = {
{"счупена", "Съжаляваме! Ще ви върнем 250 € още днес."},
{"2210", "Поръчка 2210 е доставена до адреса в Пловдив."},
{"отстъпка", "Да, за 1042 имате 10 € отстъпка."},
{"1042", "Поръчка 1042 е изпратена и пристига на 03.11."},
};
String create(String system, List<Message> messages) {
String text = messages.get(messages.size() - 1).content();
for (String[] answer : ANSWERS) {
if (text.contains(answer[0])) {
return answer[1];
}
}
return "Ще проверя и ще ви пиша.";
}
}

Упражнение

Правилото за отстъпка

Реши задачата в редактора си.

Добави в checkOutput правило: ако черновата споменава „отстъпка“, това е нарушение „обещава отстъпка“. Отстъпките ги дава маркетингът, не асистентът. Съобщението от „Предвиди изхода“ вече не минава:

Изход
клиент: Обещахте ми отстъпка за 1042. Колко е?
лог: вход: ок
лог: изход: спрян — обещава отстъпка
отговор: Предадох въпроса на служител. Ще ви пише до един работен ден.

Още задачи

  • Дълго съобщение. В checkInput откажи съобщение с повече от 80 думи, без да го пращаш на модела: клиентът получава „Моля, напишете накратко какво се е случило“, а в лога пише защо.
  • Моделът мълчи. Направи фалшивия модел да хвърля изключение при думата „бавно“. handle трябва да даде безопасния отговор и да запише грешката в лога, вместо програмата да спре.

Тест

Провери се

1. Защо номерът на картата се скрива преди модела, а не след него?
2. Проверката на изхода намери „обещава пари“. Какво получава клиентът?
3. „Да, за 1042 имате 10 € отстъпка.“ минава през проверките. Защо?
4. Какво трябва да пише в лога за второто съобщение?

Създаден от Иво Гергов като част от кампанията на Evo Tech за достъпно обучение по програмиране ·