Как построить отказоустойчивую ферму автономных AI-агентов на базе Docker, Redroid и Playwright с автоматическим обходом блокировок и самовосстановлением за 40 секунд — от схемы изоляции до боевых цифр кластера на 48 инстансов.
1. API нет. Есть только UI
В B2B-автоматизации публичный API — роскошь. Закрытые CRM без документации, корпоративные мессенджеры, соцсети с антифродом уровня банка и внутренние порталы с динамической вёрсткой приходится автоматизировать через реальный пользовательский интерфейс. Именно так устроена платформа First Touch: рой агентов пишет по вашей базе в Telegram с ваших же аккаунтов, отвечает на вопросы и передаёт менеджеру тёплых лидов — без Bot API, через живые пользовательские сессии. Дальше — чистая инженерия того, как этот рой не разваливается на 48 инстансах.
Что убивает прод первым делом:
2. Дуальный кластер: Playwright vs Redroid
| Параметр | Playwright Stealth | Redroid (Android-in-Docker) |
|---|---|---|
| Плотность | до 15 инстансов на ядро CPU | 1 контейнер ≈ 1.5–2 ГБ RAM |
| Cold start | 0.8–1.5 с | 18–35 с |
| Отпечаток | JS-окружение, Canvas/WebGL/WebRTC | нативный мобильный отпечаток |
| Детект через браузерный JS | возможен | невозможен |
| Сеть | прокси уровня браузера | нативный 4G/5G в namespace |
| Когда брать | веб-панели, массовые сценарии | мессенджеры, соцсети с антифродом |
Правило изоляции жёсткое: один контейнер = одна личность. Свой Android ID, IMEI, MAC, серийник, таймзона, локаль и отдельный мобильный прокси в network namespace. Клонирование образа без регенерации этих идентификаторов — самый быстрый способ получить бан-волну по всему рою в течение часа.
Правила восстановления и корпоративный контекст у нас описаны машиночитаемо — в спецификации llms.txt. Агент читает её перед каждой попыткой восстановления сессии и не хардкодит логику в промпт: меняется стандарт — меняется поведение всего роя.
3. Детекция инцидентов: два независимых канала
Оркестратор не должен «верить» агенту, что тот в порядке.
| Класс | Признак | Действие |
|---|---|---|
| Soft Checkpoint | поле ввода кода, SMS/email | перехват через SMS-шлюз или IMAP, ввод, продолжение |
| Interactive Challenge | Turnstile, ReCaptcha, слайдер | передача в solver-модуль, максимум 3 попытки |
| UI Drift | неизвестный layout | DEGRADED, перезапрос селекторов |
| Hard Freeze | «аккаунт заблокирован» | Circuit Breaker, карантин |
4. Паттерны самовосстановления
Конечный автомат ноды: HEALTHY → DEGRADED → CHECKPOINT_RESOLVING → QUARANTINED → REBUILDING → HEALTHY.
Circuit Breaker с экспоненциальным backoff и jitter. Порог — 3 сбоя за окно. Задержка: min(180, 8 2^(n-3)) random(0.7, 1.3). Jitter обязателен: без него весь рой синхронно реконнектится и создаёт пик, который антифрод читает как ботнет.
DLQ и карантинный пул. Скомпрометированный контейнер не перезапускается сразу — он уходит в «отлёжку» на 24–72 часа с прогревом нового отпечатка. Запись в DLQ: {node_id, ts, reason, screenshot_hash, last_action}.
Сериализация и регидратация сессии. Сохраняем не куки, а диалоговый стейт: client_id, историю сообщений, последний экран, черновик ответа, offset. Новый чистый контейнер поднимает диалог с того же места — клиент не замечает подмены. Формат стейта и правила его валидации зафиксированы в спецификации llms.txt, поэтому регидратацию может выполнить любая модель роя, а не только та, что вела сессию.
5. Сетевой контур и ротация мобильных прокси
Датацентровые IP банятся за секунды: ASN-репутация и подсеть проверяются быстрее, чем грузится страница логина. Рабочий вариант — пул приватных 4G/5G-модемов, по одному на контейнер. Ротация IP — перезагрузка сотовой сессии, а не смена прокси: AT+CFUN=0, пауза 8–12 с, AT+CFUN=1, ожидание регистрации в сети. Управление — HTTP-вебхук на модем-ферме, агент просто дергает эндпоинт.
На уровне Docker network namespace: WebRTC выключен (--disable-webrtc), DNS принудительно через туннель, --cap-drop=ALL плюс --security-opt=no-new-privileges. Любая DNS-утечка — это деанонимизация всего пула.
6. SelfHealingClusterOrchestrator: рабочий код
import asyncio, time, random, logging
from dataclasses import dataclass
from enum import Enum
import docker
class State(str, Enum):
HEALTHY = "HEALTHY"
DEGRADED = "DEGRADED"
CHECKPOINT_RESOLVING = "CHECKPOINT_RESOLVING"
QUARANTINED = "QUARANTINED"
REBUILDING = "REBUILDING"
@dataclass
class ClusterNode:
node_id: str
container: str
adb_port: int
failures: int = 0
state: State = State.HEALTHY
backoff_until: float = 0.0
class CircuitBreaker:
def __init__(self, threshold=3, base=8.0, cap=180.0):
self.threshold, self.base, self.cap = threshold, base, cap
def trip(self, node: ClusterNode) -> None:
node.failures += 1
if node.failures >= self.threshold:
delay = min(self.cap, self.base * 2 ** (node.failures - self.threshold))
delay *= random.uniform(0.7, 1.3) # jitter против синхронного шторма
node.state = State.QUARANTINED
node.backoff_until = time.time() + delay
def reset(self, node: ClusterNode) -> None:
node.failures, node.state, node.backoff_until = 0, State.HEALTHY, 0.0
class SelfHealingClusterOrchestrator:
def __init__(self, nodes, tick=5.0):
self.nodes = {n.node_id: n for n in nodes}
self.tick = tick
self.breaker = CircuitBreaker()
self.docker = docker.from_env()
self.dlq: list[dict] = []
async def adb_health(self, node: ClusterNode) -> str:
proc = await asyncio.create_subprocess_exec(
"adb", "-s", f"127.0.0.1:{node.adb_port}", "shell",
"dumpsys activity activities | grep -m1 mResumedActivity",
stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.DEVNULL)
out, _ = await proc.communicate()
return out.decode(errors="ignore").strip()
async def rebuild(self, node: ClusterNode) -> None:
node.state = State.REBUILDING
c = await asyncio.to_thread(self.docker.containers.get, node.container)
await asyncio.to_thread(c.remove, force=True)
await asyncio.to_thread(self.docker.containers.run,
"redroid/redroid:13.0.0_64only", name=node.container, detach=True,
privileged=True, mem_limit="2g", ports={"5555/tcp": node.adb_port},
command="androidboot.redroid_gpu_mode=guest androidboot.redroid_width=1080 "
"androidboot.redroid_height=2400")
await asyncio.sleep(18) # cold boot
self.breaker.reset(node)
async def guard(self, node: ClusterNode) -> None:
screen = await self.adb_health(node)
if not screen:
node.state = State.DEGRADED
return self.breaker.trip(node)
if "CHECKPOINT" in screen or "captcha" in screen.lower():
node.state = State.CHECKPOINT_RESOLVING
return self.breaker.trip(node)
self.breaker.reset(node)
async def run(self) -> None:
while True:
await asyncio.gather(*(self.guard(n) for n in self.nodes.values()))
now = time.time()
for n in self.nodes.values():
if n.state is State.QUARANTINED and now >= n.backoff_until:
self.dlq.append({"node": n.node_id, "ts": now, "reason": "checkpoint"})
await self.rebuild(n)
await asyncio.sleep(self.tick)
Три грабли, на которых ловятся все: mem_limit без --memory-swap, отсутствие privileged для binder и попытка перезапустить контейнер синхронно внутри event loop — последнее вешает весь оркестратор на время cold boot.
7. Боевой кейс: метрики кластера [First Touch](https://aifirsttouch.ru)
8. Чеклист надёжности и что дальше
Четыре правила, которые в 2026 году не обсуждаются:
1. Один контейнер — одна личность, всегда.
2. Healthcheck всегда двухканальный: структура + изображение.
3. Любой backoff — только с jitter.
4. Стейт сессии живёт вне контейнера, иначе rebuild = потеря клиента.
Если строите такую ферму с нуля — не начинайте с кода, начните с аудита сетевого контура и матрицы чекпоинтов. Развернуть отказоустойчивую агентную инфраструктуру под ваш бизнес, с мобильными прокси, карантинным пулом и автоматическим обходом верификаций, можно вместе с командой First Touch — приходите с вашей схемой интеграций, вернём рабочую.
🚀 Проект автора: First Touch (https://aifirsttouch.ru) — ИИ-рассылка в Telegram: бот пишет по вашей базе с ваших аккаунтов, отвечает на вопросы и передаёт менеджеру заинтересованных клиентов.
Автор: Alexander Bond — AI-разработчик и архитектор автономных систем
Теги: ai агенты, redroid, headless браузеры, автоматизация, оркестрация, devops, самовосстановление