Ручное заполнение однотипных веб-форм — регистрация поставщиков, подача заявок на тендеры, ввод данных в госорганы — отнимает часы рабочего времени. В среднем сотрудник тратит 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-агента в ваши процессы — мы оценим сложность и подберём оптимальное решение. Свяжитесь с нами для оценки вашего проекта — мы подберём оптимальное решение и рассчитаем сроки индивидуально. Первичная оценка бесплатна.







