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

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

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

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

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

Пробвай

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

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

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

Идеята

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

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

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

Пробвай

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

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

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

Идеята

Кога вреди

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

Тест

Провери се

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

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