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

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

Накратко: Предпазителите (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) са код преди и след модела. Те не вярват на модела, както програмата не вярва на вход отвън:

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

При съмнение програмата спира отговора, а не го пуска. Това е отказ при грешка (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}`);
}
Стъпка по стъпка, с думи
  1. Клиентът пише, а в съобщението има номер на карта.
  2. Проверката на входа е преди модела: картата става **** 1111, а логът казва защо.
  3. Моделът получава само скрития текст. Черновата му: „Съжаляваме! Ще ви върнем 250 € още днес.“
  4. Проверката на изхода мери черновата с правилата на магазина: обещава пари; 250 € > 89 €.
  5. Правило не мина: клиентът получава безопасния отговор, а черновата отива при служител.
  6. Логът пази всяко решение: какво е скрито и кое правило спря отговора. Без картата.
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;

Пробвай

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

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

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

Идеята

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

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

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

Пробвай

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

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

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

Идеята

Кога вреди

  • Прекалено строги правила. Правило „без суми“ спира и „доставката е 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 трябва да даде безопасния отговор и да запише грешката в лога, вместо програмата да спре.

Тест

Провери се

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

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