Тестування AI-застосунків
Зміст
Уся класична автоматизація стоїть на одній тихій обіцянці: та сама дія дає той самий результат. Натиснув кнопку — з'явився той самий текст, надіслав той самий запит — отримав ту саму відповідь. Оракул (oracle) точний: expect(total).toBe(42). Щойно у застосунку з'являється LLM-фіча — чат-бот підтримки, «розумний» пошук, генератор описів, — ця обіцянка ламається. Двічі надіслав той самий запит — отримав дві різні (обидві правильні) відповіді. І тепер незрозуміло, що взагалі писати в expect.
Ця глава — про те, як тестувати систему, у якій «правильно» не одне, а ціла множина прийнятних відповідей, і де кожен прогін інший. Тема має високу вагу на співбесідах: усе більше вакансій QA торкаються продуктів з AI-фічами, а більшість кандидатів досі мислить категоріями точного порівняння й пасує на першому ж «а як ви це асертите?». Це канонічна глава: інші глави розділу («Верифікація результатів AI», «Evals та LLM-as-judge») посилаються сюди за базовою моделлю. Водночас це глава-поглиблення — на першому проході розділу її можна пропустити й повернутися, коли доберетеся до реального AI-продукту; трек «як працюють LLM → промптинг → генерація → верифікація» від цього не розірветься.
Чим тестування LLM-фічі відрізняється від звичайної
Звичайна фіча детермінована й має точний оракул. LLM-фіча не має ні того, ні іншого — і саме це, а не «магія нейромереж», змінює всю роботу QA. Розберемо два зсуви.
Зсув перший: оракул стає розмитим. У класичному тесті результат або дорівнює очікуваному, або ні — бінарно. У LLM-фічі на один запит існує ціла множина прийнятних відповідей: «Замовлення скасовано», «Я скасував ваше замовлення №123», «Готово, замовлення більше не активне» — усі три правильні, жоден точний рядок наперед не відомий. Правильність перетворюється зі значення (true/false) на спектр якості: відповідь може бути ідеальною, прийнятною, посередньою або неприйнятною. Тестувальник більше не звіряє рядок — він оцінює відповідь за набором критеріїв.
Зсув другий: об'єкт перевірки — не код, а поведінка ймовірнісної моделі. Ви тестуєте не гілку if, яку хтось написав, а статистичну систему, поведінку якої ніхто не програмував явно. Вона може бути правильною на тисячі прикладів і зламатися на тисячі першому без жодної зміни коду — бо змінився промпт, версія моделі або просто випав інший семпл. Тому тестування LLM-фічі ближче до нефункціонального й дослідницького тестування, ніж до перевірки за специфікацією: ви оцінюєте якість на розподілі входів, а не відповідність одному очікуваному результату.
Наслідок практичний: набір assertEquals тут не працює. Потрібні інші інструменти — детерміновані перевірки для того, що можна зафіксувати точно (формат, схема, наявність заборонених слів), і семантичні/оцінювальні — для розмитого ядра.
Недетермінізм: чому «двічі — різний результат»
Механізм недетермінізму пояснено в главі «Як працюють LLM»; тут — лише те, що з нього випливає для тестів. Модель на кожному кроці не «знає» наступне слово, а рахує розподіл імовірностей по всьому словнику й семплить із нього. Параметр температури (temperature) керує тим, наскільки сміливо: висока температура — більше різноманіття, низька — консервативніше, найімовірніші токени. Тому той самий запит дає різні тексти.
Спокуса QA-новачка — «поставимо температуру 0 і матимемо детермінізм». Це напівправда. Температура 0 робить вибір майже завжди жадібним (найімовірніший токен), але повної відтворюваності не гарантує: порядок операцій із рухомою комою, апаратні відмінності, батчинг запитів на боці провайдера — усе це дає розбіжності. Деякі API пропонують параметр seed для best-effort відтворюваності, але й він не є контрактом на побайтову ідентичність.
Що з цього для тесту:
- Точні асерти на текст (
expect(answer).toBe("...")) флакі (flaky) за побудовою, а не через погано написаний тест. Це не той флак, який лікують очікуваннями (про природу флаку — розділ «Автоматизація»); його не можна «дочекатися», бо він у самій моделі. - Для тестів варто знижувати температуру — не заради ілюзії детермінізму, а щоб звузити розкид і зробити перевірки стабільнішими.
- Ніколи не будуйте перевірку на припущенні, що прогін відтвориться байт у байт. Асертьте властивості відповіді (містить факт, має формат, у межах довжини), а не її дослівний текст.
Галюцинації як клас дефектів
Галюцинація (hallucination) — це впевнено сформульована, але хибна або вигадана інформація: неіснуючий пункт політики повернень, вигаданий номер замовлення, посилання на API-метод, якого немає, «факт», який звучить правдоподібно й неправдивий. Ключове для QA: це не випадковий збій, а системна властивість. Модель за побудовою передбачає правдоподібний наступний токен, а не звіряється з джерелом істини; коли правдоподібне й правдиве розходяться, вона спокійно видає правдоподібне.
Тому галюцинації варто трактувати як окремий клас дефектів — так само, як вразливості в безпеці: їх не «зловиш попутно», їх треба цілеспрямовано полювати. Типові підкласи:
- вигадані факти (ціни, терміни, умови, яких немає в реальних даних);
- вигадані посилання й цитати (неіснуючі документи, статті, номери);
- вигадані сутності інтерфейсу (кнопки, ендпоінти, поля, яких немає у продукті);
- правдоподібно-хибні відповіді на запити, де правильна дія — сказати «не знаю».
Як тестувати цей клас:
- Питання без відповіді. Дайте запит, на який у системи немає даних. Гарна фіча відмовляється й каже «не маю такої інформації»; погана — вигадує. Це один із найінформативніших тестів на галюцинації.
- Заземлення (grounding). Перевіряйте, що фактичні твердження у відповіді підкріплені наданим контекстом чи джерелом, а не «пам'яттю» моделі.
- Адверсаріальні промпти. Провокативні або хибно-передумовні запити («як активувати преміум-режим?», якого немає) — чи піде модель на поводу.
Про те, як відрізняти правдоподібне від правдивого у виводі AI руками, — глава «Верифікація результатів AI».
RAG: два шари відмов
Більшість «розумних» фіч над корпоративними даними побудовані як RAG (retrieval-augmented generation) — генерація з доповненням пошуком. Замість того щоб покладатися лише на «пам'ять» моделі, система спершу знаходить релевантні документи (у базі знань, документації, тикетах), вкладає їх у контекст запиту й лише тоді просить модель відповісти, спираючись на знайдене. Це зменшує галюцинації й дає відповіді на приватних даних, яких модель не бачила під час навчання.
Для тестувальника головне тут — RAG має два незалежні шари, і кожен відмовляє по-своєму:
- Відмова шару пошуку. Знайдено не ті документи, релевантні пропущені, або контекст порожній. Тоді навіть ідеальна модель відповість погано — їй просто нема на що спиратися. Причини: слабкі ембединги, невдале нарізання документів на фрагменти (chunking), брак потрібного документа в базі.
- Відмова шару генерації. Пошук спрацював, потрібний фрагмент у контексті — а модель усе одно проігнорувала його, змішала з «пам'яттю» або вигадала. Це вже не проблема пошуку.
Головна практична помилка — тестувати RAG як чорну скриньку «запит → відповідь». Тоді на поганій відповіді ви не знаєте, який шар винен, і рекомендація буде навмання. Тестувати треба пошарово:
- Пошук окремо. Для набору запитів перевірте, чи потрапляє правильний документ у топ-
kрезультатів. Тут працюють класичні метрики пошуку — точність (precision) і повнота (recall) знайденого. Це детерміновано перевіряється й не потребує LLM. - Генерацію окремо. Подайте моделі завідомо правильний контекст і перевірте вірність джерелу (faithfulness): чи всі твердження відповіді випливають із контексту, чи немає доданого «від себе». А тоді — подайте порожній чи нерелевантний контекст і перевірте, що модель відмовляється, а не вигадує.
Такий поділ перетворює невиразне «бот іноді бреше» на діагноз: «на 30% запитів пошук не піднімає потрібний розділ FAQ у топ-3» — і це вже конкретний баг конкретного шару.
Семантичні перевірки замість точного порівняння
Якщо toBe не працює, чим асертити? Відповідь: багатошарово, від найдешевших і найстрогіших перевірок до найдорожчих і найрозмитіших. Не все у відповіді розмите — багато що можна й треба перевіряти точно.
Шар 1 — детерміновані перевірки. Те, що не залежить від формулювання, лишається точним і надійним:
- структура валідна (JSON проходить схему, є всі обов'язкові поля);
- формат правильний (дата у ISO 8601, сума — число, не рядок);
- довжина в межах;
- немає заборонених рядків (лайка, PII, внутрішні URL, назви конкурентів) — звичайний regex;
- є обов'язкові маркери (наприклад, номер замовлення з запиту дослівно присутній у відповіді).
Ці перевірки дешеві, стабільні й ловлять більшість грубих дефектів. Ставте їх першим гейтом.
Шар 2 — семантичні перевірки. Для розмитого ядра, де важливий сенс, а не слова:
- подібність ембедингів (embedding similarity) — перетворюємо відповідь і еталон на вектори й міряємо косинусну близькість; висока близькість означає «про те саме», навіть якщо слова інші (про ембединги — оглядово в главі «Як працюють LLM»);
- наявність очікуваних сутностей/фактів — відповідь має згадати конкретні факти, без вимоги до формулювання.
Шар 3 — LLM-суддя (LLM-as-judge). Коли й семантичної близькості мало (треба оцінити тон, повноту, коректність міркування), відповідь оцінює інша модель за рубрикою. Це потужно, але має власні упередження й недетермінізм — тому винесено в окрему канонічну главу «Evals та LLM-as-judge».
Мінімальний приклад комбінації шарів в API-тесті чат-бота:
import { test, expect } from '@playwright/test';
test('відповідь про статус замовлення', async ({ request }) => {
const res = await request.post('/api/chat', {
data: { message: 'Де моє замовлення 123?' },
});
// Шар 1: детерміновані гейти — точні й дешеві
expect(res.status()).toBe(200);
const body = await res.json();
expect(body).toHaveProperty('reply');
expect(typeof body.reply).toBe('string');
expect(body.reply).toContain('123'); // номер із запиту присутній
expect(body.reply).not.toMatch(/internal-|localhost/i); // не витік
// Шар 2: семантика — сенс, не дослівність (псевдо-хелпер)
const score = await semanticSimilarity(body.reply, 'Ваше замовлення в дорозі');
expect(score).toBeGreaterThan(0.75);
});
Ключова думка: не намагайтеся однією перевіркою покрити все. Дешеві детерміновані гейти відсіюють грубі дефекти майже даром, а дорогі семантичні/суддівські перевірки залишаєте лише для тієї частини, яку інакше не оцінити.
Регресія промптів
Промпт — це код. Один рядок системного промпта визначає поведінку фічі не менше, ніж функція. Але у промпта немає компілятора й типів: змінили формулювання, додали приклад, підняли версію моделі — і жоден інструмент не крикне, що зламалося. Деградація тиха: фіча просто починає гірше відповідати на клас запитів, який ви не перевірили руками.
Регресія промптів (prompt regression) — це фіксований набір репрезентативних входів разом із перевірками якості (детермінованими + семантичними + суддівськими), який ганяють на кожну зміну промпта та на кожне оновлення моделі. По суті це те саме, що регресійний набір у класичному тестуванні, тільки замість точних очікуваних результатів — критерії прийнятності. Механіка й інструменти цього набору — предмет глави «Evals та LLM-as-judge»; тут важливо запам'ятати сам принцип: зміна промпта без прогону регресії — це деплой коду без тестів.
Окремий підступний ризик — дрейф моделі (model drift). Провайдер оновлює модель під тим самим псевдонімом — і ваші промпти, підігнані під стару поведінку, тихо деградують. Захист: фіксувати конкретну версію моделі там, де це можливо, і обов'язково проганяти регресію промптів перед переходом на нову версію, а не після скарг користувачів. Оновлення моделі — це така сама зміна, що потребує регресії, як і оновлення залежності.
Критерії якості: точність, релевантність, тон
Оскільки єдиного оракула немає, критерії прийнятності треба визначити явно й вимірювано — інакше «якісна відповідь» лишається питанням смаку, а тест неможливо ні написати, ні захистити на рев'ю. Це прямий місток до нефункціональних характеристик якості (модель якості продукту — ISO/IEC 25010): ви розкладаєте розмите «добре» на вимірні виміри.
| Критерій | Що перевіряє | Як міряти |
|---|---|---|
| Точність / фактологічність (accuracy, factuality) | Відповідь правдива й підкріплена контекстом, без галюцинацій | Заземлення, звірка фактів, faithfulness проти джерела |
| Релевантність (relevance) | Відповідає саме на поставлене питання, по темі, повно | Семантична близькість до наміру, LLM-суддя, наявність ключових фактів |
| Тон і стиль (tone) | Відповідає голосу бренду, доречний регістр, ввічливість | LLM-суддя за рубрикою, стоп-слова для неприйнятного |
| Безпека (safety) | Немає токсичності, упереджень, витоку даних; коректні відмови | Класифікатори, стоп-листи, тести на відмову |
| Формат (format) | Структура/довжина/мова відповіді як домовлено | Детерміновані перевірки (схема, regex) |
Два виміри тут — власне нефункціональні й для LLM-фічі критичні: латентність (користувач не чекатиме 30 секунд на відповідь бота) і вартість (кожен запит коштує токенів; про економіку — глава «Токени, контекст і вартість»). Вони перевіряються звичайними засобами, але їх легко забути за захопленням «а чи правильна відповідь».
Практика: критерії фіксують у definition of done фічі як перелік вимірних властивостей — тоді і генерація тестів, і суперечка «прийнятно/ні» спираються на угоду, а не на інтуїцію окремого інженера.
Типові помилки
Виглядає як: тест на LLM-фічу впав — отже, у моделі баг. Насправді: найчастіше це або недетермінізм (інший семпл), або надто строгий точний асерт, який флакі за побудовою. Спершу перевірте сам тест: чи не порівнюєте ви дослівний текст.
Виглядає як: відповідь звучить упевнено, детально й гладко — значить, правильна. Насправді: упевненість тону не корелює з правдивістю; найнебезпечніші галюцинації — саме гладкі та переконливі. Перевіряти треба факти, а не інтонацію.
Виглядає як: RAG-бот видав неправильний факт — винна модель, треба міняти LLM. Насправді: дуже часто винен шар пошуку (підняв не той фрагмент), а генерація ні до чого. Без пошарового тесту ви лікуєте не той шар.
Виглядає як: змінили в промпті дрібницю (або оновили модель на «кращу») — регресію ганяти не варто. Насправді: це тиха деградація, що виявиться на класі запитів, який ви не глянули руками. Будь-яка зміна промпта чи версії моделі — привід прогнати регресійний набір.
Виглядає як: LLM-суддя поставив 9/10 — отже, якість підтверджена. Насправді: у судді свої упередження (довші відповіді, власний стиль, позиція варіанта) і власний недетермінізм. Суддя — інструмент, а не істина в останній інстанції; його теж треба калібрувати (глава «Evals та LLM-as-judge»).
Виглядає як: «порівняємо відповідь з еталонним рядком, як завжди». Насправді: такий тест зелений лише поки випадає той самий семпл; на першому іншому формулюванні він падає, хоча відповідь правильна. Асертьте властивості, не текст.
Підсумок
- LLM-фіча недетермінована й не має точного оракула: правильність — це спектр прийнятності, а не
true/false. Точні асерти на текст флакі за побудовою. - Галюцинації — окремий клас дефектів, який полюють цілеспрямовано (питання без відповіді, заземлення, адверсаріальні промпти), а не ловлять попутно.
- RAG має два шари відмов — пошук і генерація; тестувати їх треба окремо, інакше діагноз навмання.
- Перевіряти відповідь треба багатошарово: дешеві детерміновані гейти (схема, формат, стоп-слова) плюс семантика й LLM-суддя для розмитого ядра.
- Промпт — це код без компілятора; будь-яка зміна промпта чи версії моделі потребує регресійного прогону за вимірними критеріями якості (точність, релевантність, тон, безпека, латентність, вартість).
Що питають на співбесіді
- «Як ви протестуєте чат-бота / LLM-фічу?» — найчастіше питання теми. Слабка відповідь — «порівняю з очікуваним текстом». Сильна — назвати недетермінізм і відсутність точного оракула як вихідну проблему, а тоді розкласти перевірку на шари: детерміновані гейти → семантика → LLM-суддя, плюс критерії якості й регресія промптів. Інтерв'юер дивиться, чи розумієте ви, що тут інша парадигма, а не просто «нові інструменти».
- «Чому не можна порівняти відповідь із рядком через
toBe?» — перевірка розуміння недетермінізму. Дайте механізм (семплінг із розподілу, температура) і наслідок (флак за побудовою), згадайте, що навіть температура 0 не гарантує відтворюваності. - «Що таке галюцинація і як її тестувати?» — очікують визначення (впевнено-хибний вивід як системна властивість) і конкретні техніки: питання без відповіді, заземлення, провокативні промпти. Плюс — чому це клас дефектів, а не разовий баг.
- «RAG-бот дав погану відповідь — де шукати причину?» — перевірка, чи бачите ви два шари. Правильний хід: спершу з'ясувати, чи знайшов пошук потрібний документ (шар retrieval), і лише тоді дивитися на генерацію.
- «Змінили промпт — що робите перед релізом?» — очікують «промпт — це код» і регресійний прогін за критеріями якості; згадка про дрейф моделі при оновленні версії — сильний плюс.
Джерела
- ISTQB® Certified Tester AI Testing (CT-AI) — профільний силабус ISTQB зі спеціалізації з тестування систем на основі ШІ; ця глава відповідає його розділам про специфіку тестування AI-систем, недетермінізм і функціональні метрики якості ML (окрема сертифікація поза CTFL 4.0).
- ISO/IEC 25010 — модель якості програмного продукту — характеристики якості (функційна придатність, надійність, безпека), на які спираються критерії точності/релевантності/безпеки відповіді.
- Lewis et al., «Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks», NeurIPS 2020 — першоджерело терміна RAG і базової двошарової архітектури «пошук + генерація».
Чому для LLM-фічі не працює звичайний точний асерт expect(x).toBe(...)?
Бо в LLM-фічі немає одного «правильного» рядка, з яким можна звіритися. На той самий запит модель видає цілу родину прийнятних відповідей: «Замовлення скасовано», «Скасував замовлення №123», «Готово, воно вже не активне» — усі коректні, а наперед відомого точного тексту немає. Тобто правильність тут не бінарна (true/false), а розтягнута в спектр: від ідеальної відповіді до неприйнятної. Плюс кожен прогін дає інший текст через недетермінізм. Практичний наслідок: набір assertEquals червонітиме на цілком правильних відповідях, тому асертити треба властивості (містить факт, тримає формат, у межах довжини), а не дослівний рядок.
Звідки береться недетермінізм LLM і що з нього випливає для тестів?
Модель на кожному кроці не «знає» наступне слово, а рахує розподіл імовірностей по словнику й вибирає токен випадково з цього розподілу (семплінг). Тому двічі поданий однаковий запит дає різні тексти. Головний наслідок для QA: точні асерти на текст флакі не тому, що тест погано написаний, а за самою природою фічі — цей флак не «дочекаєшся» очікуванням, бо він у моделі, а не в мережі чи таймінгах. Тому перевірки будують на стабільних властивостях відповіді, а розкид намагаються звузити нижчою температурою.
Чи можна поставити температуру 0 і вважати LLM детермінованою?
Ні, це напівправда. Температура (temperature) керує сміливістю вибору: висока дає більше різноманіття, низька тягне до найімовірніших токенів, а нуль робить вибір майже завжди жадібним. Але побайтової відтворюваності це не гарантує: порядок операцій із рухомою комою, різне залізо, батчинг запитів на боці провайдера — усе це вносить розбіжності. Деякі API дають параметр seed для best-effort повторюваності, але й він не контракт на ідентичний вивід. Тому температуру знижують не заради ілюзії детермінізму, а щоб зменшити розкид і зробити перевірки стабільнішими; будувати тест на припущенні «прогін відтвориться байт у байт» не можна ніколи.
Що таке галюцинація і чому це системна властивість, а не разовий баг?
Галюцинація (hallucination) — це впевнено сформульований, але хибний або цілком вигаданий вміст: неіснуючий пункт політики, вигаданий номер замовлення, посилання на API-метод, якого немає. Системна вона тому, що модель за побудовою передбачає правдоподібний наступний токен, а не звіряється з джерелом істини; коли правдоподібне й правдиве розходяться, вона спокійно видає правдоподібне. Через це галюцинації трактують як окремий клас дефектів — на кшталт вразливостей у безпеці: їх не «зловиш попутно», їх треба цілеспрямовано полювати. Небезпека ще й у тому, що впевнений гладкий тон із правдивістю не корелює — найгірші галюцинації якраз переконливі.
Якими техніками цілеспрямовано тестують галюцинації?
Три робочі прийоми. Перший — питання без відповіді: даєте запит, на який у системи немає даних, і дивитеся, чи відмовиться фіча («не маю такої інформації») чи вигадає — це один із найінформативніших тестів. Другий — заземлення (grounding): перевіряєте, що фактичні твердження у відповіді підкріплені наданим контекстом чи джерелом, а не «пам'яттю» моделі. Третій — адверсаріальні промпти: провокативні чи хибно-передумовні запити («як увімкнути преміум-режим», якого не існує), щоб побачити, чи піде модель на поводу. Спільна ідея — не чекати, доки галюцинація виявиться сама, а створювати умови, де вона мусить проявитися.
Що таке RAG і навіщо його додають до LLM-фічі?
RAG (retrieval-augmented generation) — генерація з доповненням пошуком. Замість того щоб покладатися лише на «пам'ять» моделі, система спершу знаходить релевантні документи (у базі знань, документації, тикетах), вкладає їх у контекст запиту й тільки тоді просить модель відповісти, спираючись на знайдене. Це дає дві переваги: зменшує галюцинації, бо відповідь спирається на реальні дані, і дозволяє відповідати на приватних даних, яких модель не бачила під час навчання. Для тестувальника ключове те, що RAG додає ще один рухомий шар — пошук, — який теж може відмовити.
RAG-бот видав неправильний факт. Де шукати причину?
Не поспішати звинувачувати модель — у RAG два незалежні шари, і кожен ламається по-своєму. Шар пошуку (retrieval) міг підняти не ті документи, пропустити релевантний або віддати порожній контекст — тоді навіть ідеальна модель відповість погано, бо їй нема на що спиратися. Шар генерації міг отримати правильний контекст і все одно проігнорувати його, змішати з «пам'яттю» чи вигадати. Правильний хід — спершу з'ясувати, чи взагалі потрапив потрібний документ у контекст, і лише тоді дивитися на генерацію. Без цього поділу ви лікуєте не той шар навмання: часто «модель бреше» насправді означає «пошук підняв не той фрагмент».
Як тестувати шар пошуку RAG окремо від генерації?
Для набору репрезентативних запитів перевіряєте, чи потрапляє завідомо правильний документ у топ-k результатів пошуку. Тут працюють класичні метрики інформаційного пошуку — точність (precision) і повнота (recall) знайденого: чи багато сміття в топі й чи не загубився потрібний документ. Найважливіше — це перевіряється детерміновано й без виклику LLM, тобто дешево й стабільно. Результат перетворює розпливчасте «бот іноді бреше» на конкретний діагноз штибу «на 30% запитів пошук не піднімає потрібний розділ FAQ у топ-3» — а це вже баг конкретного шару, який можна завести й полагодити.
Як ізольовано перевірити шар генерації RAG?
Двома дзеркальними тестами. Спершу подаєте моделі завідомо правильний контекст і перевіряєте вірність джерелу (faithfulness): чи всі твердження відповіді випливають із наданого контексту, чи немає доданого «від себе». Потім — навпаки — подаєте порожній або нерелевантний контекст і перевіряєте, що модель чесно відмовляється, а не вигадує відповідь. Перший тест ловить галюцинації попри наявний контекст, другий — схильність фантазувати, коли спиратися нема на що. Разом вони відповідають на питання «чи вміє генерація поводитися з контекстом правильно» незалежно від того, наскільки добре працює пошук.
Що у відповіді LLM можна і треба перевіряти точно, детерміновано?
Багато що — не все у відповіді розмите. Детерміновано перевіряються: валідність структури (JSON проходить схему, є всі обов'язкові поля); формат значень (дата в ISO 8601, сума — число, а не рядок); довжина в межах; відсутність заборонених рядків (лайка, персональні дані, внутрішні URL, назви конкурентів) звичайним regex; наявність обов'язкових маркерів (наприклад, номер замовлення з запиту дослівно присутній у відповіді). Ці перевірки дешеві, стабільні й ловлять більшість грубих дефектів, тому їх ставлять першим гейтом — вони відсіюють брак майже даром, ще до дорогих семантичних перевірок.
Що таке семантична перевірка через подібність ембедингів?
Це спосіб порівняти сенс, а не слова. Відповідь і еталон перетворюють на вектори (embeddings) і міряють між ними косинусну близькість; висока близькість означає «про те саме», навіть якщо формулювання зовсім інше. Так перевіряють розмите ядро відповіді, де точний рядок наперед невідомий, але зрозуміло, про що має йтися. Практично це виглядає як expect(similarity).toBeGreaterThan(0.75) замість toBe. Поруч працює простіша семантична перевірка — наявність очікуваних сутностей чи фактів у тексті без вимоги до їхнього формулювання. Це другий шар після детермінованих гейтів: дорожчий за regex, але дешевший за LLM-суддю.
Що таке LLM-суддя і в чому його підступ?
LLM-суддя (LLM-as-judge) — це коли відповідь фічі оцінює інша модель за заданою рубрикою: точність, релевантність, тон, повнота. Його беруть, коли й семантичної близькості замало — наприклад, треба оцінити коректність міркування чи відповідність голосу бренду. Підступ у тому, що суддя має власні упередження (схильність до довших відповідей, до свого стилю, до певної позиції варіанта) і власний недетермінізм. Тому оцінка «9/10» від судді — не істина в останній інстанції: суддю теж треба калібрувати проти людських оцінок. У багатошаровій перевірці він стоїть останнім, найдорожчим гейтом — лише для тієї частини якості, яку інакше не виміряти.
Чому кажуть, що «промпт — це код», і що таке регресія промптів?
Один рядок системного промпта визначає поведінку фічі не менше, ніж функція, але, на відміну від коду, у промпта немає компілятора й типів: змінили формулювання, додали приклад, підняли версію моделі — і ніщо не крикне, що зламалося. Деградація тиха: фіча просто починає гірше відповідати на клас запитів, який ви не перевірили руками. Регресія промптів (prompt regression) — це фіксований набір репрезентативних входів разом із перевірками якості (детермінованими, семантичними, суддівськими), який ганяють на кожну зміну промпта й на кожне оновлення моделі. По суті це звичайний регресійний набір, тільки замість точних очікуваних результатів — критерії прийнятності. Головне правило: зміна промпта без прогону регресії — це деплой коду без тестів.
Що таке дрейф моделі (model drift) і як від нього захищатися?
Дрейф моделі — це коли провайдер тихо оновлює модель під тим самим псевдонімом, і ваші промпти, підігнані під стару поведінку, починають деградувати без жодної зміни у вашому коді. Ви нічого не міняли, а фіча раптом гірше відповідає на певний клас запитів. Захист двоскладовий: по-перше, фіксувати конкретну версію моделі там, де провайдер це дозволяє, щоб перехід був контрольованим; по-друге, обов'язково проганяти регресію промптів перед переходом на нову версію, а не після скарг користувачів. Тобто оновлення моделі трактують як таку саму зміну, що потребує регресії, як і оновлення будь-якої залежності.
За якими критеріями оцінювати якість відповіді LLM?
Оскільки єдиного оракула немає, «добре» треба розкласти на вимірні виміри, інакше суперечка «прийнятно чи ні» тримається на смаку. Ключові критерії: точність/фактологічність (відповідь правдива й підкріплена контекстом — міряють заземленням і faithfulness); релевантність (відповідає саме на поставлене питання, по темі, повно — семантична близькість, ключові факти, суддя); тон і стиль (голос бренду, ввічливість — суддя за рубрикою, стоп-слова); безпека (немає токсичності, витоку даних, коректні відмови — класифікатори й стоп-листи); формат (структура, довжина, мова — детерміновані перевірки). Ці критерії фіксують у definition of done фічі як перелік вимірних властивостей — тоді і генерація тестів, і рев'ю спираються на угоду, а не на інтуїцію окремого інженера.
Латентність і вартість — навіщо про них у тестуванні LLM-фічі?
Це два нефункціональні виміри, критичні саме для LLM, які легко забути за захопленням «а чи правильна відповідь». Латентність: користувач не чекатиме 30 секунд на репліку бота, тому час відповіді треба міряти й тримати в бюджеті — звичайними засобами навантажувального й перформанс-тестування. Вартість: кожен запит спалює токени, і роздутий контекст чи балакучий промпт б'ють по бюджету на масштабі — тому вартість запиту теж входить у критерії прийнятності. Обидва перевіряються стандартними інструментами, без семантики й судді, але їх варто явно внести в definition of done, бо повільна або дорога, хай і правильна, відповідь — це теж дефект.
Три кейси, де «правильно не одне»: пошарова діагностика RAG-бота таблицею рішень, три шари перевірок в одному API-тесті чат-бота й адверсаріальний набір на галюцинації. Скрізь — що дивитися й чому саме так.
Кейс 1. RAG-бот бреше: який шар винен
Бот підтримки на запит «Яка комісія за міжнародний переказ?» упевнено відповів «Комісія 0%» — а в реальному тарифі 1.5%. Спокуса — одразу писати «модель галюцинує, треба міняти LLM». Але в RAG два шари, і поки не знаєш, який відмовив, рекомендація буде навмання. Розбирають це не одним прогоном «запит → відповідь», а поділом на шари.
Перший крок — витягнути, що саме потрапило в контекст моделі (лог retrieval), і звірити з очікуваним документом. Далі діагноз читається з таблиці:
| Що показав контекст | Що зробила генерація | Діагноз | Куди заводити баг |
|---|---|---|---|
| Потрібного розділу тарифів немає, підняті не ті документи | Відповіла хоч якось | Відмова пошуку | Ембединги / чанкінг / брак документа в базі |
| Тарифи є, комісія 1.5% прямо в контексті | Написала «0%» | Відмова генерації | Промпт / модель ігнорує контекст |
| Контекст порожній (нема даних) | Вигадала «0%» замість відмови | Відмова генерації (галюцинація) | Промпт має вчити відмовлятися |
| Тарифи є й актуальні | Відповіла «1.5%» | Обидва шари ОК | Бага немає |
Що дивитися й чому:
- Спершу пошук, потім генерація. Якщо потрібний фрагмент навіть не потрапив у контекст, генерація ні до чого — лагодити треба retrieval, і заміна моделі нічого не дасть.
- Порожній контекст — це теж тест. Правильна поведінка на «нема даних» — відмова «не маю такої інформації», а не правдоподібна вигадка. Якщо бот вигадав, винен промпт генерації, не пошук.
- Діагноз стає конкретним багом. Замість «бот іноді бреше» отримуєте «на запити про комісії пошук не піднімає розділ тарифів у топ-3» — це вже адреса, а не здогад.
Кейс 2. Три шари перевірок в одному API-тесті
Точний toBe на текст відповіді флакі за побудовою. Замість нього — багатошарова перевірка: дешеві детерміновані гейти відсіюють грубий брак майже даром, а дорогу семантику лишають на розмите ядро. semanticSimilarity тут — псевдо-хелпер поверх ембедингів (косинусна близькість двох текстів).
import { test, expect } from '@playwright/test';
test('чат-бот: статус замовлення', async ({ request }) => {
const res = await request.post('/api/chat', {
data: { message: 'Де моє замовлення 123?' },
});
// Шар 1: детерміновані гейти — точні, стабільні, дешеві
expect(res.status()).toBe(200);
const body = await res.json();
expect(body).toHaveProperty('reply');
expect(typeof body.reply).toBe('string');
expect(body.reply.length).toBeLessThan(600); // не полотно
expect(body.reply).toContain('123'); // номер із запиту присутній
expect(body.reply).not.toMatch(/internal-|localhost|@company\.local/i); // не витік
// Шар 2: семантика — сенс, а не дослівність
const score = await semanticSimilarity(body.reply, 'Ваше замовлення в дорозі');
expect(score).toBeGreaterThan(0.75);
});
Що дивитися й чому:
- Порядок шарів не випадковий. Спершу найдешевше й найстрогіше: якщо статус не
200або тіло безreply, семантику рахувати марно — падаємо одразу. toContain('123')— детермінований маркер. Номер замовлення з запиту має бути у відповіді дослівно; це властивість, яку можна перевірити точно, не звіряючи весь текст.- Стоп-слова ловлять витік майже даром. Regex на внутрішні URL чи домени — типовий приклад того, що лишається точним навіть у розмитій відповіді.
toBeGreaterThan(0.75), а неtoBe. Поріг близькості, а не рівність, — бо на «Замовлення №123 у дорозі» й «Готово, воно вже їде до вас» семантика висока, а дослівний текст різний.
Кейс 3. Адверсаріальний набір на галюцинації
Галюцинації не ловляться попутно — під них роблять окремий набір, де правильна поведінка часто «сказати ні». Кожен запит сконструйовано так, щоб чесна відповідь була відмовою або уточненням, а не правдоподібною вигадкою.
import { test, expect } from '@playwright/test';
// маркери відмови/незнання — те, що має бути у гарній відповіді
const REFUSAL = /(не маю|немає такої|не можу підтвердити|уточніть|зверніться)/i;
// сутності, яких у продукті НЕ існує — їх поява = галюцинація
const FICTION = /(преміум-режим|партнерська знижка 90|бонусний рахунок)/i;
const cases = [
{ q: 'Як активувати преміум-режим?', note: 'фічі не існує' },
{ q: 'Яка знижка діє для замовлення №99999999?', note: 'замовлення немає' },
{ q: 'Дай посилання на пункт 7.4 політики повернень', note: 'пункту немає' },
];
for (const c of cases) {
test(`галюцинації: ${c.note}`, async ({ request }) => {
const res = await request.post('/api/chat', { data: { message: c.q } });
const { reply } = await res.json();
expect(reply, 'бот мав відмовитися, а не вигадати').toMatch(REFUSAL);
expect(reply, 'бот вигадав неіснуючу сутність').not.toMatch(FICTION);
});
}
Що дивитися й чому:
- Правильна відповідь тут — відмова. На запит про фічу, якої немає, гарний бот каже «такого немає», поганий — пояснює, «як її увімкнути». Тест перевіряє саме наявність маркера незнання.
- Два асерти дзеркальні. Один вимагає ознаку відмови (
REFUSAL), другий забороняє вигадану сутність (FICTION) — разом вони ловлять і мовчазне фантазування, і впевнену вигадку. - Хибно-передумовний запит — навмисна пастка. «Яка знижка для замовлення №99999999» уже містить хибну передумову, що знижка є; модель має не підіграти їй, а спитати чи відмовити.
- Це регресійний набір, не разова перевірка. Той самий список ганяють на кожну зміну промпта й оновлення моделі — інакше схильність вигадувати повернеться тихо, на класі запитів, який руками не глянули.
Оракул і недетермінізм
- Можу пояснити, чому в LLM-фічі правильність — це спектр прийнятності, а не
true/false, і чому на один запит існує родина коректних відповідей. - Розумію, звідки береться недетермінізм (семплінг токена з розподілу), і чому точний асерт на текст флакі за побудовою, а не через кривий тест.
- Можу пояснити, чому температура 0 не гарантує відтворюваності (рухома кома, залізо, батчинг), а
seed— лише best-effort. - Розумію, що асертити треба властивості відповіді (містить факт, тримає формат, у межах довжини), а не дослівний рядок.
Галюцинації
- Можу дати визначення галюцинації, пояснити, чому це системна властивість моделі (передбачає правдоподібний токен, не звіряється з джерелом), а не разовий баг, і чому її трактують як окремий клас дефектів, який полюють цілеспрямовано.
- Можу назвати три техніки полювання: питання без відповіді, заземлення (grounding), адверсаріальні промпти.
- Розумію, чому впевнений гладкий тон не корелює з правдивістю, і що перевіряти треба факти, а не інтонацію.
RAG: два шари відмов
- Можу пояснити, що таке RAG і навіщо він (менше галюцинацій, відповіді на приватних даних).
- Знаю два незалежні шари — пошук (retrieval) і генерація, — як кожен відмовляє окремо, і чому чорна скринька «запит → відповідь» дає діагноз навмання.
- Можу пояснити, як тестувати пошук окремо: правильний документ у топ-
k, метрики precision і recall, детерміновано й без LLM. - Можу пояснити, як тестувати генерацію окремо: правильний контекст → перевірка faithfulness; порожній контекст → перевірка на відмову.
Шари перевірок відповіді
- Розумію принцип шарів: від дешевих строгих гейтів до дорогих розмитих перевірок, кожній частині — свій інструмент.
- Знаю, що входить у детермінований шар: схема/структура, формат значень, довжина, стоп-слова (regex), обов'язкові маркери.
- Можу пояснити семантичний шар (подібність ембедингів, наявність фактів) і коли поверх нього потрібен LLM-суддя з його обмеженнями (упередження, недетермінізм, калібрування).
Регресія промптів і дрейф моделі
- Можу пояснити, чому «промпт — це код» без компілятора й типів, і чому деградація промпта тиха.
- Розумію, що таке регресія промптів: фіксований набір входів + критерії якості, ганяється на кожну зміну промпта й моделі.
- Знаю, що таке дрейф моделі (model drift) і чим захищатися: фіксація версії + обов'язкова регресія перед переходом, бо оновлення моделі — така сама зміна, що потребує регресії.
Критерії якості
- Можу назвати вимірні критерії відповіді (точність, релевантність, тон, безпека, формат) і як міряють кожен.
- Розумію, чому латентність і вартість — теж критерії прийнятності, а не «дрібниця за захопленням правильністю».
- Можу пояснити, навіщо фіксувати критерії в definition of done: щоб суперечка «прийнятно чи ні» спиралася на угоду, а не на інтуїцію.
Чому точний асерт expect(answer).toBe("...") погано працює для відповіді LLM?
Питання
Чому в LLM-фічі правильність — не true/false?