Предпазители: проверки преди и след модела
Накратко: Предпазителите (guardrails) са проверки в кода преди и след модела: входът се чисти, изходът се сверява с правилата, а при нарушение отива безопасен отговор и чернова за човек.
Проблем
Асистентът обеща 250 €
Клиентът е ядосан и пише номера на картата си направо в чата. Асистентът праща съобщението на модела и показва отговора:
const model = new FakeModel();const message = "1187 дойде счупена. Платих с карта 4111 1111 1111 1111, искам си парите!";console.log(`клиент: ${message}`);const answer = model.create(SYSTEM, [{ role: "user", content: message }]);console.log(`отговор: ${answer}`);клиент: 1187 дойде счупена. Платих с карта 4111 1111 1111 1111, искам си парите!отговор: Съжаляваме! Ще ви върнем 250 € още днес.Две грешки, без нито един инструмент:
- Номерът на картата е отишъл при модела и е останал в записа на разговора. Записът се пази, копира и чете от колеги.
- Поръчка 1187 е за 89 €, а асистентът обещава 250 €, „още днес“. Пари връща само служител, а клиентът вече има обещанието писмено.
Моделът не е злонамерен. Той пише правдоподобен и учтив текст, а ядосан клиент, който иска парите си, звучи като „върнете му ги“.
Идеята
Проверки около модела
Предпазителите (guardrails) са код преди и след модела. Те не вярват на модела, както програмата не вярва на вход отвън:
- Проверка на входа, преди модела. Махаш това, което моделът не трябва да вижда: номер на карта, лични данни. Тук отказваш и прекалено дълги съобщения, които само струват токени.
- Проверка на изхода, след модела. Черновата се мери с правилата на магазина: без обещания за пари, само поръчки на този клиент, без сума над стойността на поръчката.
- Човек в цикъла. Ако правило не мине, клиентът получава безопасен отговор, а черновата отива при служител.
- Лог. Всяко решение се записва: какво е скрито, кое правило е спряло отговора. Без самите лични данни.
При съмнение програмата спира отговора, а не го пуска. Това е отказ при грешка (fail closed): по-добре клиентът да чака служител ден, отколкото да получи обещание, което магазинът не може да спази. Същото правило е в урока за недоверения вход.
Предпазителите не заменят prompt injection защитите, а ги допълват. Никой промпт не спира модела напълно, затова кодът около него ограничава щетите.
Виж го
Проверката на входа
Номерът на картата става **** 1111, а в лога пише защо:
// Преди модела: картата не стига до модела и до лога.function checkInput(raw, log) { const text = raw.replace(CARD, "**** $1"); if (text === raw) { log.push("вход: ок"); } else { log.push("вход: скрит номер на карта"); } return text;}$1 в replace е първата група в скобите: последните четири цифри. Регулярният израз е с g, за да скрие всяка карта, не само първата.Данните на магазина и правилата са в началото на програмата:
const SYSTEM = "Ти си асистентът на поддръжката на онлайн магазин. Отговаряй кратко и само за поръчките на клиента.";const CUSTOMER = "C-17";const FALLBACK = "Предадох въпроса на служител. Ще ви пише до един работен ден.";const ORDERS = { "1042": { customer: "C-17", total: 45 }, "1187": { customer: "C-17", total: 89 }, "2210": { customer: "C-23", total: 120 },};const MONEY = ["ще ви върнем", "ще ви възстановим"];const CARD = /\d{4} \d{4} \d{4} (\d{4})/g;const ORDER_ID = /(?<!\d)\d{4}(?!\d)/g;const AMOUNT = /(\d+) €/g;const REVIEW = [];Виж го
Проверката на изхода
Черновата на модела срещу правилата. Всяко нарушение е ред в списък, а празен списък значи „мина“:
function isOwn(id) { const order = ORDERS[id]; return order !== undefined && order.customer === CUSTOMER;}// След модела: черновата срещу правилата на магазина.function checkOutput(draft, order) { const problems = []; const lower = draft.toLowerCase(); for (const phrase of MONEY) { if (lower.includes(phrase)) { problems.push("обещава пари"); } } for (const [id] of draft.matchAll(ORDER_ID)) { if (!isOwn(id)) { problems.push(`${id} не е ваша поръчка`); } } const limit = order === undefined ? 0 : order.total; for (const [, digits] of draft.matchAll(AMOUNT)) { const sum = Number(digits); if (sum > limit) { problems.push(`${sum} € > ${limit} €`); } } return problems;}Сумата се мери с поръчката, за която пита клиентът. Нея я намира orderIn, и то само ако е негова:
// Първата поръчка на клиента в текста (или никоя).function orderIn(text) { for (const [id] of text.matchAll(ORDER_ID)) { if (isOwn(id)) return ORDERS[id]; } return undefined;}Виж го
Целият път
function handle(model, message) { const log = []; const text = checkInput(message, log); console.log(`клиент: ${text}`); const messages = [{ role: "user", content: text }]; const draft = model.create(SYSTEM, messages); const problems = checkOutput(draft, orderIn(text)); let answer = draft; if (problems.length === 0) { log.push("изход: ок"); } else { log.push(`изход: спрян — ${problems.join("; ")}`); REVIEW.push(draft); answer = FALLBACK; } for (const line of log) { console.log(` лог: ${line}`); } console.log(` отговор: ${answer}`);}// Преди модела: картата не стига до модела и до лога.function checkInput(raw, log) { const text = raw.replace(CARD, "**** $1"); if (text === raw) { log.push("вход: ок"); } else { log.push("вход: скрит номер на карта"); } return text;}// След модела: черновата срещу правилата на магазина.function checkOutput(draft, order) { const problems = []; const lower = draft.toLowerCase(); for (const phrase of MONEY) { if (lower.includes(phrase)) { problems.push("обещава пари"); } } for (const [id] of draft.matchAll(ORDER_ID)) { if (!isOwn(id)) { problems.push(`${id} не е ваша поръчка`); } } const limit = order === undefined ? 0 : order.total; for (const [, digits] of draft.matchAll(AMOUNT)) { const sum = Number(digits); if (sum > limit) { problems.push(`${sum} € > ${limit} €`); } } return problems;}Стъпка по стъпка, с думи
- Клиентът пише, а в съобщението има номер на карта.
- Проверката на входа е преди модела: картата става **** 1111, а логът казва защо.
- Моделът получава само скрития текст. Черновата му: „Съжаляваме! Ще ви върнем 250 € още днес.“
- Проверката на изхода мери черновата с правилата на магазина: обещава пари; 250 € > 89 €.
- Правило не мина: клиентът получава безопасния отговор, а черновата отива при служител.
- Логът пази всяко решение: какво е скрито и кое правило спря отговора. Без картата.
function handle(model, message) { const log = []; const text = checkInput(message, log); console.log(`клиент: ${text}`); const messages = [{ role: "user", content: text }]; const draft = model.create(SYSTEM, messages); const problems = checkOutput(draft, orderIn(text)); let answer = draft; if (problems.length === 0) { log.push("изход: ок"); } else { log.push(`изход: спрян — ${problems.join("; ")}`); REVIEW.push(draft); answer = FALLBACK; } for (const line of log) { console.log(` лог: ${line}`); } console.log(` отговор: ${answer}`);}const model = new FakeModel();handle(model, "Кога пристига 1042?");handle(model, "1187 дойде счупена. Платих с карта 4111 1111 1111 1111, искам си парите!");handle(model, "Къде е поръчка 2210?");console.log("за служител:");for (const draft of REVIEW) { console.log(` ${draft}`);}клиент: Кога пристига 1042? лог: вход: ок лог: изход: ок отговор: Поръчка 1042 е изпратена и пристига на 03.11.клиент: 1187 дойде счупена. Платих с карта **** 1111, искам си парите! лог: вход: скрит номер на карта лог: изход: спрян — обещава пари; 250 € > 89 € отговор: Предадох въпроса на служител. Ще ви пише до един работен ден.клиент: Къде е поръчка 2210? лог: вход: ок лог: изход: спрян — 2210 не е ваша поръчка отговор: Предадох въпроса на служител. Ще ви пише до един работен ден.за служител: Съжаляваме! Ще ви върнем 250 € още днес. Поръчка 2210 е доставена до адреса в Пловдив.Първото съобщение минава през двете проверки без промяна. Второто губи картата преди модела и черновата му е спряна след него. В третото моделът казва нещо за чужда поръчка: такъв текст е теч на чужди данни, дори да е верен.
Моделът тук е фалшив клиент като в предишните уроци: по ключова дума в съобщението връща записан отговор. Отговорите са в „По-точно“.
Пробвай
Попълни
Правило не мина. Какво отива при служителя?
REVIEW.push();
answer = FALLBACK;draft, черновата на модела. Служителят вижда какво е искал да каже моделът и решава: да върне парите, да пише сам или да поправи правилото. Клиентът получава FALLBACK, а не черновата.
Пробвай
Предвиди изхода
Клиентът пита за отстъпка. Какво печата handle?
const 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 ANSWERS = [ ["счупена", "Съжаляваме! Ще ви върнем 250 € още днес."], ["2210", "Поръчка 2210 е доставена до адреса в Пловдив."], ["отстъпка", "Да, за 1042 имате 10 € отстъпка."], ["1042", "Поръчка 1042 е изпратена и пристига на 03.11."], ];
create(system, messages) { const text = messages[messages.length - 1].content; for (const [key, answer] of FakeModel.ANSWERS) { if (text.includes(key)) return answer; } return "Ще проверя и ще ви пиша."; }}Упражнение
Правилото за отстъпка
Реши задачата в редактора си.
Добави в checkOutput правило: ако черновата споменава „отстъпка“, това е нарушение „обещава отстъпка“. Отстъпките ги дава маркетингът, не асистентът. Съобщението от „Предвиди изхода“ вече не минава:
клиент: Обещахте ми отстъпка за 1042. Колко е? лог: вход: ок лог: изход: спрян — обещава отстъпка отговор: Предадох въпроса на служител. Ще ви пише до един работен ден.Още задачи
- Дълго съобщение. В
checkInputоткажи съобщение с повече от 80 думи, без да го пращаш на модела: клиентът получава „Моля, напишете накратко какво се е случило“, а в лога пише защо. - Моделът мълчи. Направи фалшивия модел да хвърля изключение при думата „бавно“.
handleтрябва да даде безопасния отговор и да запише грешката в лога, вместо програмата да спре.
Тест
Провери се
Свързани уроци
Раздел „Свързани уроци“- Нужно отпреди: Инструменти: моделът пита, програмата прави · Оценяване: тестов набор за асистента · Цикълът на агента: план, действие, наблюдение · Prompt injection: писмото, което дава нареждания
- Продължава в: Асистентът на поддръжката
- Задачи по темата: Агенти