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

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

Накратко: Предпазителите (guardrails) са проверки в кода преди и след модела: входът се чисти, изходът се сверява с правилата, а при нарушение отива безопасен отговор и чернова за човек.

Проблем

Асистентът обеща 250 €

Клиентът е ядосан и пише номера на картата си направо в чата. Асистентът праща съобщението на модела и показва отговора:

model = FakeModel()
message = "1187 дойде счупена. Платих с карта 4111 1111 1111 1111, искам си парите!"
print(f"клиент: {message}")
answer = model.create(SYSTEM, [{"role": "user", "content": message}])
print(f"отговор: {answer}")
Изход
клиент: 1187 дойде счупена. Платих с карта 4111 1111 1111 1111, искам си парите!
отговор: Съжаляваме! Ще ви върнем 250 € още днес.

Две грешки, без нито един инструмент:

  • Номерът на картата е отишъл при модела и е останал в записа на разговора. Записът се пази, копира и чете от колеги.
  • Поръчка 1187 е за 89 €, а асистентът обещава 250 €, „още днес“. Пари връща само служител, а клиентът вече има обещанието писмено.

Моделът не е злонамерен. Той пише правдоподобен и учтив текст, а ядосан клиент, който иска парите си, звучи като „върнете му ги“.

Идеята

Проверки около модела

Предпазителите (guardrails) са код преди и след модела. Те не вярват на модела, както програмата не вярва на вход отвън:

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

При съмнение програмата спира отговора, а не го пуска. Това е отказ при грешка (fail closed): по-добре клиентът да чака служител ден, отколкото да получи обещание, което магазинът не може да спази. Същото правило е в урока за недоверения вход.

Предпазителите не заменят prompt injection защитите, а ги допълват. Никой промпт не спира модела напълно, затова кодът около него ограничава щетите.

Виж го

Проверката на входа

Номерът на картата става **** 1111, а в лога пише защо:

# Преди модела: картата не стига до модела и до лога.
def check_input(raw, log):
text = CARD.sub(r"**** \1", raw)
if text == raw:
log.append("вход: ок")
else:
log.append("вход: скрит номер на карта")
return text
\1 в sub е първата група в скобите: последните четири цифри.

Данните на магазина и правилата са в началото на програмата:

SYSTEM = "Ти си асистентът на поддръжката на онлайн магазин. Отговаряй кратко и само за поръчките на клиента."
CUSTOMER = "C-17"
FALLBACK = "Предадох въпроса на служител. Ще ви пише до един работен ден."
ORDERS = {
"1042": {"customer": "C-17", "total": 45},
"1187": {"customer": "C-17", "total": 89},
"2210": {"customer": "C-23", "total": 120},
}
MONEY = ["ще ви върнем", "ще ви възстановим"]
CARD = re.compile(r"\d{4} \d{4} \d{4} (\d{4})")
ORDER_ID = re.compile(r"(?<!\d)\d{4}(?!\d)")
AMOUNT = re.compile(r"(\d+) €")
REVIEW = []

Виж го

Проверката на изхода

Черновата на модела срещу правилата. Всяко нарушение е ред в списък, а празен списък значи „мина“:

def is_own(order_id):
order = ORDERS.get(order_id)
return order is not None and order["customer"] == CUSTOMER
# След модела: черновата срещу правилата на магазина.
def check_output(draft, order):
problems = []
lower = draft.lower()
for phrase in MONEY:
if phrase in lower:
problems.append("обещава пари")
for found in ORDER_ID.finditer(draft):
number = found.group()
if not is_own(number):
problems.append(f"{number} не е ваша поръчка")
limit = 0 if order is None else order["total"]
for found in AMOUNT.finditer(draft):
amount = int(found.group(1))
if amount > limit:
problems.append(f"{amount} € > {limit} €")
return problems

Сумата се мери с поръчката, за която пита клиентът. Нея я намира orderIn, и то само ако е негова:

# Първата поръчка на клиента в текста (или никоя).
def order_in(text):
for found in ORDER_ID.finditer(text):
if is_own(found.group()):
return ORDERS[found.group()]
return None

Виж го

Целият път

def handle(model, message):
log = []
text = check_input(message, log)
print(f"клиент: {text}")
messages = [{"role": "user", "content": text}]
draft = model.create(SYSTEM, messages)
problems = check_output(draft, order_in(text))
answer = draft
if not problems:
log.append("изход: ок")
else:
log.append("изход: спрян — "
+ "; ".join(problems))
REVIEW.append(draft)
answer = FALLBACK
for line in log:
print(f" лог: {line}")
print(f" отговор: {answer}")
Стъпка по стъпка, с думи
  1. Клиентът пише, а в съобщението има номер на карта.
  2. Проверката на входа е преди модела: картата става **** 1111, а логът казва защо.
  3. Моделът получава само скрития текст. Черновата му: „Съжаляваме! Ще ви върнем 250 € още днес.“
  4. Проверката на изхода мери черновата с правилата на магазина: обещава пари; 250 € > 89 €.
  5. Правило не мина: клиентът получава безопасния отговор, а черновата отива при служител.
  6. Логът пази всяко решение: какво е скрито и кое правило спря отговора. Без картата.
