Пять ограждений для агентного тестирования
Updated Jul 2026
Почему ограждения обязательны
Неограниченные агенты опасны. Они могут работать бесконечно, получать доступ к ресурсам, к которым не должны, давать недетерминированные результаты и потреблять неограниченное количество токенов. Хорошо спроектированная тестовая обвязка ограничивает агента, сохраняя его способность к адаптации.
Принцип: свобода в рамках границ. Агент свободно действует внутри ограждений, но ограждения -- это жёсткие лимиты, которые нельзя превысить.
Пять ограждений
+--------------------------------------------------+
| AGENT TEST HARNESS |
| - |
| +---------------------------------------------+ |
| | 1. MAX STEPS (prevent infinite loops) | |
| | 2. ALLOWED DOMAINS (prevent unauthorized | |
| | resource access) | |
| | 3. TIMEOUT (wall-clock limit) | |
| | 4. TOKEN BUDGET (cost ceiling) | |
| | 5. ACTION ALLOWLIST (restrict operations) | |
| +---------------------------------------------+ |
| |
| Agent operates freely WITHIN these boundaries |
+--------------------------------------------------+
Ограждение 1: Максимум шагов
Предотвращает бесконечное зацикливание агента. Если агент выполнил более N действий, не достигнув цели, тест прерывается.
max_steps: int = 30 # Default: 30 actions before forced stop
Как устанавливать лимит:
- Посчитайте шаги в вашем самом длинном ручном тестовом потоке (например, 15 шагов для потока оформления заказа)
- Умножьте на 2, чтобы учесть накладные расходы агента на исследование
- Это и есть ваш max_steps
Что происходит при превышении:
if self.step_count >= self.config.max_steps:
return TestResult(
status="ABORTED",
reason=f"Max steps ({self.config.max_steps}) exceeded. "
f"Agent could not complete objective in {self.step_count} steps.",
steps_taken=self.step_count,
last_observation=self.last_observation
)
Ограждение 2: Разрешённые домены
Предотвращает навигацию агента по URL вне тестового окружения. Без этого агент, тестирующий веб-приложение, может перейти по ссылкам на внешние сайты, запустить потоки OAuth или даже взаимодействовать с продакшен-системами.
allowed_domains: list[str] = ["staging.myapp.com", "api.staging.myapp.com"]
Реализация:
def is_domain_allowed(self, url: str) -> bool:
from urllib.parse import urlparse
domain = urlparse(url).netloc
if not self.config.allowed_domains:
return True # No restriction configured
return any(
domain == allowed or domain.endswith(f".{allowed}")
for allowed in self.config.allowed_domains
)
Пример из практики: Агент, тестирующий страницу входа, переходит по ссылке «Забыли пароль», которая перенаправляет на страницу OAuth почтового провайдера. Без ограничения доменов агент взаимодействовал бы со страницей входа Google. С ограничением действие блокируется и логируется.
Ограждение 3: Таймаут
Лимит по реальному времени, предотвращающий бесконтрольное выполнение тестов. Даже если агент остаётся в рамках бюджета шагов и токенов, медленно отвечающий сервер или зависший браузер могут держать тест запущенным бесконечно.
timeout_seconds: int = 300 # 5-minute wall clock limit
Реализация:
import time
def check_timeout(self) -> str | None:
elapsed = time.time() - self.start_time
if elapsed > self.config.timeout_seconds:
return (f"Timeout ({self.config.timeout_seconds}s) exceeded. "
f"Elapsed: {elapsed:.1f}s")
return None
Совет: Установите два уровня таймаута:
- Таймаут на действие: 30 секунд на браузерное действие (ловит зависания)
- Таймаут на тест: 300 секунд на весь тест (ловит медленные циклы)
Ограждение 4: Бюджет токенов
Потолок стоимости, предотвращающий неограниченные вызовы LLM от агента. Каждый вызов стоит токенов, и затраты быстро накапливаются в многошаговых агентах.
max_tokens: int = 50_000 # Token budget per test run
Почему это важно финансово:
| Поведение агента | Токенов на тест | Стоимость (Claude Sonnet) |
|---|---|---|
| 10-шаговый простой тест | ~10K | $0.05 |
| 30-шаговый сложный тест | ~30K | $0.15 |
| Неконтролируемый агент (без бюджета) | ~500K+ | $2.50+ |
Реализация:
def track_tokens(self, response) -> None:
self.token_count += response.usage.input_tokens + response.usage.output_tokens
if self.token_count >= self.config.max_tokens:
raise TokenBudgetExceeded(
f"Token budget ({self.config.max_tokens}) exhausted. "
f"Used: {self.token_count}"
)
Ограждение 5: Список разрешённых действий
Ограничивает, какие действия может выполнять агент. В мониторинге продакшена нужны только действия чтения. В CI -- всё, кроме деструктивных операций.
allowed_actions: list[str] = ["NAVIGATE", "CLICK", "TYPE", "ASSERT", "SCREENSHOT"]
# Blocked: "DELETE", "DROP", "RESET", "ADMIN_*"
Реализация:
def is_action_allowed(self, action) -> bool:
if self.config.allowed_actions:
if action.type not in self.config.allowed_actions:
return False
# Additional checks for specific action types
if action.type == "NAVIGATE":
if not self.is_domain_allowed(action.url):
return False
if action.type == "TYPE" and action.selector.contains("password"):
# Never type real credentials -- use test tokens
if not action.text.startswith("test_"):
return False
return True
Конфигурация ограждений по окружениям
Разные окружения требуют разных профилей ограждений:
| Ограждение | Разработка | CI/CD | Мониторинг продакшена |
|---|---|---|---|
| Максимум шагов | 50 (щедрый) | 30 (стандарт) | 10 (минимальный) |
| Таймаут | 10 минут | 5 минут | 2 минуты |
| Бюджет токенов | 100K (исследование) | 50K (стандарт) | 10K (сфокусированный) |
| Разрешённые домены | * (любой) |
staging.* |
prod.* (только чтение) |
| Действия | Все | Все кроме DELETE | Только чтение (GET, наблюдение) |
| Поведение при сбое | Лог + продолжить | Провалить тест + скриншот | Оповещение + скриншот |
Загрузка конфигурации из окружения
from dataclasses import dataclass, field
import os
@dataclass
class HarnessConfig:
max_steps: int = field(
default_factory=lambda: int(os.getenv("AGENT_MAX_STEPS", "30"))
)
timeout_seconds: int = field(
default_factory=lambda: int(os.getenv("AGENT_TIMEOUT", "300"))
)
max_tokens: int = field(
default_factory=lambda: int(os.getenv("AGENT_MAX_TOKENS", "50000"))
)
allowed_domains: list[str] = field(
default_factory=lambda: os.getenv("AGENT_ALLOWED_DOMAINS", "").split(",")
)
allowed_actions: list[str] = field(
default_factory=lambda: os.getenv(
"AGENT_ALLOWED_ACTIONS",
"NAVIGATE,CLICK,TYPE,ASSERT,SCREENSHOT"
).split(",")
)
Нарушения ограждений: логирование и оповещение
Каждое нарушение ограждения должно быть залогировано с достаточным контекстом для диагностики проблемы:
@dataclass
class GuardrailViolation:
guardrail: str # "max_steps", "timeout", "token_budget", etc.
threshold: str # "30 steps", "300s", "50000 tokens"
actual: str # "31 steps", "305s", "51234 tokens"
test_objective: str
last_observation: str # What the agent saw before violation
last_action: str # What the agent tried to do
history_summary: str # Compressed history for debugging
screenshot_path: str | None
Ключевой вывод
Пять ограждений (максимум шагов, разрешённые домены, таймаут, бюджет токенов, список разрешённых действий) формируют систему глубокой обороны. Каждое ловит свой тип сбоя. Вместе они делают агентное тестирование безопасным для CI/CD-пайплайнов, где неограниченный агент может заблокировать деплой, потребить дорогие токены или взаимодействовать с системами за пределами тестовой области.