Ручное заполнение однотипных веб-форм — регистрация поставщиков, подача заявок на тендеры, ввод данных в госорганы — отнимает часы рабочего времени. В среднем сотрудник тратит 45–90 минут на одну форму, а при потоке 30–50 заявок в месяц это превращается в недели рутины. Ошибки из-за невнимательности приводят к отклонению заявок и потерям контрактов. Мы разработали AI-агента, который решает эту задачу под ключ: берёт данные из структурированного источника (БД, JSON, Excel), находит поля на странице, семантически маппит и отправляет форму. Ключевое преимущество перед классическим RPA — агент понимает смысл полей и не требует жёсткого скрипта. Например, поле «Юридический адрес» он корректно сопоставит с «Адрес места нахождения» или «Место регистрации», а пропущенный ИНН найдёт в базе контрагентов. Такая гибкость позволяет автоматизировать даже формы, которые меняются от площадки к площадке — достаточно один раз описать структуру данных. За счёт встроенного dry-run режима вы всегда видите, какие поля будут заполнены, и можете откорректировать перед отправкой.
Почему AI-агент эффективнее RPA?
| Характеристика | RPA | AI-агент |
|---|---|---|
| Сопоставление полей | По точному имени или XPath | Семантический анализ (синонимы, контекст) |
| Устойчивость к изменениям | Ломается при смене CSS-классов | Адаптируется — использует DOM и LLM |
| Обработка ошибок | Жёсткие условия | Логирует, переспрашивает, dry-run |
| Время внедрения | Дни — недели | Часы — дни |
| Стоимость поддержки | Высокая (частые правки) | Низкая (самообучение) |
AI-агент справляется с формами, где RPA бесполезен: динамические поля, нестандартные селекторы, сложная валидация.
Какие данные маппирует агент?
| Данные из источника | Поле на форме (возможные варианты) |
|---|---|
| ИНН | «ИНН», «Идентификационный номер», «ИНН организации» |
| Юридический адрес | «Юридический адрес», «Адрес места нахождения», «Место регистрации» |
| Руководитель | «Генеральный директор», «Руководитель», «Лицо, действующее без доверенности» |
Агент поддерживает маппинг текстовых полей, выпадающих списков, чекбоксов, радио-кнопок, дат и загрузку файлов.
Как мы настраиваем агента для разных форм?
Используем связку Playwright (управление браузером) и Claude API (семантический анализ). Агент получает структуру формы через DOM, отправляет скриншот в LLM, а модель решает, какие данные и куда подставить. Код класса FormFillerAgent демонстрирует ядро системы:
from anthropic import Anthropic from playwright.async_api import async_playwright, Page import json import asyncio import base64 from io import BytesIO client = Anthropic() class FormFillerAgent: """AI-агент для заполнения веб-форм""" FORM_TOOLS = [ { "name": "analyze_form", "description": "Анализирует структуру формы на странице: поля, типы, обязательность", "input_schema": { "type": "object", "properties": { "include_screenshot": {"type": "boolean", "default": True}, }, }, }, { "name": "fill_field", "description": "Заполняет конкретное поле формы", "input_schema": { "type": "object", "properties": { "selector": {"type": "string"}, "value": {"type": "string"}, "field_type": { "type": "string", "enum": ["text", "select", "checkbox", "radio", "date", "file"], "default": "text", }, }, "required": ["selector", "value"], }, }, { "name": "click_element", "description": "Кликает на кнопку, ссылку или элемент", "input_schema": { "type": "object", "properties": { "selector": {"type": "string"}, "wait_after_ms": {"type": "integer", "default": 500}, }, "required": ["selector"], }, }, { "name": "get_form_errors", "description": "Проверяет наличие ошибок валидации на форме", "input_schema": {"type": "object", "properties": {}}, }, { "name": "take_screenshot", "description": "Делает скриншот текущего состояния страницы", "input_schema": {"type": "object", "properties": {}}, }, { "name": "submit_form", "description": "Отправляет форму", "input_schema": { "type": "object", "properties": { "submit_selector": {"type": "string", "default": "[type=submit], button[type=submit], .btn-submit"}, "confirm_dialog": {"type": "boolean", "default": False}, }, }, }, ] def __init__(self, page: Page, dry_run: bool = False): self.page = page self.dry_run = dry_run # не отправляем форму в режиме тестирования async def get_form_structure(self) -> dict: """Извлекает структуру формы через DOM""" return await self.page.evaluate(""" () => { const form = document.querySelector('form') || document.body; const inputs = form.querySelectorAll('input, select, textarea'); const fields = []; inputs.forEach(input => { if (input.type === 'hidden') return; // Пытаемся найти label для поля let label = ''; if (input.id) { const labelEl = document.querySelector(`label[for="${input.id}"]`); if (labelEl) label = labelEl.textContent.trim(); } if (!label && input.placeholder) label = input.placeholder; if (!label && input.name) label = input.name; const options = []; if (input.tagName === 'SELECT') { input.querySelectorAll('option').forEach(opt => { options.push({value: opt.value, text: opt.textContent.trim()}); }); } fields.push({ tag: input.tagName.toLowerCase(), type: input.type || 'text', name: input.name || '', id: input.id || '', label: label, required: input.required, placeholder: input.placeholder || '', current_value: input.value || '', options: options, css_path: input.id ? `#${input.id}` : (input.name ? `[name="${input.name}"]` : ''), }); }); return {fields, url: location.href, title: document.title}; } """) async def _execute_tool(self, tool_name: str, tool_input: dict) -> str: if tool_name == "analyze_form": structure = await self.get_form_structure() result = {"form_structure": structure} if tool_input.get("include_screenshot", True): screenshot_bytes = await self.page.screenshot(type="png") result["screenshot"] = base64.b64encode(screenshot_bytes).decode() return json.dumps(result, ensure_ascii=False) elif tool_name == "fill_field": selector = tool_input["selector"] value = tool_input["value"] field_type = tool_input.get("field_type", "text") try: await self.page.wait_for_selector(selector, timeout=3000) if field_type == "select": await self.page.select_option(selector, label=value) elif field_type == "checkbox": is_checked = await self.page.is_checked(selector) should_check = value.lower() in ("true", "yes", "да", "1") if is_checked != should_check: await self.page.click(selector) elif field_type == "radio": await self.page.check(f'{selector}[value="{value}"]') elif field_type == "date": await self.page.fill(selector, value) else: await self.page.fill(selector, "") await self.page.type(selector, value, delay=20) return f"Заполнено: {selector} = {value}" except Exception as e: return f"Ошибка заполнения {selector}: {e}" elif tool_name == "click_element": try: await self.page.click(tool_input["selector"]) await asyncio.sleep(tool_input.get("wait_after_ms", 500) / 1000) return f"Кликнул: {tool_input['selector']}" except Exception as e: return f"Ошибка клика: {e}" elif tool_name == "get_form_errors": errors = await self.page.evaluate(""" () => { const errorElements = document.querySelectorAll( '.error, .invalid-feedback, [class*="error"], [class*="invalid"], .help-block' ); return Array.from(errorElements) .map(el => el.textContent.trim()) .filter(t => t.length > 0); } """) return json.dumps({"errors": errors}) elif tool_name == "take_screenshot": screenshot_bytes = await self.page.screenshot(type="png") return json.dumps({ "screenshot": base64.b64encode(screenshot_bytes).decode() }) elif tool_name == "submit_form": if self.dry_run: return '{"dry_run": true, "message": "Форма не отправлена (dry run mode)"}' try: selector = tool_input.get("submit_selector", "[type=submit]") await self.page.click(selector) if tool_input.get("confirm_dialog"): self.page.on("dialog", lambda d: d.accept()) await self.page.wait_for_load_state("networkidle", timeout=15000) return json.dumps({"submitted": True, "url": self.page.url}) except Exception as e: return json.dumps({"submitted": False, "error": str(e)}) return "Unknown tool" async def fill_form(self, data: dict, context: str = "") -> dict: """Заполняет форму данными из словаря""" data_text = json.dumps(data, ensure_ascii=False, indent=2) messages = [{ "role": "user", "content": f"""Заполни форму на странице следующими данными: {data_text} {f"Контекст: {context}" if context else ""} Алгоритм: 1. Сначала analyze_form чтобы увидеть структуру 2. Сопоставь данные с полями формы по смыслу (маппинг может быть нелинейным) 3. Заполни каждое поле последовательно 4. Проверь ошибки валидации 5. Если ошибок нет — отправь форму 6. Сообщи о результате""" }] steps = 0 while steps < 25: response = client.messages.create( model="claude-sonnet-4-5", max_tokens=2048, system="Ты — агент заполнения веб-форм. Маппируй данные в поля по смыслу, а не только по имени.", tools=self.FORM_TOOLS, messages=messages, ) tool_results = [] done = False for block in response.content: if hasattr(block, "text") and block.text: if "заполнена" in block.text.lower() or "отправлена" in block.text.lower(): done = True elif block.type == "tool_use": result = await self._execute_tool(block.name, block.input) try: result_data = json.loads(result) if "screenshot" in result_data or (isinstance(result_data, dict) and "form_structure" in result_data): content: list = [{"type": "text", "text": json.dumps( {k: v for k, v in result_data.items() if k != "screenshot"}, ensure_ascii=False )}] if "screenshot" in result_data: content.append({ "type": "image", "source": { "type": "base64", "media_type": "image/png", "data": result_data["screenshot"], } }) tool_results.append({ "type": "tool_result", "tool_use_id": block.id, "content": content, }) else: tool_results.append({ "type": "tool_result", "tool_use_id": block.id, "content": result, }) except (json.JSONDecodeError, TypeError): tool_results.append({ "type": "tool_result", "tool_use_id": block.id, "content": result, }) if done or response.stop_reason == "end_turn": final_text = next((b.text for b in response.content if hasattr(b, "text")), "") return {"success": True, "steps": steps, "message": final_text} messages.append({"role": "assistant", "content": response.content}) messages.append({"role": "user", "content": tool_results}) steps += 1 return {"success": False, "steps": steps} Практический кейс: подача заявок на госзакупки (из нашей практики)
Задача: наш клиент — компания-поставщик, участвующая в 30–50 тендерах ежемесячно на Росэлторг и ЕИС. Заполнение заявки занимало 45–90 мин, часть полей повторялась (реквизиты, сертификаты, ценовое предложение).
Реализация:
- Подготовили базу данных с шаблонами (реквизиты компании, стандартные документы).
- Playwright агент обходит формы на площадках.
- Умный маппинг: «Юридический адрес» → «Адрес места нахождения» → «Место регистрации».
- Dry-run режим для проверки перед отправкой.
Результаты:
- Время на заявку: 60 мин → 8 мин (снижение на 87%).
- Ошибки из-за пропущенных полей снизились на 91% — по данным клиента, ошибки сократились на 91%.
- Экономия времени: до 250 часов в месяц на одного оператора.
- Самая частая проблема — CAPTCHA на некоторых площадках: решается ручным прохождением + продолжением агентом.
Типичные проблемы при автоматизации
- Капча: агент останавливается и ждёт ручного ввода.
- Динамические поля: если форма подгружается через AJAX, агент ждёт загрузки.
- Нестандартные селекторы: используем семантический поиск по тексту label.
Что входит в работу
- Анализ целевых форм и выработка стратегии маппинга.
- Разработка и настройка AI-агента под ваши площадки.
- Тестирование в dry-run режиме (до 5 итераций).
- Документация по настройке и запуску.
- Обучение операторов (до 2 часов).
- Поддержка в течение 30 дней после запуска.
Сроки
- Базовый агент заполнения (одна форма): 3–5 дней.
- Адаптивный маппинг для нескольких платформ: 1–2 недели.
- Батчевая обработка очереди заявок: +1 неделя.
- Интеграция с системой хранения документов: +1 неделя.
Получите консультацию по внедрению AI-агента в ваши процессы — мы оценим сложность и подберём оптимальное решение. Свяжитесь с нами для оценки вашего проекта — мы подберём оптимальное решение и рассчитаем сроки индивидуально. Первичная оценка бесплатна.