def handle(model, message):
log = []
text = check_input(message, log)
print(f"клиент: {text}")
messages = [{"role": "user", "content": text}]
draft = model.create(SYSTEM, messages)
problems = check_output(draft, order_in(text))
answer = draft
if not problems:
log.append("изход: ок")
else:
log.append("изход: спрян — "
+ "; ".join(problems))
REVIEW.append(draft)
answer = FALLBACK
for line in log:
print(f" лог: {line}")
print(f" отговор: {answer}")
model = FakeModel()
handle(model, "Кога пристига 1042?")
handle(model, "1187 дойде счупена. Платих с карта 4111 1111 1111 1111, искам си парите!")
handle(model, "Къде е поръчка 2210?")
print("за служител:")
for draft in REVIEW:
print(f" {draft}")
Изход
клиент: Кога пристига 1042?
лог: вход: ок
лог: изход: ок
отговор: Поръчка 1042 е изпратена и пристига на 03.11.
клиент: 1187 дойде счупена. Платих с карта **** 1111, искам си парите!
лог: вход: скрит номер на карта
лог: изход: спрян — обещава пари; 250 € > 89 €
отговор: Предадох въпроса на служител. Ще ви пише до един работен ден.
клиент: Къде е поръчка 2210?
лог: вход: ок
лог: изход: спрян — 2210 не е ваша поръчка
отговор: Предадох въпроса на служител. Ще ви пише до един работен ден.
за служител:
Съжаляваме! Ще ви върнем 250 € още днес.
Поръчка 2210 е доставена до адреса в Пловдив.

Първото съобщение минава през двете проверки без промяна. Второто губи картата преди модела и черновата му е спряна след него. В третото моделът казва нещо за чужда поръчка: такъв текст е теч на чужди данни, дори да е верен.

Моделът тук е фалшив клиент като в предишните уроци: по ключова дума в съобщението връща записан отговор. Отговорите са в „По-точно“.

Пробвай

Попълни

Правило не мина. Какво отива при служителя?

REVIEW.append()
answer = FALLBACK

Пробвай

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

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

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

Идеята

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

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

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

Пробвай

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

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

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

Идеята

Кога вреди

  • Прекалено строги правила. Правило „без суми“ спира и „доставката е 4,99 €“. Клиентите получават „ще ви пише служител“ за всичко, а служителите се давят. Мери колко отговора спира всяко правило и кои от тях са били верни.
  • Предпазител вместо права. Проверката на изхода не прави опасен инструмент безопасен. Ако агентът може да върне пари, текстова проверка няма да го спре навреме. Правата се режат в инструмента.
  • Лог с лични данни. Логът, който пази целия разговор, е втора база с карти и адреси. Записвай решенията и причините, а личното скривай преди лога, както преди модела.
  • Проверка с друг модел за всичко. Втори модел, който оценява отговора, хваща повече, но струва още едно извикване и закъснение на всяко съобщение. Пусни първо проверките с код, а модела само за това, което те не могат.
  • Никой не чете лога. Предпазителите се поправят от хората, които виждат спрените отговори. Наблюдението на живо приложение (табла, аларми) е част от ZeroToOps.
По-точно, за любопитните

Номерът на карта отвътре. Изразът тук хваща само четири групи по четири цифри с интервали. Истинските номера са от 13 до 19 цифри, с тирета или слети, а проверката на Лун (Luhn) различава номер на карта от случайно число. Тайни като ключове и пароли изобщо не влизат в разговора: те са в средата, не в кода (урокът за тайните).

Обещанията. Списъкът MONEY е две фрази. Моделът може да напише „ще получите парите си“ и да мине. Затова на практика проверката на обещания често е класификатор (малък модел или втори модел със схема), а правилата с код остават за това, което се мери точно: суми, номера на поръчки, дати.

Готови предпазители. Доставчиците и библиотеките предлагат проверки за обидно съдържание, лични данни и injection. Те са същият слой: преди и след модела, с лог. Бизнес правилата на магазина (кой клиент, коя поръчка, колко €) обаче знае само твоят код.

Фалшивият модел отвътре. Връща първия записан отговор, чиято ключова дума е в съобщението, иначе „Ще проверя и ще ви пиша.“.

# Стои на мястото на истинския модел: записани отговори,
# без мрежа и всеки път едни и същи.
class FakeModel:
ANSWERS = [
("счупена", "Съжаляваме! Ще ви върнем 250 € още днес."),
("2210", "Поръчка 2210 е доставена до адреса в Пловдив."),
("отстъпка", "Да, за 1042 имате 10 € отстъпка."),
("1042", "Поръчка 1042 е изпратена и пристига на 03.11."),
]
def create(self, system, messages):
text = messages[-1]["content"]
for key, answer in self.ANSWERS:
if key in text:
return answer
return "Ще проверя и ще ви пиша."

Упражнение

Правилото за отстъпка

Реши задачата в редактора си.

Добави в checkOutput правило: ако черновата споменава „отстъпка“, това е нарушение „обещава отстъпка“. Отстъпките ги дава маркетингът, не асистентът. Съобщението от „Предвиди изхода“ вече не минава:

Изход
клиент: Обещахте ми отстъпка за 1042. Колко е?
лог: вход: ок
лог: изход: спрян — обещава отстъпка
отговор: Предадох въпроса на служител. Ще ви пише до един работен ден.

Още задачи

  • Дълго съобщение. В checkInput откажи съобщение с повече от 80 думи, без да го пращаш на модела: клиентът получава „Моля, напишете накратко какво се е случило“, а в лога пише защо.
  • Моделът мълчи. Направи фалшивия модел да хвърля изключение при думата „бавно“. handle трябва да даде безопасния отговор и да запише грешката в лога, вместо програмата да спре.

Тест

Провери се

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

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