Ви побудували 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 рядків
Ми побудуємо тестовий харнес — невеликий фреймворк, який спостерігає за вашим агентом — що:
- Мокає LLM, щоб тести виконувались за мілісекунди з нульовою вартістю
- Записує, які інструменти агент викликає і в якому порядку
- Перевіряє послідовність, а не слова
Крок 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-тести вночі. Ваш агент деплоїться з тією ж впевненістю, що й решта кодобази — не тому що став детермінованим, а тому що ви перестали тестувати не те. Текст змінюється кожного запуску. Дії — не мають права.





