Коротка відповідь. Тестувати агента – означає мати три речі: golden dataset із представницьких кейсів (почни з 20-50, узятих із реальних фейлів), грейдери трьох типів (детерміновані перевірки, model judge, людина) і regression suite у CI з порогами, нижче яких реліз не їде. Усе інше – деталі реалізації, які ми зараз розберемо на прикладі support-агента з кодом.

Чому «потикав у чаті» не рахується

Звичайний код падає гучно: exception, червоний тест, stack trace. Агент фейлиться тихо і впевнено: викликає не той tool, вигадує поле, якого немає, ввічливо відповідає не на те питання. Користувач бачить це раніше за тебе.

Друга проблема – кожна зміна глобальна. Поміняв формулювання в system prompt, оновив модель, додав tool – і поведінка змінилась на всіх сценаріях одразу, включно з тими, які вчора працювали. Без regression suite ти дізнаєшся про це з відгуків.

Anthropic у своєму інженерному гайді розкладає eval на складові: task, trial, agent harness, eval harness, transcript, outcome, grader, suite. Запам'ятовувати термінологію не обов'язково – важлива сама думка: eval – це інженерна система, а не разовий скрипт.

Що саме вимірюємо

Для агента недостатньо одного числа «accuracy». Мінімальний набір вимірів:

Вимір Питання Тип перевірки
Task success Задача користувача вирішена? Часто – model judge або людина
Tool correctness Викликані правильні tools із правильними аргументами? Детермінована
Groundedness Відповідь спирається на реальні дані, не вигадана? Детермінована + judge
Latency Вклалися в бюджет часу? Детермінована
Cost Скільки токенів/викликів з'їв прохід? Детермінована

Зверни увагу: більшість вимірів – детерміновані. Це добра новина: дешеві, відтворювані, швидкі.

Golden dataset: 20 кейсів кращі за 1000 синтетичних

Найчастіша помилка – почати з генерації тисячі синтетичних прикладів. Anthropic прямо радить протилежне: 20-50 простих задач, узятих із реальних фейлів – на ранньому етапі зміни мають великий ефект, і маленький чесний датасет його покаже.

Для кожного кейсу потрібен reference solution – еталонний результат, який доводить, що задача взагалі розв'язувана і грейдер налаштований правильно. Якщо еталон не проходить власний грейдер – проблема в задачі чи грейдері, не в агенті.

Ось як виглядають фікстури для support-агента (формат наш, принципи – з гайдів):

[
  {
    "id": "billing-double-charge",
    "input": "Мене списали двічі за одну підписку, поверніть гроші",
    "expected": {
      "department": "billing",
      "tools": ["lookup_invoices", "create_refund_ticket"],
      "must_not_tools": ["close_conversation"],
      "answer_must_mention": ["повернення", "тикет"]
    }
  },
  {
    "id": "angry-but-simple",
    "input": "ВАШ САЙТ ЖАХЛИВИЙ!!! де кнопка зміни пароля???",
    "expected": {
      "department": "account",
      "tools": ["get_help_article"],
      "must_not_tools": ["escalate_to_human"],
      "answer_must_mention": ["пароль"]
    }
  },
  {
    "id": "out-of-scope-legal",
    "input": "Хочу подати на вас до суду, дайте юридичну адресу",
    "expected": {
      "department": "other",
      "tools": ["escalate_to_human"],
      "must_not_tools": ["create_refund_ticket"],
      "answer_must_mention": []
    }
  }
]

Повний suite – 20 таких кейсів: щасливі шляхи, злі користувачі, out-of-scope, спроби prompt injection, двозначні запити. Кожен новий production-фейл – новий кейс у датасеті, назавжди.

Scoring: детерміновано все, що можна

type EvalCase = {
  id: string;
  input: string;
  expected: {
    department: string;
    tools: string[];
    must_not_tools: string[];
    answer_must_mention: string[];
  };
};

type AgentRun = {
  department: string;
  calledTools: string[];
  answer: string;
  latencyMs: number;
  costUsd: number;
};

type CaseScore = {
  id: string;
  passed: boolean;
  failures: string[];
};

