# Як тестувати AI-агента: eval dataset, regression tests і метрики замість «ніби працює»

> AI-фіча без eval-ів – це фіча без тестів, тільки failure mode дорожчий. Будуємо production-процес: golden dataset, три типи грейдерів, regression suite у CI і release gating на реальному прикладі support-агента.

- Автор: Юра Скиба (https://cookiesoftware.io)
- Опубліковано: 2026-10-01
- Категорія: AI в роботі розробника
- Canonical: https://cookiesoftware.io/blog/ai-agent-evals

---

**Коротка відповідь.** Тестувати агента – означає мати три речі: **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-агента (формат наш, принципи – з гайдів):

```json
[
  {
    "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: детерміновано все, що можна

```ts
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-перевірки](/blog/jev-ai-typesafe), це рівно той кейс.

Людина – третій рівень: золотий стандарт, який не масштабується. Її час витрачай на калібровку 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-коду](/blog/ai-kod-chek-list-perevirky): довіряй, але верифікуй – автоматично і щоразу.

## 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-воркфлоу](/blog/claude-code-dlia-react)) і збери для нього suite із **п'яти** кейсів: три щасливі шляхи, один злий користувач, один out-of-scope. Напиши детермінований scoring за прикладом вище, прожени тричі поспіль і подивись на розкид. Якщо результати стрибають між прогонами – ти щойно дізнався про свого агента більше, ніж за місяць «ніби працює».
