Предпазители: проверки преди и след модела
Накратко: Предпазителите (guardrails) са проверки в кода преди и след модела: входът се чисти, изходът се сверява с правилата, а при нарушение отива безопасен отговор и чернова за човек.
Проблем
Асистентът обеща 250 €
Клиентът е ядосан и пише номера на картата си направо в чата. Асистентът праща съобщението на модела и показва отговора:
var model = new FakeModel();var message = "1187 дойде счупена. Платих с карта 4111 1111 1111 1111, искам си парите!";Console.WriteLine($"клиент: {message}");var answer = model.Create(SystemPrompt, [new Message("user", message)]);Console.WriteLine($"отговор: {answer}");клиент: 1187 дойде счупена. Платих с карта 4111 1111 1111 1111, искам си парите!отговор: Съжаляваме! Ще ви върнем 250 € още днес.Две грешки, без нито един инструмент:
- Номерът на картата е отишъл при модела и е останал в записа на разговора. Записът се пази, копира и чете от колеги.
- Поръчка 1187 е за 89 €, а асистентът обещава 250 €, „още днес“. Пари връща само служител, а клиентът вече има обещанието писмено.
Моделът не е злонамерен. Той пише правдоподобен и учтив текст, а ядосан клиент, който иска парите си, звучи като „върнете му ги“.
Идеята
Проверки около модела
Предпазителите (guardrails) са код преди и след модела. Те не вярват на модела, както програмата не вярва на вход отвън:
- Проверка на входа, преди модела. Махаш това, което моделът не трябва да вижда: номер на карта, лични данни. Тук отказваш и прекалено дълги съобщения, които само струват токени.
- Проверка на изхода, след модела. Черновата се мери с правилата на магазина: без обещания за пари, само поръчки на този клиент, без сума над стойността на поръчката.
- Човек в цикъла. Ако правило не мине, клиентът получава безопасен отговор, а черновата отива при служител.
- Лог. Всяко решение се записва: какво е скрито, кое правило е спряло отговора. Без самите лични данни.
При съмнение програмата спира отговора, а не го пуска. Това е отказ при грешка (fail closed): по-добре клиентът да чака служител ден, отколкото да получи обещание, което магазинът не може да спази. Същото правило е в урока за недоверения вход.
Предпазителите не заменят prompt injection защитите, а ги допълват. Никой промпт не спира модела напълно, затова кодът около него ограничава щетите.
Виж го
Проверката на входа
Номерът на картата става **** 1111, а в лога пише защо:
// Преди модела: картата не стига до модела и до лога.static string CheckInput(string raw, List<string> log){ var text = Card.Replace(raw, "**** $1"); if (text == raw) { log.Add("вход: ок"); } else { log.Add("вход: скрит номер на карта"); } return text;}$1 в Replace е първата група в скобите: последните четири цифри.Данните на магазина и правилата са в началото на програмата:
static class Shop{ public const string SystemPrompt = "Ти си асистентът на поддръжката на онлайн магазин. Отговаряй кратко и само за поръчките на клиента."; public const string Customer = "C-17"; public const string Fallback = "Предадох въпроса на служител. Ще ви пише до един работен ден."; public static readonly Dictionary<string, Order> Orders = new() { ["1042"] = new("C-17", 45), ["1187"] = new("C-17", 89), ["2210"] = new("C-23", 120), }; public static readonly string[] Money = ["ще ви върнем", "ще ви възстановим"]; public static readonly Regex Card = new(@"\d{4} \d{4} \d{4} (\d{4})"); public static readonly Regex OrderId = new(@"(?<!\d)\d{4}(?!\d)"); public static readonly Regex Amount = new(@"(\d+) €"); public static readonly List<string> Review = [];}Виж го
Проверката на изхода
Черновата на модела срещу правилата. Всяко нарушение е ред в списък, а празен списък значи „мина“:
static bool IsOwn(string id) => Orders.TryGetValue(id, out var order) && order.Customer == Customer;// След модела: черновата срещу правилата на магазина.static List<string> CheckOutput(string draft, Order? order){ var problems = new List<string>(); var lower = draft.ToLowerInvariant(); foreach (var phrase in Money) { if (lower.Contains(phrase)) { problems.Add("обещава пари"); } } foreach (Match id in OrderId.Matches(draft)) { if (!IsOwn(id.Value)) { problems.Add($"{id.Value} не е ваша поръчка"); } } var limit = order?.Total ?? 0; foreach (Match m in Amount.Matches(draft)) { var sum = int.Parse(m.Groups[1].Value); if (sum > limit) { problems.Add($"{sum} € > {limit} €"); } } return problems;}Сумата се мери с поръчката, за която пита клиентът. Нея я намира orderIn, и то само ако е негова:
// Първата поръчка на клиента в текста (или никоя).static Order? OrderIn(string text){ foreach (Match id in OrderId.Matches(text)) { if (IsOwn(id.Value)) { return Orders[id.Value]; } } return null;}Виж го
Целият път
static void Handle(FakeModel model, string message){ var log = new List<string>(); var text = CheckInput(message, log); Console.WriteLine($"клиент: {text}"); var draft = model.Create(SystemPrompt, [new Message("user", text)]); var problems = CheckOutput(draft, OrderIn(text)); var answer = draft; if (problems.Count == 0) { log.Add("изход: ок"); } else { log.Add("изход: спрян — " + string.Join("; ", problems)); Review.Add(draft); answer = Fallback; } foreach (var line in log) { Console.WriteLine($" лог: {line}"); } Console.WriteLine($" отговор: {answer}");}// Преди модела: картата не стига до модела и до лога.static string CheckInput(string raw, List<string> log){ var text = Card.Replace(raw, "**** $1"); if (text == raw) { log.Add("вход: ок"); } else { log.Add("вход: скрит номер на карта"); } return text;}// След модела: черновата срещу правилата на магазина.static List<string> CheckOutput(string draft, Order? order){ var problems = new List<string>(); var lower = draft.ToLowerInvariant(); foreach (var phrase in Money) { if (lower.Contains(phrase)) { problems.Add("обещава пари"); } } foreach (Match id in OrderId.Matches(draft)) { if (!IsOwn(id.Value)) { problems.Add($"{id.Value} не е ваша поръчка"); } } var limit = order?.Total ?? 0; foreach (Match m in Amount.Matches(draft)) { var sum = int.Parse(m.Groups[1].Value); if (sum > limit) { problems.Add($"{sum} € > {limit} €"); } } return problems;}Стъпка по стъпка, с думи
- Клиентът пише, а в съобщението има номер на карта.
- Проверката на входа е преди модела: картата става **** 1111, а логът казва защо.
- Моделът получава само скрития текст. Черновата му: „Съжаляваме! Ще ви върнем 250 € още днес.“
- Проверката на изхода мери черновата с правилата на магазина: обещава пари; 250 € > 89 €.
- Правило не мина: клиентът получава безопасния отговор, а черновата отива при служител.
- Логът пази всяко решение: какво е скрито и кое правило спря отговора. Без картата.
static void Handle(FakeModel model, string message){ var log = new List<string>(); var text = CheckInput(message, log); Console.WriteLine($"клиент: {text}"); var draft = model.Create(SystemPrompt, [new Message("user", text)]); var problems = CheckOutput(draft, OrderIn(text)); var answer = draft; if (problems.Count == 0) { log.Add("изход: ок"); } else { log.Add("изход: спрян — " + string.Join("; ", problems)); Review.Add(draft); answer = Fallback; } foreach (var line in log) { Console.WriteLine($" лог: {line}"); } Console.WriteLine($" отговор: {answer}");}var model = new FakeModel();Handle(model, "Кога пристига 1042?");Handle(model, "1187 дойде счупена. Платих с карта 4111 1111 1111 1111, искам си парите!");Handle(model, "Къде е поръчка 2210?");Console.WriteLine("за служител:");foreach (var draft in Review){ Console.WriteLine($" {draft}");}клиент: Кога пристига 1042? лог: вход: ок лог: изход: ок отговор: Поръчка 1042 е изпратена и пристига на 03.11.клиент: 1187 дойде счупена. Платих с карта **** 1111, искам си парите! лог: вход: скрит номер на карта лог: изход: спрян — обещава пари; 250 € > 89 € отговор: Предадох въпроса на служител. Ще ви пише до един работен ден.клиент: Къде е поръчка 2210? лог: вход: ок лог: изход: спрян — 2210 не е ваша поръчка отговор: Предадох въпроса на служител. Ще ви пише до един работен ден.за служител: Съжаляваме! Ще ви върнем 250 € още днес. Поръчка 2210 е доставена до адреса в Пловдив.Първото съобщение минава през двете проверки без промяна. Второто губи картата преди модела и черновата му е спряна след него. В третото моделът казва нещо за чужда поръчка: такъв текст е теч на чужди данни, дори да е верен.
Моделът тук е фалшив клиент като в предишните уроци: по ключова дума в съобщението връща записан отговор. Отговорите са в „По-точно“.
Пробвай
Попълни
Правило не мина. Какво отива при служителя?
Review.Add();
answer = Fallback;draft, черновата на модела. Служителят вижда какво е искал да каже моделът и решава: да върне парите, да пише сам или да поправи правилото. Клиентът получава FALLBACK, а не черновата.
Пробвай
Предвиди изхода
Клиентът пита за отстъпка. Какво печата handle?
var 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 readonly (string Key, string Answer)[] Answers = [ ("счупена", "Съжаляваме! Ще ви върнем 250 € още днес."), ("2210", "Поръчка 2210 е доставена до адреса в Пловдив."), ("отстъпка", "Да, за 1042 имате 10 € отстъпка."), ("1042", "Поръчка 1042 е изпратена и пристига на 03.11."), ];
public string Create(string system, List<Message> messages) { var text = messages[^1].Content; foreach (var (key, answer) in Answers) { if (text.Contains(key)) { return answer; } } return "Ще проверя и ще ви пиша."; }}Упражнение
Правилото за отстъпка
Реши задачата в редактора си.
Добави в checkOutput правило: ако черновата споменава „отстъпка“, това е нарушение „обещава отстъпка“. Отстъпките ги дава маркетингът, не асистентът. Съобщението от „Предвиди изхода“ вече не минава:
клиент: Обещахте ми отстъпка за 1042. Колко е? лог: вход: ок лог: изход: спрян — обещава отстъпка отговор: Предадох въпроса на служител. Ще ви пише до един работен ден.Още задачи
- Дълго съобщение. В
checkInputоткажи съобщение с повече от 80 думи, без да го пращаш на модела: клиентът получава „Моля, напишете накратко какво се е случило“, а в лога пише защо. - Моделът мълчи. Направи фалшивия модел да хвърля изключение при думата „бавно“.
handleтрябва да даде безопасния отговор и да запише грешката в лога, вместо програмата да спре.
Тест
Провери се
Свързани уроци
Раздел „Свързани уроци“- Нужно отпреди: Инструменти: моделът пита, програмата прави · Оценяване: тестов набор за асистента · Цикълът на агента: план, действие, наблюдение · Prompt injection: писмото, което дава нареждания
- Продължава в: Асистентът на поддръжката
- Задачи по темата: Агенти