Ви побудували AI-агента — програму, яка використовує LLM (large language model — мозок за ChatGPT, Claude, Gemini) для автономного виклику інструментів. Він робить запити до баз даних, перевіряє дозволи, надсилає імейли. У терміналі все працює бездоганно. Потім ви відкриваєте директорію tests/ і бачите: вона порожня. Не тому що вам ліньки, а тому що ваш SDK (software development kit — набір інструментів від фреймворку) поставляється з рівно нулем утиліт для тестування.

Ласкаво просимо в розробку агентів у квітні 2026.

Прогалина (коротко)

Ми детально розглянули ландшафт SDK у супровідній новині. Коротка версія: Anthropic поставляє нуль утиліт для тестування, OpenAI відмовляється експортувати свій внутрішній FakeModel, а евалюатор Google ADK має відкритий баг, що повертає 0.0 на правильних збігах. Ніхто не дає вам інструменти для тестування агентів.

Очевидна думка: «Я просто напишу assert на текстовий вивід агента». Не треба. LLM перефразовує відповідь кожного разу. Temperature (параметр, що контролює випадковість відповідей моделі), виставлений на нуль, не рятує — інша версія моделі, інший день, інше формулювання. Перевірка тексту — це тестування підкидання монетки.

Що не змінюється? Послідовність викликів інструментів. Коли ваш агент обробляє запит «перевір, чи може користувач надіслати імейл», він викликає lookup_user → check_permissions → send_email кожного разу, незалежно від того, як сформулює відповідь. Патерн дій — це контракт. Тестуйте саме його.

Цей гайд дає вам робочий тестовий харнес приблизно у ~60 рядків. Моки, golden-фікстури, розділення CI — все, що SDK мали б включити, але не включили.

Рецепт: поведінкове тестування у ~60 рядків

Ми побудуємо тестовий харнес — невеликий фреймворк, який спостерігає за вашим агентом — що:

  1. Мокає LLM, щоб тести виконувались за мілісекунди з нульовою вартістю
  2. Записує, які інструменти агент викликає і в якому порядку
  3. Перевіряє послідовність, а не слова

Крок 1: Оберіть стратегію моків

Два шляхи. Mock-model тести замінюють LLM скриптованим респондером — миттєво, безкоштовно, детерміновано. Real-model smoke-тести викликають справжній API і записують відповіді — реалістично, але дорого і нестабільно. Потрібні обидва. Моки в CI (кожен pull request), real-model — на нічному cron.

Для моків Pydantic AI має найкращі примітиви: TestModel (автоматично викликає кожен інструмент агента) і FunctionModel (ви скриптуєте точну відповідь). Якщо ви не використовуєте Pydantic AI, ми побудуємо те саме з нуля.

Крок 2: Побудуйте перехоплювач

Ось підхід на чистому pytest — без залежності від фреймворку, працює з будь-яким SDK:

# test_agent_tools.py
import json
from dataclasses import dataclass, field

@dataclass
class ToolCallRecord:
    """One recorded tool invocation."""
    name: str
    arguments: dict
    order: int

@dataclass
class AgentRecorder:
    """Intercepts and records every tool call."""
    calls: list[ToolCallRecord] = field(default_factory=list)

    def record(self, tool_name: str, arguments: dict):
        self.calls.append(ToolCallRecord(
            name=tool_name,
            arguments=arguments,
            order=len(self.calls),
        ))

    @property
    def sequence(self) -> list[str]:
        """Just the tool names, in order."""
        return [c.name for c in self.calls]

    def assert_sequence(self, expected: list[str]):
        assert self.sequence == expected, (
            f"Expected {expected}, got {self.sequence}"
        )

    def assert_called_with(self, tool_name: str, **expected_args):
        match = [c for c in self.calls if c.name == tool_name]
        assert match, f"{tool_name} was never called"
        assert match[0].arguments == expected_args

Це 35 рядків. Рекордер захоплює кожен виклик інструменту і надає два assertion-и: assert_sequence (правильні інструменти в правильному порядку?) і assert_called_with (правильні аргументи?).

Крок 3: Підключіть до агента

Оберніть ваші tool-функції, щоб рекордер бачив кожен виклик:

def make_tracked_tool(original_fn, recorder: AgentRecorder):
    """Wrap a tool function to record calls before executing."""
    async def tracked(*args, **kwargs):
        recorder.record(original_fn.__name__, kwargs)
        return await original_fn(*args, **kwargs)
    return tracked

