Кожен запобіжник – правило не на своєму місці

Цілий вівторок я писав запобіжник, щоб помирити два інші запобіжники. Поставила крапку в цьому підході друга модель, якій я доручив перевіряти першу: вона спрацювала 18 разів із точністю 17%, і вже за тиждень усі правила переїхали з промпту в код – туди, де їх дістане тест.

Цілий вівторок я писав запобіжник, щоб один запобіжник перестав сперечатися з іншим запобіжником.

Стерегли вони чат-агента, який веде структуровану розмову-анкету. Він розпитує про ваш досвід і навички, де ви хочете працювати й коли зможете почати – загалом 11 полів, – а тоді пропонує з живого каталогу те, що вам пасує. Під капотом дев’ять інструментів, а назовні – одна розмова, яка мусить звучати як жива людина.

Після моделі стояло ще близько 25 запобіжників у звичайному коді: вони правили її текст, перш ніж його побачить кандидат. Одинадцять із них переписували сам текст, і назви мали на кшталт stripLeakedToolCalls та enforceOneQuestion, а два з них ніяк не могли дійти згоди, чи речення вже переписане.

Ось до чого я дійшов: запобіжник – це правило, яке живе там, куди не дотягнеться жоден тест, і коли система все більше обростає запобіжниками, вона розповідає вам, де живуть її правила, а не те, який ретельний у неї автор. Кожен мій запобіжник вмикався вже після того, як рішення ухвалено, працював із готовим текстом і бачив лише кінцевий рядок. Правилам на такому рівні потрібні правила про правила – і мені довелося самому таке збудувати, щоб у це повірити.

Запобіжник – це правило, що спрацьовує вже після рішення

Чотири місяці я вважав, що агент – це кастрований ChatGPT. Прив’язуєш загальну модель до ролі, видаєш їй фіксований набір інструментів – і маєш працівника, який ніколи не спить. Перша версія саме такою й була, і працювала чудово – аж доки їй не почали відповідати живі люди.

[тодішній макс]

Живі люди на питання про вік називають рік народження. Надсилають два номери телефону – або один, але без однієї цифри. Буває ім’я, з якого не вгадаєш стать, і бот мусить про неї спитати так, щоб не вийшло дивно. Буває запитання без знака питання, і коду, що стоїть далі, доводиться вгадувати: відповідати чи йти далі.

Кожен такий випадок ставав рядком у системному промпті, аж поки промпт не розрісся так, що модель почала пропускати його шматки повз вуха. Тож уся архітектура звелася до гасла: ШІ реагує, код перевіряє. Перш ніж відповідь потрапляла до кандидата, код переглядав чернетку, записував те, що модель записати забула, і підказував їй, як поставити наступне питання.

Кілька тижнів я почувався всемогутнім: будь-яка моя забаганка могла стати ще одним запобіжником.

А тоді відхилення посипалися одне за одним. Номер телефону дають аж наприкінці, коли вакансія видається вартою того. Посеред розмови передумують, де хочуть працювати. В одній переписці жінка шукає роботу і собі, і чоловікові. У текст просочується уламок виклику інструмента. Бот, ще не знаючи, як звати кандидата, називає його своїм іменем.

Кожне виправлення потрапляло в одне й те саме місце, бо інших місць уже не лишилося: після того, як модель написала речення, – просто в це речення.

Найсильніший аргумент за запобіжники – кожен виріс із реального випадку

За кожним запобіжником стояв справжній інцидент і коментар у коді, який цей інцидент називав. Жоден не з’явився «про всяк випадок», і кожен окремо був найдешевшим правильним виправленням, яке я мав того дня. Хто скаже, що варто було одразу краще все спроєктувати, той сперечається з тим, як насправді приходили вимоги: по одному кандидату за раз.

Найкращою ідеєю в усьому підході був аудитор. 14 липня я дав дешевій другій моделі чернетку відповіді й те, що насправді сталося за цей хід, – реальні виклики інструментів – і спитав: чи не приписує собі текст дії, якої не було? Аудитор розумів сенс, тож перефразування повз нього не прослизнуло б. Йому було байдуже, якою мовою відповідає агент, тож він упорався б і з тими мовами, під які мої шаблони ніколи не писалися. На першій же перевірці на живих даних він зловив речення на кшталт «let me check a few more details» – обіцянку зробити щось потім, сформульовану так, як не передбачав жоден мій шаблон.

Саме такому аргументові я й готовий програти – і години зо шість я вигравав.

Точність коректор мусить заслужити

На прогоні з 15 вигаданими кандидатами аудитор спрацював 18 разів із точністю близько 17%. Ось три речення, які він «полагодив»:

Спершу я подумав, що йому бракує контексту, і став давати аудиторові не назви інструментів, а результати їхньої роботи. Щойно я записав у лог, що саме він отримує, питання закрилося: результати були на місці, а він однаково бив на сполох. Маленька модель, якій доручили пам’ятати стільки винятків, якісь із них неодмінно загубить, а хибне виправлення коштує дорожче за пропущене, бо псує той природний голос, заради якого все це й будувалося.

[адвокат диявола]

