Галоўная / Артыкулы

Эвалы для GenAI і агентаў: аднакрокавае і шматкрокавае тэставанне ў CI

Аднакрокавыя эвалы на аснове трас супраць шматкрокавых з сімулятарам: эталонныя наборы, LLM у ролі суддзі, агентныя метрыкі, фільтры ў CI і сем практык, якія апраўдваюць сябе.

Выдавец: REACTAPP.TOP8 хв чытання1800 слоў

Крыніцы: End-to-End Evals for Gen AI and Agentic AI: A Practitioner’s Guide to Single-Turn and Multi-Turn Evaluation

«У мяне на промпце працуе» — гэта не інжынерны стандарт. Звычайны модульны тэст праходзіць або падае дэтэрмінавана, а генератыўныя сістэмы даюць адкрытыя, імавернасныя адказы. RAG-канвеер можа мець рацыю ў большасці выпадкаў, а агент можа выканаць задачу так, што на паперы гэта поспех, а на практыцы — небяспека. Таму менавіта эвалы, а не адчуванні ці чат, поўны «як на мяне, нармальна», сёння ляжаць у аснове сур’ёзнай ML-інжынерыі для генератыўнага AI і агентных сістэм. Далей — што вымяраць, чым адрозніваюцца аднакрокавыя і шматкрокавыя эвалы, кампрамісы кожнага падыходу і патэрны на Python, якія ўпісваюцца ў CI-канвеер.

Чаму без эвалаў не абысціся

Тры ўласцівасці LLM-праграм робяць іх неабходнымі:

  1. Недэтэрмінаванасць. Аднолькавыя промпты могуць даваць розныя вынікі ў розных запусках, версіях мадэлі ці пры іншай тэмпературы.
  2. Назапашванне памылак у агентах. Звычайны чат-бот мае адну магчымасць памыліцца, а агент выконвае ад 10 да 50 выклікаў мадэлі, выклікаў інструментаў і рашэнняў запар. Невялікая доля памылак на кожным кроку ператвараецца ў частыя збоі ўсяго сцэнарыя.
  3. Ціхія рэгрэсіі. Праўка промпту, абнаўленне мадэлі ці пераіндэксацыя вектарнага сховішча могуць пагоршыць якасць без адзінага выключэння.

Эвал ператварае «здаецца, стала лепш» у лік, які можна адсочваць у часе, выкарыстоўваць як фільтр перад выкаткай і параўноўваць паміж версіямі.

Дзве формы эвалаў

Амаль кожны фрэймворк — 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 разоў і глядзіце на размеркаванні, а не на адну ацэнку.

Практыкі, якія апраўдваюць сябе

  1. Пачынайце з невялікага чыстага эталоннага набору на 30–100 прыкладаў. Шумны набор на 1000 прыкладаў горшы за акуратныя 50, бо дрэнная разметка атручвае сігнал.
  2. У RAG ацэньвайце пошук і генерацыю асобна. Вернасць крыніцам і паўната кантэксту адказваюць на розныя пытанні, а калі іх змяшаць, сапраўдны вінаваты схаваецца.
  3. Суддзём бярыце мацнейшую незалежную мадэль і час ад часу выбарачна звярайце яе ацэнкі з ацэнкамі людзей. Нават 20–30 прыкладаў на месяц паказваюць дрэйф суддзі.
  4. Трасіруйце ўсё. Ацэньвайце па захаваных трасах (знойдзеныя дакументы, выклікі інструментаў, прамежкавыя крокі), а не толькі па канчатковым тэксце. Таму захаванне трасы ідзе раней за пабудову тэставага выпадку.
  5. Сімулюйце агентаў да выкаткі. Праганяйце размовы з рэалістычнымі персонамі і варожымі мэтамі — раззлаваны карыстальнік, неадназначны запыт, памежны выпадак правілаў — да выпуску, а не пасля інцыдэнту.
  6. Лічыце эвалы жывым актывам. Перазапускайце ўвесь набор пасля кожнай змены промпту, абнаўлення мадэлі ці перабудовы індэкса і ўносьце парогі долі праходжання ў CI, як парогі пакрыцця кодам.
  7. Сачыце за размеркаваннямі ў часе. Адзін запуск эвалаў — гэта выбарка, а не ісціна. Паўтарайце крытычныя наборы і сачыце за трэндам.

Галоўнае

Эвалы для генератыўнага AI пачыналіся як зноска — ячэйка ў нататніку, якую нехта запускаў перад дэма. Агентныя сістэмы з гэтага выраслі. Калі праграма можа дзейнічаць некалькі разоў запар, выклікаць інструменты і захоўваць стан паміж крокамі, ацэнка перастае быць проста QA. Гэта галоўны спосаб убачыць, як сістэма паводзіць сябе насамрэч.

Аднакрокавыя эвалы на аснове трас даюць хуткія і танныя праверкі рэгрэсій якасці адказаў. Шматкрокавыя эвалы з сімулятарам даюць адзіную надзейную ацэнку таго, ці паводзіць сябе агент так, як задумана, на працягу ўсяго ўзаемадзеяння, і для большасці прадакшн-агентаў менавіта гэтая ацэнка важнейшая. Будуйце і аўтаматызуйце абодва віды. Не давярайце ні аднаму ўсляпую і пакідайце чалавека, які правярае суддзю.