faion — CLI-асистент, що генерує скафолдинг коду зі структурованих промптів — вставте блок нижче, і він створить рекордер і обгортку, адаптовані під ваш SDK.

/faion
Generate an AgentRecorder class and make_tracked_tool wrapper for behavioral testing of an AI agent.
Requirements:
- Pure Python, no framework dependency, dataclass-based
- AgentRecorder must record tool name, arguments, and call order
- Must expose assert_sequence(expected_names) and assert_called_with(tool_name, **kwargs)
- make_tracked_tool wraps any async tool function to record calls before executing
- Detect my agent SDK (anthropic / openai-agents / google-adk / pydantic-ai) from imports and adapt the wrapper signature accordingly
- Include a pytest fixture that returns a fresh AgentRecorder

Крок 4: Напишіть перший поведінковий тест

import pytest

@pytest.fixture
def recorder():
    return AgentRecorder()

@pytest.mark.asyncio
async def test_email_permission_flow(recorder, mock_agent):
    """Agent MUST check permissions before sending email."""
    await mock_agent.run("Send the quarterly report to [email protected]")

    # The CONTRACT: always check before sending
    recorder.assert_sequence([
        "lookup_user",
        "check_permissions",
        "send_email",
    ])

    # Verify the agent looked up the right user
    recorder.assert_called_with(
        "lookup_user", email="[email protected]"
    )

Цьому тесту байдуже, що агент каже. Його цікавить, що агент робить. Текст може змінюватись кожного запуску. Перевірка дозволів — не має права.

Крок 5: Тестуйте шляхи відновлення після помилок

У новині ми розглянули «happy path». Гайди відпрацьовують свою ціну на брудних шляхах — що відбувається, коли інструмент кидає помилку, коли модель робить retry, коли вона галюцинує неіснуючий інструмент.

@pytest.mark.asyncio
async def test_graceful_degradation_on_tool_failure(recorder, mock_agent):
    """When send_email fails, agent MUST NOT retry without re-checking permissions."""
    mock_agent.configure_tool_failure("send_email", error="SMTP timeout")

    await mock_agent.run("Send the quarterly report to [email protected]")

    recorder.assert_sequence([
        "lookup_user",
        "check_permissions",
        "send_email",        # fails
        "check_permissions",  # must re-verify before retry
        "send_email",        # retry
    ])

@pytest.mark.asyncio
async def test_unknown_tool_call_is_caught(recorder, mock_agent):
    """If model hallucinates a tool name, harness catches it."""
    mock_agent.inject_fake_tool_call("send_fax")  # doesn't exist

    with pytest.raises(UnknownToolError):
        await mock_agent.run("Fax the report")

    # Agent should have called nothing successfully
    assert recorder.sequence == []

Відновлення після помилок — це місце, де агенти тихо ламаються в продакшені. Мок, що симулює падіння інструменту, нічого не коштує і ловить збої, які ваші happy-path тести ніколи не зловлять.

/faion
Scaffold a full behavioral test suite for my AI agent project.
Requirements:
- Scan my agent's registered tools and generate one test per tool
- For each tool: happy-path test + error-recovery test (tool raises exception)
- Generate a mock agent fixture that uses scripted responses instead of real LLM calls
- Include conftest.py with AgentRecorder fixture and make_tracked_tool helper
- Add golden fixture save/load utilities (JSON snapshots in tests/fixtures/)
- Mark real-model tests with @pytest.mark.smoke
- Detect my SDK from imports and adapt accordingly

Крок 6: Додайте golden-фікстури для виявлення регресій

Golden-фікстура — це знімок очікуваної поведінки, який ви зберігаєте у version control — ваш еталонний бейзлайн. Збережіть вивід рекордера як JSON:

# conftest.py
import json
from pathlib import Path

FIXTURE_DIR = Path("tests/fixtures")

def save_golden(recorder: AgentRecorder, name: str):
    """Save current tool-call sequence as the blessed baseline."""
    fixture = {
        "sequence": recorder.sequence,
        "calls": [
            {"name": c.name, "arguments": c.arguments}
            for c in recorder.calls
        ],
    }
    (FIXTURE_DIR / f"{name}.json").write_text(json.dumps(fixture, indent=2))

