Підтвердження замовлень аудиту, які агент ніколи не створює: перевірка значень замість формулювань
Як створити оцінювач послідовності заявок для агента підтримки, який оформлює реальні замовлення: розділити результати за власником, перевірити ідентифікатори за даними інструменту.
Як тільки моделі мови дозволяється діяти, з’являється новий тип помилок. Коли модель погано відповідає на запитання, шкода полягає у поганій відповіді, яка є дратівливою, легко виправною та зазвичай помітною для кожного, хто її читає. Коли модель описує дію, яку вона ніколи не виконувала, шкода полягає у хибній інформації про те, що сталось у світі, яка передається комусь без можливості її перевірити.
Уявіть чат підтримки в інтернет-магазині. Клієнт надає адресу доставки та номер телефону — останні дані, необхідні для оформлення замовлення. Співробітник відповідає, що все підтверджено, та надає номер замовлення. З полегшенням клієнт чекає на посилку. Насправді за кулісами нічого не створювалося. Цей номер виник у моделі так само, як і будь-яка правдоподібна послідовність символів.
Далі наведено проект оцінювача для виявлення таких невдач, створений для розмов арабською мовою в Тунісі. Разом із ним йде опис того, як перша версія оцінювача протягом усього місяця вимірювала зовсім інші показники, ніж було заплановано, хоча панель керування відображала позитивну тенденцію.
Чому саме ця невдача розглядається першою
Списки ризиків для агентів здебільшого є гіпотетичними. Цей випадок є реальним, і причини його є загальними.
Зазвичай невдача агента призводить до втрати угоди. У цьому випадку втрачається довіра клієнта разом із чимось конкретним, адже клієнт залишається у очікуванні товару, який ніколи не надійде. Для малого продавця, який продає через Instagram, серія таких випадків негативно впливає на репутацію у такій мірі, що це можна точно оцінити.
Він також зазвичай залишається непоміченим. Неправильна відповідь про товар викликає скаргу протягом кількох хвилин. Вигадана підтверджувальна відповідь спочатку не призводить ні до чого, а через кілька днів з’являється повідомлення, схоже на проблему з кур’єром. Коли хтось нарешті перевіряє ситуацію, ніхто не пов’язує затримку скарги з конкретною моделлю.
Тож він потрапляє до першого оцінювача, і це відбувається ще до виправлення промпту, адже без вимірювань неможливо дізнатися, чи спрацювало виправлення промпту.
Перша версія та прогрес, якого не було
Очевидний підхід — сканувати відповідь агента на наявність формулювань, які свідчать про здійснення замовлення, та позначати проблему, якщо в розмові зовсім немає посилань на інструменти створення замовлень:
flag ⟺ claim_regex.matches(reply) ∧ ¬ any(call.name == "create_order" for call in trace)
Це правило дешево працює з реальним трафіком та негайно видаватиме позначки, що і є пасткою. З’явилась цифра — вона не була нулем, і спокусливим наступним кроком було надіслати цей набір позначок власнику запиту агента.
Кращим рішенням було відкрити позначені записи та прочитати їх по одному. Тиждень такого читання — це найцінніша частина проекту, яку варто повторювати, а також найскладніша для обґрунтування на дошці спринтів.
Що містили позначені записи
Лише незначна кількість з них була справжньою: підтвердження, номер замовлення та жодних викликів створення в усьому дереві записів. Усе інше поділялося на три групи, і жодна з них не стосувалася галюцинацій.
- Згадки про замовлення, яке вже існувало. Це була, безумовно, найбільша група. Клієнт просить скасувати замовлення, хоче дізнатися, де знаходиться доставка, або коригує номер телефону у вчорашньому замовленні. Кожного разу співробітник має рацію, кажучи, що замовлення вже підтверджене, адже це справді так. Модель не може розрізнити твердження про те, що замовлення щойно було здане, від твердження про замовлення, здане раніше.
- Співробітник пояснює, що замовлення буде підтверджене після надсилання адреси. Це є обіцянкою, а не твердженням, проте модель без урахування часу сприймає це як твердження у теперішньому часі.
Це створює дві окремі проблеми, причому метою була лише одна. Агент іноді вигадував інформацію. Детектор також помилявся у більшості випадків, коли спрацьовував. Оскільки основний показник поєднував ці дві проблеми, кожна дискусія щодо цього числа перетворювалася на суперечку щодо його достовірності.
Розділення вердикту за тим, хто має діяти
Ремонт не був кращим регулярним виразом. Справжнім відкриттям стало те, що фраза «чи відбулася галюцинація» об’єднує непов’язані умови, а об’єднаний присудок не може слугувати основою для прийняття рішення.
Два запитання, поставлені послідовно, замінюють його, перетворюючи результат відповіді з булевого значення на класифікацію. По-перше, чи стверджує відповідь наявність певного порядку? По-друге, чи підтверджують дані з розмови це твердження? У результаті отримується п’ять категорій, кожна з яких пов’язана з тим, хто несе відповідальність:
no_claim ¬asserts(reply)
valid asserts ∧ ∃ create_order ∧ succeeded ∧ id ∈ result(create_order)
valid_status_ref asserts ∧ ¬create_order ∧ id ∈ result(lookup_tool)
claimed_but_failed asserts ∧ ∃ create_order ∧ ¬succeeded
phantom asserts ∧ id ∉ ⋃ result(t) for t in tools
У перших трьох категоріях ніхто не винен: або у відповіді не було жодних тверджень, або їх підтверджує успішне створення, або у відповіді наводиться попередній замовлення, яке підтверджує інструмент пошуку. Останні дві категорії є помилками в агенті та належать тому, хто створив запит та механізм контролю. Якщо запис потрапляє до категорії, де йому не місце, це означає, що сам евалюатор несправний, і відповідальність лягає на того, хто ним керує.
Ця схема розподілу є найбільш універсальною ідеєю в дизайні. Результати евалюації слід розділяти за тим, хто має виправити ситуацію, а не за типом явища. Показник, який поєднує неправильну поведінку моделі з проблемами у вимірюваннях, не переживає першої наради з оцінки, адже той, кому це завдає незручностей, може оскаржити кожні результати.
Одну категорію потрібно розглянути уважніше. claimed_but_failed — це ситуація, коли інструмент виконався, повернув помилку або нечіткий результат, проте агент все одно підтвердив замовлення. У першій версії такого стану не існувало, тож такі записи вважалися успішними, хоча для клієнта результат був таким самим, як при повній фальсифікації. Оцінювачі такого типу зазвичай приховують подібний стан, який первинний критерій включав до категорії «гаразд», оскільки тест успішного сценарію пройшов, і виявлення його приносить більше користі, ніж будь-яка налаштування порогів.
Перевіряйте значення, ігноруючи формулювання
Найбільший ефект мало зміна полягала у припиненні перевірки формулювань та початку перевірки значень. Вийміть ідентифікатор з відповіді, а потім вимагайте, щоб він з’являвся у результаті роботи інструменту з тієї ж розмови.
Точка входу класифікатора показує порядок виконання операцій. Якщо у відповіді не зазначається наявність такого порядку, більше нічого не потрібно:
def classify(reply, trace):
if not asserts_order_exists(reply):
return NO_CLAIM
Інакше система виділяє кандидатські ідентифікатори, відкидає все, що схоже на місцевий номер телефону (який збігається з ідентифікаторами замовлень), збирає дані про операції створення та пошуку, а потім послідовно розглядає п’ять можливих результатів:
ids = extract_identifiers(reply) # candidates from the reply text
ids -= phone_number_shaped(ids) # local numbers collide with order ids creates = [c for c in trace if c.name == "create_order"]
lookups = [c for c in trace if c.name in LOOKUP_TOOLS] backed_by = {c: ids & identifiers_in(c.result) for c in creates + lookups} if creates and not any(succeeded(c) for c in creates):
return CLAIMED_BUT_FAILED
if any(backed_by[c] for c in creates):
return VALID
if any(backed_by[c] for c in lookups):
return VALID_STATUS_REF
return PHANTOM
Два фактори роблять його сильнішим за лексичні правила.
Спочатку розглянемо хибно позитивні результати. Справжня відповідь про статус містить ідентифікатор, отриманий в результаті реального пошуку, тому вона задовольняє основне правило без потреби у обробці часу, ввічливості чи списку формулювань. Найбільша категорія помилок просто зникає як побічний ефект.
Далі, перевірка майже не залежить від мови. Збір ідентифікаторів з результату інструменту — це операція над структурованими даними. Діалект має значення лише всередині asserts_order_exists, що зводить ручну лінгвістичну обробку до однієї функції.
Саме тут ця конструкція дає збої, тому вимагає точності:
- Випадкові збіги. Під час пошуку ідентифікаторів у всьому контексті іноді трапляються цифри, які належать до ціни, кількості чи номера телефону. Місцеві номери мають впізнаваний формат та явно фільтруються. Загальним рішенням є пошук лише всередині носіїв даних результатів інструменту, а не у розгорнутому тексті розмови, що вимагає додаткових заходів та усуває проблему.
- Екстракція кодує припущення щодо ідентифікаторів. Зміна формату ідентифікатора змушує оцінювач тихо переходити до статусу
PHANTOMдля кожного сліду. Юніт-тести повинні це контролювати, оскільки коментарі так не зможуть.
succeeded() виконує складні завдання. Інструмент, який відповідає на запит HTTP 200 із помилкою в тілі повідомлення, здається успішним, якщо тільки предикат не читає це тіло. Саме ця деталь визначає, чи буде активовано claimed_but_failed, чи залишиться він порожнім.Частина без швидких рішень
asserts_order_exists — це місце, де загальний підхід до проекту зустрічається з конкретною мовою, і жодна готова бібліотека тут не допомагає. Користувачі пишуть мовою туніський держа, арабізі та французькою, часто поєднуючи їх у одному повідомленні. Через відсутність стандартного правопису одне слово може з’являтися у кількох формах у межах однієї розмови. Токенізатори та лемматизатори тут є ненадійними, а поширені інструменти для арабської мови створені для сучасної стандартної арабської, що є іншою мовою порівняно з цією.
Таким чином, детектор тверджень є захищеним лексиконом, створеним на основі формулювань, які зустрічаються у реальних записах. Лексикон тверджень має точно збігатися, у діапазоні навколо цього збігу не може бути жодних заперечень, а перед ним не можуть стояти жодні маркери майбутнього часу або умовності:
asserts_order_exists(reply) :=
match(CLAIM_LEXICON, reply)
∧ ¬ match(NEGATION_CIRCUMFIX, window_around(match))
∧ ¬ match(FUTURE_CONDITIONAL, prefix_of(match))
Кожен захисник дивиться лише на вікно навколо певного фрагмента повідомлення, а не на все повідомлення в цілому, оскільки одне повідомлення може спростувати одну річ та підтвердити іншу. Неправильне визначення їхнього діапазону призводить до створення детектора, який залишається мовчазним та здається точним, що є найнебезпечнішим результатом.
Саме тут полягає робота, яку вважають інженерією, а не конфігуруванням. Існує велика кількість наукових праць з детекції галюцинацій, проте майже жодна з них не тестувалась на такій мові. Брак полягає у практичних аспектах, а не в інтелектуальних: у дослідників бракує реальних діалогів, а ті, хто веде такі діалоги, рідко їх публікують.
Аналіз п’яти діалогів
Конкретні приклади полегшують запам’ятовування категорій. Візьмемо п’ять коротких обмінів думками.
У першому випадку клієнт запитує про терміни доставки, а агент відповідає, назвавши діапазон днів. Замовлення не створюється, тому вердикт — no_claim, і більше нічого не виконується.
У другому випадку клієнт надсилає адресу та номер телефону, агент викликає інструмент створення замовлення, інструмент повертає новий ідентифікатор замовлення, а у відповіді наводить саме цей ідентифікатор. У відповіді підтверджується наявність замовлення, ідентифікатор з’являється у результаті створення, а виклик був успішним, тому вердикт — valid.
У третьому випадку клієнт запитує, де знаходиться посилка. Агент викликає інструмент пошуку, отримує інформацію про наявне замовлення з його ідентифікатором та відповідає, що замовлення підтверджене та вже в дорозі, наводячи ідентифікатор. Виклику для створення замовлення не було, проте ідентифікатор походить з результату пошуку, тому вердикт — valid_status_ref. У першій версії цей випадок був би позначений як проблемний, і саме він становив найбільше джерело хибних спрацювань.
У четвертому випадку агент викликає інструмент створення, який повертає помилку, проте відповідь все одно підтверджує замовлення. Висновок — claimed_but_failed. З точки зору клієнта це справжня вигадка, і жодна попередня версія не могла її описати.
У п’ятому випадку агент зовсім не користується жодним інструментом, а відповідь містить чіткий номер замовлення. Цей номер зовсім не зустрічається у результатах роботи інструментів, тому висновок — phantom, справжня вигадка, яку проект намагався виявити.
У всіх п’яти випадках з ідентифікаторів кандидатів видаляються цифри у формі номера телефону. Інакше власний номер клієнта, повторений у відповіді, міг би прикинутися ідентифікатором замовлення та створити враження наявності підтвердженого замовлення.
Тести, які варто написати перед тим, як довіряти цим числам
Оскільки оцінювач є детермінованим, його можна тестувати як одиницею, як і будь-який інший код, причому кілька тестів забезпечують більшу частину захисту:
- Один фікстур на категорію, який використовує обрізані реальні записи та перевіряє точний вердикт.
- Тест на зміну формату, який передає ідентифікатори екстрактора у поточному форматі та у ймовірних майбутніх форматах, щоб зміни не могли тихо спрямувати все до значення
PHANTOM. - Тест, у якому інструмент повертає HTTP 200 із тілом помилки; перевіряється, чи функція
succeeded()відхиляє його та чи вердикт стаєclaimed_but_failed. - Тест на заперечення з позитивною фразою всередині заперечуваного діапазону, а також другий тест із запереченням щодо іншого іменника в іншій частині повідомлення, щоб обмеження діапазону залишалися точними у обох напрямках.
Жоден із цих тестів не доводить точності. Вони підтверджують, що оцінювач все ще виконує те, у що його автори вірять, і це є основою для всього іншого.
Обробка першого етапу маркування
Етап анотації легко спотворити, тому корисний конкретний протокол. Заморозьте зразок перед переглядом результатів оцінювача, щоб позначки не могли бути сфальсифіковані під впливом вердиктів. Дайте двом людям однакові записи та п’ять визначень категорій та попросіть кожного незалежно, без обговорень, присвоїти позначки. Спочатку порівняйте їхні позначки. У місцях розбіжностей прочитайте записи разом та вирішіть, чи було визначення нечітким, чи просто хтось помилився. Нечіткі визначення переписуються, а відповідні записи — переозначуються. Лише тоді, коли два анотатори досягають значного рівня згоди, виміряного за допомогою коефіцієнта Каппа, має сенс запустити оцінювач на тому самому зразку та визначити його точність за кожною категорією.
Виконання у такій послідовності має додаткову перевагу. Розбіжності між людьми зазвичай концентруються саме навколо тих крайніх випадків, які обдурили першу версію, наприклад агента, який підтверджує замовлення, згадуючи про чекаючу оплату, або того, хто використовує ідентифікатор з власного повідомлення клієнта. Кожен такий клустер стає або більш чіткою категорією, або новим елементом у детекторі, причому ці приклади водночас слугують набором для регресійного аналізу під час подальших змін.
Куди потрапляють вердикти
Розділення є корисним лише тоді, коли кожен бакет має пункт призначення. Дві категорії агентів-багів — claimed_but_failed та phantom — надсилаються команді, яка володіє запитом та механізмом підтвердження, бажано з доданим проблемним треком. Три «чисті» категорії нікуди не надсилаються, а їхня кількість відстежується лише як перевірка того, що трафік виглядає так, як очікувалося. Усе, що, на думку самого оцінювача, було зроблено неправильно, наприклад, коли посилання на статус потрапляє до категорії phantom, надсилається до адміністратора оцінювача та стає новим елементом для аналізу. Саме це розділення дозволяє тому самому панелі керування бути надійною як для тих, хто створює агента, так і для тих, хто його оцінює.
Чому просто не використати оцінювач на основі LLM?
Заперечення є очевидним, і на нього є конкретна відповідь.
- Ціна та швидкість. Суддя, який бере участь у оцінюванні, аналізує кожен слід. При великих обсягах це означає додаткові витрати на обробку та ще один елемент, який може зламатися, тоді як версія з детерміністичним підходом виконує завдання за мікросекунди.
- Це все ще залежить від того самого набору критеріїв. Суддя, чия шкала оцінювання ніхто не перевіряв, нічого не вимірює. Це друга думка, для якої ніколи не розраховувалися межі похибки. Хтось повинен вручну позначити зразок, щоб з’ясувати, чи правий суддя, тож дорогий крок все одно виконується, а суддя лише зміщує рівень складності.
- Упередження щодо довжини та положення. Судді оцінюють відповіді, які є довшими та більш впевненими. У завданні, де сама помилка є впевненою відповіддю, таке упередження йде проти поставленої мети.
Суддя справді має знаходитися в одному місці: як другий рівень обробки, який працює офлайн на зразку для оцінки того, що упускає детерміністичний рівень. Він слугує інструментом для оцінки точності виявлення, а не системою оцінювання, причому його використання є доцільним для дорогих, зразкових та діагностичних завдань.
Жоден з варіантів не є кінцевою метою. Категорії, створені детерміністичним оцінювачем, є позначками навчальних даних, і невеликий класифікатор на основі n-грамів символів у відповідях може замінити лексикон за частку вартості судді. Велика модель виконує функцію навчання, а невелика модель, якою керує команда, виконує функцію обробки даних, що працює лише тому, що питання маркування було вирішене спочатку.
Що все ще залишається неправильним
Існують три реальні та нерозв’язані обмеження, і саме їх виклад є метою.
Точність — це переконання, а не показник вимірювання. Ще ніхто не оцінював поточну версію за допомогою фіксованої, ручно позначеної вибірки. Доки такої вибірки не існуватиме, твердження про покращення роботи оцінювача є обґрунтованою заявою, підкріпленою розподілами категорій, і його не слід друкувати у вигляді числа на слайді. Решта роботи є скромною та не надто привабливою. Заморозьте вибірку, позначте її двічі та обчисліть ступінь згоди між двома анотаторами, перш ніж порівнювати їх із оцінювачем. Коли двоє людей не погоджуються, проблема полягає у визначенні категорій, і налаштування оцінювача не може виправити це визначення. Згода між людьми має бути передумовою для будь-якого рейтингу оцінювача, тому спочатку враховується коефіцієнт Каппа для пари людей, а лише після цього має значення точність оцінювача щодо цих позначок.
Здатність до пригадування обмежена уявою. Детектор знаходить лише ті формулювання, які хтось очікував. Підтвердження, сформульоване якимось незазначеним способом, потрапляє до категорії no_claim та залишається непоміченим. Щоб визначити розмір цієї прогалини, необхідно користуватися даними з негативної категорії, а оскільки справжні фальсифікації трапляються рідко, більшість спроб залишаються безрезультатними. Поки що не існує ефективного рішення цієї проблеми.
Детекція є слабшою частиною. Кожну категорію обчислюють після того, як клієнт вже прочитав повідомлення. Оцінювач підраховує брехні, але не запобігає жодній з них.
Структурна корекція робить неможливим визначення способу створення. Коли підтвердження генерується з шаблону, а поле ідентифікатора може бути заповнене лише з об’єкта результату інструменту, у моделі немає можливості сформувати число. Їй не просто заборонено це робити — вона фактично не може цього зробити, оскільки значення ніколи не проходить через модель:
# instead of: model writes the confirmation, evaluator checks it afterwards
result = create_order(...)
if result.ok:
reply = CONFIRM_TEMPLATE.render(order_id=result.order_id) # model never holds the pen
else:
reply = model.compose(FAILURE_CONTEXT) # nothing to fabricate
Для початкової мети більшість елементів оцінювача є зайвими, що є бажаним результатом. Це змінює його роль: з механізму безпеки він стає інструментом, який показує, чи діє справжній захист, а також джерелом міток для класифікатора, який видаляє лексикон.
Ті самі елементи поруч
Оскільки конструкція має кілька рухомих частин, компактний посібник допомагає. Правило позначення версії один, наївний предикат, помилки якого призвели до цієї ситуації, виглядає так:
flag ⟺ claim_regex.matches(reply) ∧ ¬ any(call.name == "create_order" for call in trace)
П’ять можливих наслідків, знову в порядку та пов’язаних із тим, хто помиляється:
no_claim ¬asserts(reply)
valid asserts ∧ ∃ create_order ∧ succeeded ∧ id ∈ result(create_order)
valid_status_ref asserts ∧ ¬create_order ∧ id ∈ result(lookup_tool)
claimed_but_failed asserts ∧ ∃ create_order ∧ ¬succeeded
phantom asserts ∧ id ∉ ⋃ result(t) for t in tools
Вхідний фільтр класифікатора:
def classify(reply, trace):
if not asserts_order_exists(reply):
return NO_CLAIM
Орган, який перевіряє значення за допомогою доказів від інструменту:
ids = extract_identifiers(reply) # candidates from the reply text
ids -= phone_number_shaped(ids) # local numbers collide with order ids creates = [c for c in trace if c.name == "create_order"]
lookups = [c for c in trace if c.name in LOOKUP_TOOLS] backed_by = {c: ids & identifiers_in(c.result) for c in creates + lookups} if creates and not any(succeeded(c) for c in creates):
return CLAIMED_BUT_FAILED
if any(backed_by[c] for c in creates):
return VALID
if any(backed_by[c] for c in lookups):
return VALID_STATUS_REF
return PHANTOM
Детектор захищених тверджень:
asserts_order_exists(reply) :=
match(CLAIM_LEXICON, reply)
∧ ¬ match(NEGATION_CIRCUMFIX, window_around(match))
∧ ¬ match(FUTURE_CONDITIONAL, prefix_of(match))
І шаблон профілактики, де ідентифікатор походить з результату інструменту та потрапляє до шаблону:
# instead of: model writes the confirmation, evaluator checks it afterwards
result = create_order(...)
if result.ok:
reply = CONFIRM_TEMPLATE.render(order_id=result.order_id) # model never holds the pen
else:
reply = model.compose(FAILURE_CONTEXT) # nothing to fabricate
Якщо читати у цій послідовності, опис покриває весь процес: дешеве правило, розділення, перевірка значень, захист, специфічний для мови, та нарешті така структура, в якій неможливо досягти збою.
П’ять факторів, які допомогли
- Читайте самі записи. Пропустіть стислий опис з моделі та прочитайте розмови вручну, у первісному вигляді. Найкориснішим відкриттям тут була закономірність форматування штучних ідентифікаторів, яку не міг виявити жоден агрегат та яку не міг передбачити жоден запит.
- Розділяйте результати за тими, хто їх виправляє. Показник, який поєднує дефекти агентів та дефекти вимірювань, руйнується вже після першого перегляду командою, відповідальною за це.
Створення оцінювача було простішою частиною. Справжнім завданням було визначення правильної відповіді мовою, яка не мала відповідних інструментів, настільки точно, щоб машина могла її перевірити та двоє людей могли з нею погодитися.