QA Interview Hub · База знань · Тести

AI

Як тестувати LLM-агента

Чому LLM не тестується як звичайний код

Класичний тест: фіксований вхід → expect(actual).toBe(expected). З LLM це ламається з трьох причин:

  • Недетермінізм. Той самий вхід дає різні, але однаково правильні відповіді: «Замовлення скасовано» і «Ваше замовлення успішно скасовано». Рівність рядків не працює;
  • Проблема оракула. Для вільного тексту немає єдиної «правильної» відповіді, тож неочевидно, з чим порівнювати;
  • Неявні залежності. Поведінку змінюють версія моделі, промпт, температура, дані в RAG, результати tools — і жодна зміна не схожа на «зламаний код».

Тому перевіряють властивості, а не точний текст, і міряють частку успіху на наборі прикладів, а не «пройшов / не пройшов» на одному:

  • Детерміновані перевірки там, де можна: валідний JSON за схемою, обов'язкові поля, відсутність заборонених слів, довжина, потрібний tool викликано з правильними аргументами, дані в БД змінилися;
  • Семантичні перевірки для вільного тексту: схожість за embeddings, перевірка фактів з довідки, LLM-as-judge;
  • Статистика: запускати кейс N разів і вимагати, наприклад, ≥ 90 % успіху, або порівнювати середню оцінку між версіями.

Піраміда лишається: найбільше швидких детермінованих тестів із замоканою моделлю (логіка навколо LLM: парсинг, маршрутизація, обробка помилок), менше eval-набору з реальною моделлю, ще менше e2e.

Eval-набір, метрики й LLM-as-judge

Eval (evaluation) — автоматизований прогін набору прикладів через систему з оцінкою результатів. Це «регресійні тести» для LLM-функціональності.

Golden dataset — курований набір кейсів: вхід, очікувана поведінка (не обов'язково точний текст), теги (категорія, складність). Звідки брати: реальні логи й тікети (з анонімізацією), крайні випадки, відомі баги (кожен знайдений баг стає кейсом), синтетичні приклади, згенеровані моделлю й перевірені людиною. Набір має покривати негативні сценарії: відмови, неоднозначні запити, спроби обійти правила.

LLM-as-judge: інша модель оцінює відповідь за рубрикою («чи спирається відповідь лише на надані документи? 1–5», «так/ні: чи є в тексті персональні дані»). Практики, щоб суддя був надійним:

  • чітка рубрика й бінарні або вузькі шкали замість «оціни якість»;
  • просити коротке обґрунтування перед оцінкою;
  • судити іншою моделлю, ніж тестована (self-preference bias), температура 0;
  • калібрувати суддю на розмічених людиною прикладах і міряти згоду з людьми;
  • враховувати bias позиції та довжини: довша відповідь не обов'язково краща.

Метрики: pass rate по категоріях, а не одне середнє; частка відмов; середнє та p95 токенів, вартість і latency на кейс. Кожна зміна промпту, моделі чи RAG порівнюється з базовою лінією на тому самому наборі.

Тестування агента: tools, траєкторія, результат

Агент складніший за чат: у нього є кроки і побічні ефекти. Перевіряють три рівні:

  1. Окремий tool — звичайні тести: контракт, валідація аргументів, помилки, ідемпотентність. LLM тут не потрібна;
  2. Рішення моделі — чи обрано правильний tool з правильними аргументами для цього запиту (tool selection accuracy), чи не викликано зайвих;
  3. Траєкторія й результат — уся послідовність кроків і підсумок у світі (стан БД, створений тікет), а не лише фінальний текст.

Основний прийом — записувати траєкторію (трейс усіх повідомлень і викликів) і робити асерти по ній та по стану системи:

test('refund request creates exactly one refund', async () => {
  const run = await agent.run('Поверніть гроші за замовлення 1042', { seed: fixtureDb });

  // шлях: правильні tools, без зайвих
  expect(run.toolCalls.map((c) => c.name)).toEqual(['get_order', 'create_refund']);
  expect(run.toolCalls[1].args).toMatchObject({ orderId: 1042 });

  // результат у світі, а не лише текст
  expect(await db.refunds.count({ orderId: 1042 })).toBe(1);

  // обмеження: бюджет і кроки
  expect(run.steps).toBeLessThanOrEqual(6);
  expect(run.usage.totalTokens).toBeLessThan(8000);
});

Практики:

  • Мокати зовнішній світ (tools, API, час) для швидких і стабільних прогонів; окремо мати набір із реальними інтеграціями в sandbox;
  • Не вимагати єдиного шляху, якщо їх кілька правильних: перевіряти обов'язкові кроки, заборонені дії й кінцевий стан;
  • Record / replay: записані відповіді моделі для детермінованих регресій логіки агента;
  • Побічні ефекти: ідемпотентність, підтвердження небезпечних дій людиною (human-in-the-loop), права tools за принципом мінімальних привілеїв;
  • Багатокроковий діалог: втрата контексту, суперечливі уточнення, зміна наміру;
  • Збої: tool повернув помилку чи таймаут, порожню відповідь, неочікуваний формат — агент не має зациклитись чи вигадати результат;
  • Ліміти: макс. кроків, токенів, часу, вартості; поведінка при досягненні ліміту.

Evals у CI і моніторинг у проді

Тестування LLM-системи не закінчується релізом: поведінка дрейфує від оновлень моделі, змін даних і нових типів запитів користувачів.

У CI:

  • швидкий шар без мережі: unit-тести парсингу, маршрутизації, схем, обробки помилок з моканою моделлю — на кожен коміт;
  • eval-набір з реальною моделлю — на PR, що чіпає промпт, модель, tools чи RAG (дорого й повільно, тому підмножина «smoke» + повний набір вночі);
  • quality gate: pass rate не нижче порога й не гірше за базову лінію більш ніж на допустиму похибку; ліміти вартості й latency;
  • зафіксована версія моделі й промптів, щоб різниця була пояснюваною; на недетермінізм закладають повтори й допуск.

У продакшні:

  • Observability: логувати (з обережністю щодо PII) промпт, версію моделі, tool calls, токени, latency, вартість; трейс на кожен запит;
  • Онлайн-оцінка: вибірка реальних діалогів проганяється через суддю; зворотний зв'язок користувачів (👍/👎) і ескалації;
  • Канарковий реліз і A/B нової моделі чи промпту;
  • Алерти: стрибок вартості, частки відмов, помилок tools, довжини відповідей;
  • Кожен реальний інцидент → новий кейс у golden dataset.