def assert_matches_golden(recorder: AgentRecorder, name: str):
    """Assert current behavior matches the saved baseline."""
    fixture = json.loads((FIXTURE_DIR / f"{name}.json").read_text())
    assert recorder.sequence == fixture["sequence"], (
        f"Behavioral drift detected!\n"
        f"  Golden: {fixture['sequence']}\n"
        f"  Actual: {recorder.sequence}"
    )

Коли ви оновите модель з Claude Sonnet 4 на що б там не вийшло далі, запустіть suite. Якщо агент почне пропускати check_permissions, ви дізнаєтесь раніше за своїх користувачів.

Крок 7: Інтеграція з CI

Ваш CI-конфіг розділяється на два jobs. Тестова піраміда від Block Engineering чудово формулює принцип: «Ми не запускаємо live LLM-тести в CI. Це надто дорого, надто повільно і надто нестабільно.»

# .github/workflows/agent-tests.yml
name: Agent Tests
on: [pull_request]
jobs:
  unit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: pytest tests/agents/ -m "not smoke" --tb=short
# .github/workflows/agent-smoke.yml
name: Agent Smoke Tests (Nightly)
on:
  schedule:
    - cron: '0 6 * * *'  # 2am ET
jobs:
  smoke:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: pytest tests/agents/ -m smoke --tb=long

Позначайте real-model тести як @pytest.mark.smoke. Вони запускаються вночі, алертять про дрифт і ніколи не блокують PR.

/faion
Generate GitHub Actions CI config for agent behavioral tests.
Requirements:
- Two workflow files: agent-tests.yml (on PR) and agent-smoke.yml (nightly cron at 2am ET)
- PR workflow runs pytest with -m "not smoke", fails fast
- Nightly workflow runs pytest with -m smoke, uses secrets for API keys
- Add a third workflow: agent-golden-update.yml (manual trigger) that runs tests with --update-golden flag and opens a PR with fixture diffs
- Include pip caching and Python 3.12 setup

Підводні камені

1. Anthropic каже цього не робити. Їхній інженерний блог (9 січня 2026) прямо попереджає: «Перевірка того, що агенти виконали дуже конкретні кроки, як-от послідовність tool-call-ів, призводить до надмірно крихких тестів.» Вони праві — для дослідницьких багатокрокових флоу, де існує кілька валідних шляхів. Вони неправі для критичних бізнес-шляхів, де пропуск кроку означає дірку в безпеці. Використовуйте sequence assertions для контрактів (перевірка дозволів, валідація даних), outcome assertions для всього іншого.

2. Golden-фікстури застарівають. Оновіть модель — і кожна фікстура ламається. Не тому що агент неправильний, а тому що він знайшов легітимно кращий шлях. Рішення: ревʼюйте діффи як код-ревʼю. Якщо нова послідовність валідна — оновіть фікстуру. Якщо ні — ви щойно зловили регресію.

3. Mock-тести вам брешуть. FakeModel, що завжди викликає check_permissions, каже вам, що логіка маршрутизації працює. Він нічого не каже про те, чи Claude або GPT справді викличуть цей інструмент. Mock-тести покривають приблизно 70% тестованої поверхні — маршрутизацію, парсинг аргументів, обробку помилок. Решта 30% потребують real-model smoke-тестів за нічним розкладом.

4. Недетермінованість — структурна, не баг. Навіть з однаковим вводом модель може викликати інструменти в іншому порядку між запусками. Для некритичних шляхів використовуйте AgentEvals з режимом unordered trajectory — він перевіряє набір викликаних інструментів, а не послідовність. Жорсткий порядок — тільки для шляхів, де порядок І Є контрактом.

5. Тестуйте multi-agent handoff-и окремо. Якщо ваш агент делегує суб-агентам, кожен handoff — це межа. Записуйте виклик делегування як tool call (delegate_to_research_agent), потім тестуйте послідовність tool-call-ів суб-агента незалежно. Перетин меж агентів в одному assertion робить все крихким і нічого не дебажиться.

Що ви маєте зараз

У вас є директорія tests/agents/ з поведінковими тестами, golden-фікстури у version control і CI, розділений на швидкі моки для кожного PR та real-model smoke-тести вночі. Ваш агент деплоїться з тією ж впевненістю, що й решта кодобази — не тому що став детермінованим, а тому що ви перестали тестувати не те. Текст змінюється кожного запуску. Дії — не мають права.