Эвалы для GenAI і агентаў: аднакрокавае і шматкрокавае тэставанне ў CI
Аднакрокавыя эвалы на аснове трас супраць шматкрокавых з сімулятарам: эталонныя наборы, LLM у ролі суддзі, агентныя метрыкі, фільтры ў CI і сем практык, якія апраўдваюць сябе.
«У мяне на промпце працуе» — гэта не інжынерны стандарт. Звычайны модульны тэст праходзіць або падае дэтэрмінавана, а генератыўныя сістэмы даюць адкрытыя, імавернасныя адказы. RAG-канвеер можа мець рацыю ў большасці выпадкаў, а агент можа выканаць задачу так, што на паперы гэта поспех, а на практыцы — небяспека. Таму менавіта эвалы, а не адчуванні ці чат, поўны «як на мяне, нармальна», сёння ляжаць у аснове сур’ёзнай ML-інжынерыі для генератыўнага AI і агентных сістэм. Далей — што вымяраць, чым адрозніваюцца аднакрокавыя і шматкрокавыя эвалы, кампрамісы кожнага падыходу і патэрны на Python, якія ўпісваюцца ў CI-канвеер.
Чаму без эвалаў не абысціся
Тры ўласцівасці LLM-праграм робяць іх неабходнымі:
- Недэтэрмінаванасць. Аднолькавыя промпты могуць даваць розныя вынікі ў розных запусках, версіях мадэлі ці пры іншай тэмпературы.
- Назапашванне памылак у агентах. Звычайны чат-бот мае адну магчымасць памыліцца, а агент выконвае ад 10 да 50 выклікаў мадэлі, выклікаў інструментаў і рашэнняў запар. Невялікая доля памылак на кожным кроку ператвараецца ў частыя збоі ўсяго сцэнарыя.
- Ціхія рэгрэсіі. Праўка промпту, абнаўленне мадэлі ці пераіндэксацыя вектарнага сховішча могуць пагоршыць якасць без адзінага выключэння.
Эвал ператварае «здаецца, стала лепш» у лік, які можна адсочваць у часе, выкарыстоўваць як фільтр перад выкаткай і параўноўваць паміж версіямі.
Дзве формы эвалаў
Амаль кожны фрэймворк — DeepEval, Ragas, LangSmith, Braintrust, OpenAI Evals — у выніку прыходзіць да аднаго з двух патэрнаў.
Аднакрокавыя, на аснове трас
Цыкл просты: бярэм кожны эталонны прыклад, адзін раз выклікаем праграму і ацэньваем трасу.
Open a test run with trace-level metrics
for each golden:
yield golden
call app with golden.input
capture trace
build a test case from the trace
score test case with metrics
upload test run (traces + scores)
Эталон (golden) — гэта размечаны прыклад: уваход, адказ, які сістэма павінна даць, і часта кантэкст ці выклікі інструментаў, якія павінны ўдзельнічаць. Траса запісвае ўсю гісторыю аднаго запуску: якія фрагменты знайшліся, якія інструменты спрацавалі, разважанні паміж імі, затрымку і колькасць токенаў, а не толькі канчатковы тэкст. Калі ацэньваць увесь запіс, а не толькі адказ, відаць, які этап падвёў: пошук, генерацыя ці выклік інструмента пасярэдзіне?
Шматкрокавыя, на аснове сімулятара
Гэта больш складаны і больш рэалістычны патэрн для агентаў і чат-памочнікаў.
def simulate(conversational goldens)
for each golden:
loop until expected outcome OR max turns:
simulator model generates a user turn
ask app for next reply
capture assistant turn
build a ConversationalTestCase from the full transcript
evaluate(test cases, metrics) # metrics run concurrently
-> pass/fail + reason per metric
Тут няма фіксаванай пары ўваход–выхад. Мадэль-сімулятар іграе карыстальніка з мэтай — напрыклад, скасаваць падпіску або вярнуць грошы за пэўны заказ — і падладжвае кожнае паведамленне пад адказы памочніка, як зрабіў бы чалавек. Сістэму ацэньваюць па ўсёй траекторыі. Ці трымалася яна задачы? Ці правільна карысталася інструментамі на працягу ўсёй размовы? Ці дасягнула ў выніку мэты? Ці не выдумала правілы на трэцім кроку?
Агентам, шматкрокавым памочнікам і ўсяму, што мае памяць ці стан, патрэбны менавіта такі стыль. Аднакрокавы эвал не заўважыць, што агент супярэчыць сабе на 4-м кроку або пяць разоў запар выклікае адзін інструмент.
Выбар метрык
Метрыкі дзеляцца на чатыры групы, і спалучаць іх разумна, бо ніводная не дае поўнай карціны.
Зручная ментальная мадэль: аднакрокавыя метрыкі ацэньваюць здымак, а агентныя — траекторыю.
LLM у ролі суддзі
Большасць метрык без эталоннага адказу сёння працуе праз «LLM у ролі суддзі». Моцная мадэль, часта іншая і больш здольная за тую, што тэстуецца, ацэньвае вынік па рубрыцы і тлумачыць сваю ацэнку.
from dataclasses import dataclass
JUDGE_PROMPT = """
You are evaluating an AI assistant's response for FAITHFULNESS.
A response is faithful if every factual claim it makes is supported
by the provided context. It is unfaithful if it adds unsupported
claims, contradicts the context, or fabricates details.
Context:
{context}
Response:
{response}
Return JSON: {{"score": <0.0-1.0>, "reason": "<short explanation>"}}
"""
@dataclass
class TestCase:
input: str
actual_output: str
retrieval_context: list[str]
def faithfulness_metric(test_case: TestCase, judge_llm) -> dict:
prompt = JUDGE_PROMPT.format(
context="\n".join(test_case.retrieval_context),
response=test_case.actual_output,
)
result = judge_llm.generate(prompt, response_format="json")
return {"score": result["score"], "reason": result["reason"], "passed": result["score"] >= 0.7}
У суддзяў ёсць пасткі:
- Перавага ўласнага стылю. Мадэлі шчадрэй ацэньваюць свой вынік або тэкст у сваім стылі. Калі можаце, бярыце суддзю з іншага сямейства мадэляў.
- Адчувальнасць да промпту. Невялікія змены ў фармулёўцы рубрыкі зрушваюць ацэнкі. Трымайце промпты суддзі пад кантролем версій, як код.
- Кошт і затрымка. Выклікі суддзі таксама каштуюць грошай. Калі бюджэт абмежаваны, ацэньвайце выбарку прадакшн-трафіку, а не ўвесь трафік.
- Перавага пазіцыі і даўжыні. Суддзі схіляюцца да даўжэйшых і больш упэўненых адказаў незалежна ад іх правільнасці. Час ад часу каліруйце іх на невялікім наборы, размечаным людзьмі.
Аднакрокавыя эвалы на практыцы
Эталонны набор можа быць проста спісам слоўнікаў з уваходам і чаканым вынікам.
goldens = [
{
"input": "What's our refund policy for digital products?",
"expected_output": "Digital products are refundable within 14 days if unused.",
"expected_tools": [],
},
{
"input": "Cancel order #48213",
"expected_output": None, # open-ended, judged not matched
"expected_tools": ["lookup_order", "cancel_order"],
},
]
Запускальнік ідзе па апісаным вышэй цыкле: адкрыць тэставы запуск, прайсці па кожным эталоне, выклікаць праграму, захаваць трасу, ацаніць і загрузіць вынікі.
from deepeval import evaluate
from deepeval.metrics import FaithfulnessMetric, AnswerRelevancyMetric
from deepeval.test_case import LLMTestCase
def run_eval_suite(goldens, app_callback, metrics):
test_cases = []
for golden in goldens:
trace = app_callback(golden["input"]) # your traced LLM/RAG app
test_case = LLMTestCase(
input=golden["input"],
actual_output=trace.final_output,
retrieval_context=trace.retrieved_chunks,
expected_output=golden.get("expected_output"),
)
test_cases.append(test_case)
results = evaluate(test_cases, metrics) # runs metrics concurrently
return results
metrics = [FaithfulnessMetric(threshold=0.7), AnswerRelevancyMetric(threshold=0.7)]
results = run_eval_suite(goldens, my_rag_app, metrics)
Праверка (assertion) ператварае ацэнкі ў фільтр.
def assert_eval_gate(results, min_pass_rate=0.9):
passed = sum(1 for r in results if r.success)
pass_rate = passed / len(results)
assert pass_rate >= min_pass_rate, f"Eval pass rate {pass_rate:.2%}
below gate {min_pass_rate:.0%}"
Падключыце гэта да канвеера PR так жа, як падключаны pytest. Калі змена промпту ці пошуку апускае вернасць крыніцам ніжэй за парог, зліццё павінна блакавацца, і нічога не павінна ціха трапіць у прадакшн.
Шматкрокавыя эвалы на практыцы
Замест фіксаванага ўваходу сімулятар атрымлівае профіль карыстальніка і мэту і сам вядзе размову.
class ConversationSimulator:
def __init__(self, simulator_model, max_turns=8):
self.simulator_model = simulator_model
self.max_turns = max_turns
def simulate(self, golden, chatbot_callback):
transcript = []
user_turn = self.simulator_model.generate_opening(golden["scenario"])
for _ in range(self.max_turns):
transcript.append({"role": "user", "content": user_turn})
assistant_turn = chatbot_callback(transcript)
transcript.append({"role": "assistant", "content": assistant_turn})
if self.simulator_model.is_goal_met(golden["expected_outcome"], transcript):
break
user_turn = self.simulator_model.generate_next_turn(transcript, golden)
return transcript
Атрыманую размову потым ацэньваюць агентнымі метрыкамі.
from deepeval.test_case import ConversationalTestCase, Turn
from deepeval.metrics import (
TaskCompletionMetric,
ToolCorrectnessMetric,
ConversationCompletenessMetric,
)
def build_conversational_test_case(transcript, tools_called):
turns = [Turn(role=t["role"], content=t["content"]) for t in transcript]
return ConversationalTestCase(turns=turns, tools_called=tools_called)
metrics = [
TaskCompletionMetric(threshold=0.8), # did it accomplish the user's goal?
ToolCorrectnessMetric(threshold=1.0), # right tools, right args, right order
ConversationCompletenessMetric(threshold=0.7), # did it address everything raised?
]
test_case = build_conversational_test_case(transcript, tools_called=agent_trace.tool_calls)
result = evaluate([test_case], metrics)
Тыповыя праверкі на ўзроўні агента:
- Правільнасць выклікаў інструментаў: патрэбны інструмент, з патрэбнымі аргументамі і ў патрэбным парадку — гэта больш, чым «нейкі інструмент быў выкліканы».
- Эфектыўнасць крокаў і выяўленне цыклаў: паўторныя выклікі аднаго інструмента без прагрэсу.
- Захаванне ролі і правілаў: агент трымаецца зададзенай персоны і абмежаванняў на працягу ўсёй размовы, а не толькі ў першым паведамленні.
- Аднаўленне пасля памылак: разумны паўтор пасля няўдалага выкліку інструмента замест выдуманага выніку.
- Перанос кантэксту: агент памятае, што карыстальнік сказаў тры паведамленні таму.
Узважваем кампрамісы
| Аднакрокавыя | Шматкрокавыя | |
|---|---|---|
| Кошт | Адзін выклік на эталон, танна і хутка | Кожны запуск — размова з многіх выклікаў, памножаная на колькасць крокаў і метрык |
| Набор даных | Вялікія эталонныя наборы лёгка сабраць з логаў або сінтэтычна | Сімулятары даходзяць да памежных выпадкаў і варожых карыстальнікаў, якіх ніхто не прапісаў бы ўручную |
| Сігнал | Выразна аддзяляе памылкі пошуку ад памылак генерацыі, амаль дэтэрмінаваны для фільтраў у CI | Ловіць назапашаныя збоі, якія выклікаюць сапраўдныя інцыдэнты, і тэстуе ўсю сістэму: інструменты, аркестрацыю, стан і абмежаванні |
| Слабыя месцы | Не бачыць дрэйфу, супярэчнасцяў і страты памяці паміж крокамі і падштурхоўвае падганяць сістэму пад эталонны набор | Большы раскід, бо сімулятар дадае выпадковасць; складаней адладжваць, бо збой на 6-м кроку можа пачынацца на 2-м; суддзі памыляюцца на доўгіх размовах |
Паколькі шматкрокавыя вынікі шумныя, тлумачце іх статыстычна. Запускайце набор N разоў і глядзіце на размеркаванні, а не на адну ацэнку.
Практыкі, якія апраўдваюць сябе
- Пачынайце з невялікага чыстага эталоннага набору на 30–100 прыкладаў. Шумны набор на 1000 прыкладаў горшы за акуратныя 50, бо дрэнная разметка атручвае сігнал.
- У RAG ацэньвайце пошук і генерацыю асобна. Вернасць крыніцам і паўната кантэксту адказваюць на розныя пытанні, а калі іх змяшаць, сапраўдны вінаваты схаваецца.
- Суддзём бярыце мацнейшую незалежную мадэль і час ад часу выбарачна звярайце яе ацэнкі з ацэнкамі людзей. Нават 20–30 прыкладаў на месяц паказваюць дрэйф суддзі.
- Трасіруйце ўсё. Ацэньвайце па захаваных трасах (знойдзеныя дакументы, выклікі інструментаў, прамежкавыя крокі), а не толькі па канчатковым тэксце. Таму захаванне трасы ідзе раней за пабудову тэставага выпадку.
- Сімулюйце агентаў да выкаткі. Праганяйце размовы з рэалістычнымі персонамі і варожымі мэтамі — раззлаваны карыстальнік, неадназначны запыт, памежны выпадак правілаў — да выпуску, а не пасля інцыдэнту.
- Лічыце эвалы жывым актывам. Перазапускайце ўвесь набор пасля кожнай змены промпту, абнаўлення мадэлі ці перабудовы індэкса і ўносьце парогі долі праходжання ў CI, як парогі пакрыцця кодам.
- Сачыце за размеркаваннямі ў часе. Адзін запуск эвалаў — гэта выбарка, а не ісціна. Паўтарайце крытычныя наборы і сачыце за трэндам.
Галоўнае
Эвалы для генератыўнага AI пачыналіся як зноска — ячэйка ў нататніку, якую нехта запускаў перад дэма. Агентныя сістэмы з гэтага выраслі. Калі праграма можа дзейнічаць некалькі разоў запар, выклікаць інструменты і захоўваць стан паміж крокамі, ацэнка перастае быць проста QA. Гэта галоўны спосаб убачыць, як сістэма паводзіць сябе насамрэч.
Аднакрокавыя эвалы на аснове трас даюць хуткія і танныя праверкі рэгрэсій якасці адказаў. Шматкрокавыя эвалы з сімулятарам даюць адзіную надзейную ацэнку таго, ці паводзіць сябе агент так, як задумана, на працягу ўсяго ўзаемадзеяння, і для большасці прадакшн-агентаў менавіта гэтая ацэнка важнейшая. Будуйце і аўтаматызуйце абодва віды. Не давярайце ні аднаму ўсляпую і пакідайце чалавека, які правярае суддзю.