Większość projektów z LLM-ami pada w ten sam sposób: ktoś pisze prompt, wygląda dobrze na pięciu przykładach, idzie na produkcję, a potem padnie na szóstym — tyle że nikt tego nie zauważa przez trzy miesiące, bo nie ma evala.
Naprawa jest niemodna. Napisz eval zanim napiszesz prompt.
Czym "eval" właściwie jest
Zbiór ewaluacyjny to lista wejść sparowanych z tym, jak wygląda dobry wynik. Tyle. Format zależy od zadania:
- Klasyfikator: wejście → oczekiwana etykieta.
- Drafter: wejście → wzorcowy draft albo rubryka oceny.
- Agent wieloetapowy: wejście → lista narzędzi, które powinny zostać wywołane + akceptowalna końcowa odpowiedź.
Celuj w 30 do 100 przypadków. Mniej niż 30 — nie odróżnisz sygnału od szumu. Więcej niż 100 — nigdy tego nie utrzymasz.
Mieszaj trzy rodzaje przypadków:
- Happy paths. Przypadki, które system zobaczy najczęściej.
- Adwersarialne. Edge case'y, niejednoznaczne wejścia, rzeczy o których wiesz, że gdzieś nawalają.
- Poza zakresem. Przypadki, których agent ma odmówić obsługi.
Jak oceniać
Poza klasyfikacją masz trzy opcje. Wybierz tę, która pasuje do Twojej tolerancji kosztu i ceremonii.
Ocena ludzka. Najtańsza w pieniądzach, droga w czasie. Najlepsza dla pierwszej wersji. Dwóch reviewerów na przypadek. Rozbieżności idą do trzeciego i rozmowy.
LLM-as-judge. Tanio i szybko. Ryzyko: model-sędzia ma uprzedzenia, które dzieli z agentem. Mitygacja: użyj silniejszego modelu niż agent, napisz dokładną rubrykę, zwaliduj sędziego na próbce ocen ludzkich.
Reguły. Gdy odpowiedź jest ustrukturyzowana (JSON, obecność cytatu, wyekstrahowane pole), sprawdzaj kodem. Najbardziej niezawodne. Używaj, gdziekolwiek się da.
Konkretny starter
Oto najmniejszy harness ewaluacyjny, który robi użyteczną robotę:
type Case = {
id: string
input: string
expected: string
category: 'happy' | 'adversarial' | 'out-of-scope'
}
type Verdict = { id: string; pass: boolean; reason: string }
async function score(cases: Case[], run: (input: string) => Promise<string>) {
const verdicts: Verdict[] = []
for (const c of cases) {
const out = await run(c.input)
const pass = await judge(c.expected, out, c.category)
verdicts.push({ id: c.id, pass: pass.ok, reason: pass.reason })
}
return {
overall: verdicts.filter((v) => v.pass).length / verdicts.length,
byCategory: groupBy(verdicts, (v) => cases.find((c) => c.id === v.id)!.category),
verdicts,
}
}
Dwieście linii później masz pipeline ewaluacyjny. Wepnij w cron i patrz, jak score się zmienia w czasie.
Dlaczego to zmienia wszystko
Gdy masz score, trzy rzeczy stają się możliwe.
Możesz refaktoryzować bez strachu. Zmień model, prompt, definicje narzędzi — i w ciągu minut wiesz, czy coś popsułeś.
Możesz ustawić kontrakt. "Wypuścimy, gdy eval trafia 0.85 na happy paths i 0.65 na adwersarialnych." Operator wie, co dostaje. Ty wiesz, co dostarczasz.
Możesz iterować w nieskończoność. Każdy bug na produkcji to nowy case w evalu. Eval rośnie razem z systemem. Po roku masz regression suite wart więcej niż kod.
Co ludzi blokuje
Trzy rzeczy, w kolejności częstotliwości.
"Jeszcze nie wiemy, jak wygląda dobre." No to zbuduj najpierw eval — to jest właśnie discovery. Nie masz projektu, masz życzenie.
"Model się poprawi i eval będzie nieaktualny." Nie. Lepsze modele dostają wyższe score na tych samych przypadkach. Eval staje się bardziej użyteczny, nie mniej.
"To dużo pracy." Owszem. Tak samo jak wypuszczenie zepsutego systemu i odkrycie tego za trzy miesiące, gdy klient się poskarży.
Napisz najpierw eval.