Тож я звузив його до одного-єдиного питання: чи обіцяє бот сам щось зробити згодом? До того ж це єдине, чого не вміли запобіжники в коді. Я виміряв звужену версію через npm run audit:probe, і вона набрала 8 із 8 і за повнотою, і за точністю – разом із двома обіцянками самого бота мовами, під які мої шаблони ніколи не писалися. Кількість спрацювань за весь прогін упала з 18 до нуля.

А тоді я подивився, що ж у мене вийшло. Модель, уся робота якої – не довіряти іншій моделі, обгорнута власними запобіжниками, і ставить вона одне-єдине питання, на яке задарма відповіло б одне поле в структурованій відповіді.

Перенести рішення означає прибрати збій, а не ловити його

Через два дні переписаний код уже працював: мінус 1500 рядків і три зміни.

Модель більше не пише фінального повідомлення. Вона повертає частини: що мала на увазі, як відреагувала, відповідь на запитання кандидата, якщо воно було, і як поставити наступне питання анкети. Рядок складає код. Уламки викликів інструментів, два питання на одному подиху, речення, обірване на півслові, – у ході розмови просто не лишилося кроку, де щось із цього могло б з’явитися, тож запобіжникам, які раніше це вичищали, більше нема роботи.

Модель більше не вирішує, якого поля бракує. Це робить чиста функція, тож нікого вже не питають про одне й те саме двічі.

Кожне твердження має мітку наміру із закритого списку з 11 варіантів, а код проглядає журнал викликів інструментів за цей хід і звіряє. Якщо відповідь каже, що знайшла кілька вакансій, а пошуку в журналі немає, код або запускає цей пошук, або знижує відповідь до чогось чесного. Виправлення лишає відповідь моделі слово в слово й підміняє тільки хибне твердження – саме цього весь час і намагалася досягти хірургія над текстом, тільки тепер так улаштована сама система.

де живуть правила той самий агент, дві архітектури
модель пише, код виправляє
  • усі правила живуть у системному промпті, а модель частину з них пропускає, щойно він задовгий
  • модель сама пише все фінальне повідомлення
  • модель вирішує, якого поля ще бракує
  • ~25 запобіжників переписують чернетку перед відправленням
  • друга модель перевіряє першу, і їй самій потрібні запобіжники
код вирішує, модель формулює
  • дешевий розбір змісту (NLU) читає повідомлення ще до будь-якої генерації
  • модель повертає частини; рядок складає код
  • наступне поле обирає чиста функція
  • закритий список з 11 намірів, кожен звіряють із журналом інструментів за цей хід
  • хибне твердження – це окреме поле зі своїм типом, тож аудиторові більше нема чого читати
один хід після переписування модель з’являється двічі, і жодного разу – останньою
  1. ● 01
    звірка
    дешевий розбір змісту (NLU), ще до генерації
  2. ● 02
    план
    planIntakeMove(): поле обирає код
  3. ● 03
    генерація
    пласка відповідь: намір, реакція, відповідь, питання
  4. ● 04
    перевірка
    намір звіряють із журналом інструментів
  5. ● 05
    складання
    composeReply(): єдине місце, де складається рядок

Міняти все це було страшно, бо старе таки працювало. Тож – тести: 804 зелені того вечора, коли запрацював новий структурований цикл, і 1190 на момент, коли я це пишу. Дев’яносто сім вигаданих кандидатів, по 16 ходів кожен, а людину навмисно погано грає інша модель: відповідає лише на те, про що спитали, по одному факту за раз – так, як роблять живі люди і як ніколи не роблять заготовлені тестові дані. Один кандидат чотири ходи поспіль намагається зламати агента, не може, а тоді заспокоюється і, як усі, шукає роботу. Одне правило в цьому тестовому стенді існує лише для того, щоб вигаданий кандидат часом пропускав знайомство й одразу починав із питання, – саме в такій ситуації агент раніше звертався до незнайомця на єдине ім’я, яке було в кімнаті. Своє.

[макс]

Чого не купиш, перенісши правила в код

Системний промпт вийшов із цієї історії довшим, ніж увійшов. Я думав, що коли перенесу правила в код, він схудне. А переїхала несуча частина – туди, де її дістане тест, і це зовсім не та перемога, по яку я йшов.

І ще одне: теза допомагає лише тоді, коли правила вже існують. Цей агент розмовляє з кандидатом, а служить компанії: дає відгук живою мовою і веде розмову за сценарієм. Винахідливо розв’язувати чиїсь проблеми в його завдання ніколи не входило – тому правила взагалі й вдалося перелічити.

[адвокат диявола]

Так. Агент із по-справжньому відкритим завданням, який служить меті самого користувача, не має такого списку, щоб перенести його в код, – і все, про що я тут пишу, довелося б доводити заново, з нуля.

Тепер у мене є переконлива надія, що цей агент поводитиметься передбачувано і його буде дешево розширювати. Рано чи пізно ми знову перевантажимо його мінливістю й суперечностями власних вимог, за якими самі погано стежимо, і він знову почне обростати запобіжниками. Різниця в тому, що тепер кожен новий запобіжник я читаю як питання: де насправді місце його правилу? І досі не знаю, скільки їх має набратися, щоб це питання я вже не міг не поставити.