Усі уроки · 44
Шлях 3 · Дорожня карта AI-інженера·Урок 30 з 33

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

Чому «ніби все гаразд» — це не перевірка якості: детерміновані перевірки, LLM у ролі судді й оцінка людьми, плюс крихітний робочий каркас оцінок, який запускається просто в браузері.

РівеньСереднійЧас~15 хв

«Ніби все гаразд» — це не перевірка якості

Ви змінили в системному промпті одне слово, переглянули три чати, усе виглядало нормально — і випустили зміну. Через два дні асистент почав відмовлятися відповідати на запитання, на які раніше відповідав чудово. Ніхто цього не помітив: решту п’ятдесят випадків ніхто не перевіряв.

Цю діру й закривають оцінки.

Якщо простими словами, оцінка (eval) — це тест для програми, яка ніколи не відповідає однаково двічі. Про звичайну функцію можна сказати: «вона має повернути 42». З LLM так не вийде: вона щоразу формулює відповідь по-новому. Тому перевіряють властивість відповіді: вона має згадувати 14-денний термін, ні в якому разі не обіцяти помісячне повернення коштів, бути коректним JSON. Один випадок, одна перевірка, один бал. Запустіть усі — і отримаєте підсумковий бал, який можна порівнювати від релізу до релізу. Величезний набір у перший день ні до чого: вистачить десяти чесних тестових випадків і звички їх запускати.

Чому це важливо

«Ми переглянули три чати, начебто все гаразд» — найдорожча фраза в AI-інженерії. Три чати з тисяч. Набір оцінок — це вибір між двома способами дізнатися про поломку: за чотири хвилини з червоного рядка на екрані або через два дні від розлюченого клієнта.

Конкретний приклад: відповіді іспанською

Ваш бот підтримки відповідає чотирма мовами. Ви переписуєте один рядок системного промпту, щоб бот звучав привітніше, — і в іспанській нове формулювання випадково збігається з тим, що стоїть у повідомленні про відмову. Наслідок: на будь-яке запитання іспанською тепер приходить ввічливе «На жаль, із цим я допомогти не можу», а англійською, німецькою та польською все працює бездоганно. Нічого не падає. Два дні ніхто нічого не помічає: жодного тестового випадку іспанською ніхто не написав. Набір оцінок із трьома випадками на кожну мову впіймає це за чотири хвилини — раніше, ніж це помітить клієнт.

Слова, які ви зустрінете

Слово Що воно означає тут
Оцінка (eval) Один тестовий випадок: запитання й правило, якому має відповідати хороша відповідь.
Набір оцінок (eval suite) Усі ваші випадки разом, що запускаються як одне завдання.
Властивість Те, що ви перевіряєте: наприклад, «згадує 14-денний термін» замість «дорівнює X».
Рубрика (rubric) Письмові критерії для судді: що вважати хорошим, що поганим і чому.
Базова лінія (baseline) Результат нинішньої версії. Кожна наступна зміна порівнюється з ним.
Частка пройдених (pass rate) Яка частина випадків пройдена: 47 із 50.
Золотий набір даних (golden dataset) Випадки, які людина перевірила й закріпила як правильну відповідь.
CI Робот, який сам запускає набір за кожної зміни.

Три види перевірок

Детерміновані — тут і досі працює звичний підхід. Детермінованість означає, що на той самий вхід ви завжди отримуєте той самий вердикт і жодного суб’єктивного оцінювання немає. Якщо відповідь має бути коректним JSON, перевірте її парсером. Якщо у відповіді має бути трек-номер, перевірте наявність підрядка. Якщо якоїсь фрази не повинно бути ніколи, знайдіть її через grep. Такі перевірки безкоштовні, миттєві й надійні, тому завжди починайте з них.

На основі моделі (LLM у ролі судді) — для всього, чого не вловить жоден регулярний вираз: «чи спирається відповідь на наданий контекст?», «чи ввічливий тон?». Ви просите сильну модель оцінити відповідь за рубрикою й, в ідеалі, повернути бал разом із причиною. Працює добре, але є два підводні камені: суддя схильний віддавати перевагу довшим і впевненішим відповідям і надто поблажливий до відповідей, які написав сам. Ніколи не доручайте оцінку тій самій моделі, що написала відповідь, і давайте судді чіткі критерії замість «оціни від 1 до 10 на око».

Оцінка людьми — еталон істини й найдорожчий спосіб. Бережіть її для найважливіших випадків: раз на тиждень дивіться випадкову вибірку діалогів із реальної експлуатації та кожен випадок, який модель-суддя позначила як прикордонний. Десять хвилин над справжніми діалогами корисніші за годину здогадок про якість.

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

Що обрати?

Детермінована LLM-суддя Людина
Підходить для Формату, обов’язкових фактів, заборонених фраз Тону, опори на контекст, питання «а це взагалі корисно?» Еталона, крайніх випадків, про які ви не подумали
Вартість Безкоштовно Один додатковий виклик моделі на випадок Ваш час — найдефіцитніший ресурс
Швидкість Миттєво Секунди на випадок Хвилини на випадок
Надійність Абсолютна Зазвичай добра, але завищує оцінки довшим відповідям Так, хоча люди розходяться між собою
Коли брати Правило можна записати Правило записати не можна Потрібна істина або суддя не впевнений

Починайте зліва, а вправо рухайтеся лише тоді, коли без цього не обійтися.

Судді потрібна рубрика

Якщо LLM-судді просто сказати «оціни від 1 до 10», він вигадає власні критерії, а завтра вигадає інші. Дайте йому натомість рубрику — письмовий перелік того, що вважається хорошим, — і вимагайте повернути причину, щоб згодом можна було перевірити його рішення. Вставте цей промпт у свого агента або в чат, і у вас буде перший робочий суддя:

Скопіюйте цей промпт: суддя зі справжньою рубрикою
Ти оцінюєш одну відповідь асистента служби підтримки. Оцінюй суворо й послідовно.

ЗАПИТАННЯ: [запитання користувача]
КОНТЕКСТ, ЯКИЙ МИ ДАЛИ АСИСТЕНТУ: [знайдені документи або «немає»]
ВІДПОВІДЬ ДЛЯ ОЦІНЮВАННЯ: [відповідь асистента]

Для кожного критерію постав pass або fail:
1. ОПОРА НА КОНТЕКСТ: чи підтверджується кожне твердження про факти наданим
 контекстом, чи у відповіді вигадано правило, ціну або дату? Будь-яка
 вигадка — автоматичний fail.
2. ПО СУТІ: чи відповідає вона саме на те запитання, яке поставлено?
3. ФОРМАТ: чи дотримано потрібний формат (дати у вигляді YYYY-MM-DD, без
 заголовків markdown)?
4. ТОН: ввічливий, без зайвих перепрошень, без надмірних обіцянок.

Потім відповідай суворо в такому вигляді:
VERDICT: pass OR fail
FAILED_CRITERIA: [номери непройдених критеріїв або «none»]
REASON: [одне речення з цитатою тих самих слів, через які винесено такий вердикт]

Ніколи не став оцінку з десяти. Ніколи не додумуй факти, яких немає в контексті.

Мінімальний каркас для оцінок

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

evals.py
1# Мінімальний каркас для оцінок: випадки, перевірки, підсумковий бал.
2from mocks import assistant # застосунок, що тестується, — замініть на виклик справжнього API
3 
4# 1. ВИПАДКИ. Кожен випадок — це запитання плюс те, що має робити хороша відповідь.
5# expected_contains: фрази, які МАЮТЬ бути у відповіді
6# forbidden: фрази, яких там НЕ має бути ніколи
7# Почніть із 10 реальних запитань. Поповнюйте набір із реальної експлуатації, а не з уяви.
8CASES = [
9 {
10 #98c379">"question": "Can I refund my annual plan?",
11 #98c379">"expected_contains": ["14 days"],
12 #98c379">"forbidden": ["monthly"],
13 },
14 {
15 #98c379">"question": "Reset my password",
16 #98c379">"expected_contains": ["login page"],
17 #98c379">"forbidden": [],
18 },
19 {
20 #98c379">"question": "What is the meaning of life?",
21 #98c379">"expected_contains": [],
22 #98c379">"forbidden": ["refund"],
23 },
24]
25 
26# 2. ПЕРЕВІРКА. На вході один випадок, на виході True або False.
27# Детермінована: та сама відповідь завжди отримує той самий вердикт.
28# Коли підрядків забракне, замініть цю функцію викликом LLM-судді —
29# цикл нижче при цьому не змінюється.
30def check(case, answer):
31 has_expected = all(s in answer for s in case[#98c379">"expected_contains"])
32 has_forbidden = any(s in answer for s in case[#98c379">"forbidden"])
33 return has_expected and not has_forbidden
34 
35# 3. ЦИКЛ. Прогнати кожен випадок, порахувати пройдені, запам’ятати провалені.
36def run_evals():
37 passed, failed = 0, []
38 for case in CASES:
39 answer = assistant(case[#98c379">"question"])
40 if check(case, answer):
41 passed += 1
42 else:
43 failed.append(case[#98c379">"question"])
44 
45 # 4. ПІДСУМОК. Одне число, яке можна порівняти між двома версіями.
46 print(#98c379">"passed: " + str(passed) + "/" + str(len(CASES)))
47 for q in failed:
48 print(#98c379">"FAIL: " + q) # саме провалене запитання, а не лише їх кількість
49 
50 return passed == len(CASES) # True = можна випускати
51 
52if __name__ == #98c379">"__main__":
53 run_evals()

Чотири частини — ось і весь задум: випадки, перевірка, цикл, підсумковий бал. Детермінована перевірка ловить дві найпоширеніші регресії: модель перестала казати те, що треба, або почала казати те, чого казати не можна. Коли підрядків стане замало, замініть функцію перевірки викликом LLM-судді; цикл і виведення підсумків залишаться незмінними.

Оцінки застарівають, якщо їх ніхто не запускає

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

Перевірте себе

Перевірте себе

Чому функцію на основі LLM не можна тестувати так само, як звичайну: «на цей вхід має повернутися рівно X»?

Ви змінили системний промпт, щоб бот став привітнішим, і тепер на кожне запитання іспанською він ввічливо відмовляє. Що вам дає набір оцінок?

Яку перевірку слід написати першою в новому наборі оцінок?

Навіщо просити LLM-суддю пояснювати причину, а не лише ставити бал?

Що забрати з собою

П’ять кроків, від найдешевших:

  1. Напишіть десять випадків сьогодні — реальні запитання реальних користувачів і те, що хороша відповідь має та чого не має містити.
  2. Перевіряйте безкоштовно все, що можна перевірити безкоштовно: коректний JSON, наявність обов’язкових фраз, відсутність заборонених.
  3. Додавайте суддю лише для того, що підрядки піймати не можуть, — з рубрикою й обов’язковою причиною замість оцінки з десяти.
  4. Запускайте набір за кожної зміни промпту чи моделі. Підключіть його до CI, щоб набір, про який ніхто не пам’ятає, усе одно запускався сорок разів на тиждень.
  5. Фіксуйте базову лінію. Результат, якого ви не записали, нема з чим порівнювати, а порівняння — уся суть.

Якщо ви не можете сказати, що має виконуватися, щоб ваша функція вважалася «хорошою», то функції у вас іще немає. Запишіть це у вигляді десяти випадків — і вона з’явиться.