Предпазители: проверки преди и след модела
Накратко: Предпазителите (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) са код преди и след модела. Те не вярват на модела, както програмата не вярва на вход отвън:
- Проверка на входа, преди модела. Махаш това, което моделът не трябва да вижда: номер на карта, лични данни. Тук отказваш и прекалено дълги съобщения, които само струват токени.
- Проверка на изхода, след модела. Черновата се мери с правилата на магазина: без обещания за пари, само поръчки на този клиент, без сума над стойността на поръчката.
- Човек в цикъла. Ако правило не мине, клиентът получава безопасен отговор, а черновата отива при служител.
- Лог. Всяко решение се записва: какво е скрито, кое правило е спряло отговора. Без самите лични данни.
При съмнение програмата спира отговора, а не го пуска. Това е отказ при грешка (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);}// Преди модела: картата не стига до модела и до лога.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;}// След модела: черновата срещу правилата на магазина.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;}Стъпка по стъпка, с думи
- Клиентът пише, а в съобщението има номер на карта.
- Проверката на входа е преди модела: картата става **** 1111, а логът казва защо.
- Моделът получава само скрития текст. Черновата му: „Съжаляваме! Ще ви върнем 250 € още днес.“
- Проверката на изхода мери черновата с правилата на магазина: обещава пари; 250 € > 89 €.
- Правило не мина: клиентът получава безопасния отговор, а черновата отива при служител.
- Логът пази всяко решение: какво е скрито и кое правило спря отговора. Без картата.
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;draft, черновата на модела. Служителят вижда какво е искал да каже моделът и решава: да върне парите, да пише сам или да поправи правилото. Клиентът получава FALLBACK, а не черновата.
Пробвай
Предвиди изхода
Клиентът пита за отстъпка. Какво печата handle?
FakeModel model = new FakeModel();handle(model, "Обещахте ми отстъпка за 1042. Колко е?");Минава. Черновата не обещава връщане на пари, поръчка 1042 е на клиента, а 10 € е под 45 €. Правилата не знаят нищо за отстъпки, затова не я спират. Магазинът обаче не е обещавал отстъпка: моделът я е измислил по думите на клиента.
Идеята
Правилата хващат само описаното
Предпазителят е списък от известни опасности. Новата грешка минава, докато някой не я опише. Затова предпазителите са един слой от няколко:
- Инструменти без опасни права. Агентът от урока за цикъла не може да върне пари, а само отваря заявка, която чака служител. Каквото и да напише моделът, парите не мърдат.
- Структуриран изход. Отговор като JSON по схема (урокът за него) се проверява по-точно от свободен текст: полето
refundсе вижда, а изречението „ще ви върнем“ има хиляда форми. - Тестов набор. Всяка грешка, хваната в лога, става случай в тестовия набор, а новото правило се пуска срещу него.
- Когато моделът не отговаря. Изтекло време или грешка от доставчика водят до същия безопасен отговор. Чакането и повторните опити са в урока за устойчивостта.
Пробвай
Намери грешката
Колега пише handle. Проверките работят, а в записа на разговора пак се вижда целият номер на картата. На кой ред е грешката?
Избери реда с грешката.
Записът печата message, суровото съобщение, а не text, скритото. Проверката на входа е направила своето, но програмата е записала оригинала покрай нея. Всичко след проверката на входа трябва да ползва само нейния резултат.
Идеята
Кога вреди
- Прекалено строги правила. Правило „без суми“ спира и „доставката е 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трябва да даде безопасния отговор и да запише грешката в лога, вместо програмата да спре.
Тест
Провери се
Свързани уроци
Раздел „Свързани уроци“- Нужно отпреди: Инструменти: моделът пита, програмата прави · Оценяване: тестов набор за асистента · Цикълът на агента: план, действие, наблюдение · Prompt injection: писмото, което дава нареждания
- Продължава в: Асистентът на поддръжката
- Задачи по темата: Агенти