vyvchy
    Теми розділу

    14 · AI для QA

    Тестування 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 має два незалежні шари, і кожен відмовляє по-своєму:

    Ні

    Так

    Ні

    Так

    Запит користувача

    Шар пошуку
    retrieval

    Знайшов правильні
    документи?

    Відмова 1
    не той контекст

    Шар генерації
    LLM

    Відповідь спирається
    на контекст?

    Відмова 2
    галюцинація попри контекст

    Обґрунтована відповідь

    Ні

    Так

    Ні

    Так

    Запит користувача

    Шар пошуку
    retrieval

    Знайшов правильні
    документи?

    Відмова 1
    не той контекст

    Шар генерації
    LLM

    Відповідь спирається
    на контекст?

    Відмова 2
    галюцинація попри контекст

    Обґрунтована відповідь

    • Відмова шару пошуку. Знайдено не ті документи, релевантні пропущені, або контекст порожній. Тоді навіть ідеальна модель відповість погано — їй просто нема на що спиратися. Причини: слабкі ембединги, невдале нарізання документів на фрагменти (chunking), брак потрібного документа в базі.
    • Відмова шару генерації. Пошук спрацював, потрібний фрагмент у контексті — а модель усе одно проігнорувала його, змішала з «пам'яттю» або вигадала. Це вже не проблема пошуку.

    Головна практична помилка — тестувати RAG як чорну скриньку «запит → відповідь». Тоді на поганій відповіді ви не знаєте, який шар винен, і рекомендація буде навмання. Тестувати треба пошарово:

    • Пошук окремо. Для набору запитів перевірте, чи потрапляє правильний документ у топ-k результатів. Тут працюють класичні метрики пошуку — точність (precision) і повнота (recall) знайденого. Це детерміновано перевіряється й не потребує LLM.
    • Генерацію окремо. Подайте моделі завідомо правильний контекст і перевірте вірність джерелу (faithfulness): чи всі твердження відповіді випливають із контексту, чи немає доданого «від себе». А тоді — подайте порожній чи нерелевантний контекст і перевірте, що модель відмовляється, а не вигадує.

    Такий поділ перетворює невиразне «бот іноді бреше» на діагноз: «на 30% запитів пошук не піднімає потрібний розділ FAQ у топ-3» — і це вже конкретний баг конкретного шару.

    Семантичні перевірки замість точного порівняння

    Якщо toBe не працює, чим асертити? Відповідь: багатошарово, від найдешевших і найстрогіших перевірок до найдорожчих і найрозмитіших. Не все у відповіді розмите — багато що можна й треба перевіряти точно.

    Провал

    Пройшов

    Провал

    Пройшов

    Відповідь LLM

    Детерміновані гейти
    схема, формат, стоп-слова

    Fail одразу — дешево

    Семантичні перевірки
    подібність ембедингів, наявність фактів

    LLM-суддя
    точність, релевантність, тон

    Оцінка якості

    Провал

    Пройшов

    Провал

    Пройшов

    Відповідь LLM

    Детерміновані гейти
    схема, формат, стоп-слова

    Fail одразу — дешево

    Семантичні перевірки
    подібність ембедингів, наявність фактів

    LLM-суддя
    точність, релевантність, тон

    Оцінка якості

    Шар 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), і лише тоді дивитися на генерацію.
    • «Змінили промпт — що робите перед релізом?» — очікують «промпт — це код» і регресійний прогін за критеріями якості; згадка про дрейф моделі при оновленні версії — сильний плюс.

    Джерела