export function scoreCase(testCase: EvalCase, run: AgentRun): CaseScore {
  const failures: string[] = [];
  const { expected } = testCase;

  if (run.department !== expected.department) {
    failures.push(`department: ${run.department}, очікувано ${expected.department}`);
  }
  for (const tool of expected.tools) {
    if (!run.calledTools.includes(tool)) {
      failures.push(`не викликано обов'язковий tool: ${tool}`);
    }
  }
  for (const tool of expected.must_not_tools) {
    if (run.calledTools.includes(tool)) {
      failures.push(`викликано заборонений tool: ${tool}`);
    }
  }
  const answer = run.answer.toLowerCase();
  for (const phrase of expected.answer_must_mention) {
    if (!answer.includes(phrase.toLowerCase())) {
      failures.push(`відповідь не згадує: «${phrase}»`);
    }
  }

  return { id: testCase.id, passed: failures.length === 0, failures };
}

Це ловить більшість регресій – і коштує копійки, бо жодного LLM-виклику в грейдері немає.

Model judge: для того, що кодом не перевіриш

«Відповідь ввічлива і не обіцяє зайвого» – детермінованим кодом не перевіриш. Тут потрібен model judge, і в нього свої правила гігієни (за гайдами Anthropic і OpenAI): чіткий rubric замість «оціни від 1 до 10», окремий judge на кожен вимір (ввічливість окремо, точність окремо), регулярна калібровка проти людських оцінок. До речі, judge не обов'язково має бути великою моделлю: для бінарних питань на кшталт «чи відповідь по темі?» підійде і спеціалізована decision-модель – ми розбирали Jev і його Noul-перевірки, це рівно той кейс.

Людина – третій рівень: золотий стандарт, який не масштабується. Її час витрачай на калібровку judge-ів і рев'ю спірних кейсів, а не на рутинне прогонювання suite.

Regression suite у CI і release gating

Техніка проста: прогін усього suite на кожну зміну prompt/моделі/tools, звіт у PR, поріг – нижче якого merge блокується:

  • Порогів два: загальний pass rate (напр., ≥90%) і нуль регресій на критичних кейсах (безпека, гроші). Падіння «angry-but-simple» – неприємно; падіння «out-of-scope-legal» – блокер.
  • Кожен прогін зберігає transcript – без нього фейл неможливо дебажити.
  • Drift-перевірка: suite ганяється не лише на свої зміни, а й за розкладом – модель за API могла оновитись без тебе.

Це та сама дисципліна, що й у чеклісті перевірки AI-коду: довіряй, але верифікуй – автоматично і щоразу.

False positives: чому метрики брешуть

Suite каже 95% – а користувачі скаржаться. Класичні причини:

  • Грейдер перевіряє не те. answer_must_mention: ["пароль"] пропустить відповідь «пароль змінити неможливо». Тому кожен кейс, що пройшов «дивно», іде в ручний рев'ю.
  • Датасет застарів. Продукт змінився, кейси – ні.
  • Judge підігрує. Model judge схильний завищувати оцінки «схожим на правильні» відповідям – без калібровки проти людини його бали дрейфують.

Практика: раз на кілька тижнів – вибірковий рев'ю пройдених (не тільки впалих!) кейсів. Саме там ховаються false positives.

Типові помилки

  1. Почати з інфраструктури на тисячу кейсів замість 20 чесних.
  2. Міряти тільки фінальну відповідь, ігноруючи tool calls – агент може вгадати відповідь, зламавши пів системи по дорозі.
  3. Один гігантський judge-prompt на всі виміри одразу.
  4. Suite є, а в CI не підключений – «прогонимо перед релізом» означає «ніколи».
  5. Жодного бюджету на cost/latency – агент «порозумнішав» ціною ×3 токенів, і ніхто не помітив.

Практичне завдання

Візьми будь-якого свого агента (хоч скрипт із Claude Code-воркфлоу) і збери для нього suite із п'яти кейсів: три щасливі шляхи, один злий користувач, один out-of-scope. Напиши детермінований scoring за прикладом вище, прожени тричі поспіль і подивись на розкид. Якщо результати стрибають між прогонами – ти щойно дізнався про свого агента більше, ніж за місяць «ніби працює».