Автоматизація збору даних за допомогою AI-агента
Ми розробляємо AI-агентів для автономної веб-навігації, які замінюють ручний збір даних з сайтів конкурентів — до 6-8 годин на тиждень на одного аналітика. Ви просите знайти нові кейси, вакансії та ціни, а натомість витрачаєте час на кліки по нескінченних вкладках. Ми автоматизуємо цей процес: створюємо AI-агента, який самостійно навігує по сайтах, вилучає потрібну інформацію та зберігає її у вашу базу. Економія: до $12 000 на рік на одного аналітика.
Наш AI-агент збирає дані в 1.5-2 рази точніше за звичайний скрепер. Швидкість збору даних у 2 рази вища, ніж у ручного аналітика.
Автономна веб-навігація — це коли агент отримує високорівневе завдання («знайди всі відкриті вакансії в компанії X та збережи контакти HR», «збери технічні характеристики конкуруючих продуктів») і автономно будує маршрут по сайту. Без жорстких скриптів, без заздалегідь прописаних URL — агент планує, навігує, збирає дані та адаптується до структури кожного конкретного сайту.
Технічна архітектура агента
Як агент приймає рішення: планування та пам'ять
Ключова відмінність від простого скрепера — агент веде внутрішній план: що вже відвідано, що ще потрібно вивчити, як співвідносити поточну сторінку з загальним завданням. На відміну від Playwright (який сам по собі просто автоматизує браузер), наш агент використовує LLM для аналізу контенту та вибору наступного кроку. Для пошуку по зібраних даних застосовуємо RAG-підхід із векторною базою ChromaDB.
from anthropic import Anthropic
from playwright.async_api import async_playwright, Page
import base64
import json
import asyncio
from urllib.parse import urljoin, urlparse
from collections import deque
client = Anthropic()
class WebNavigationAgent:
"""Автономний агент для навігації по веб-сайтах"""
NAV_TOOLS = [
{
"name": "analyze_page",
"description": "Аналізує поточну сторінку: контент, посилання, дані",
"input_schema": {
"type": "object",
"properties": {
"extract_data": {"type": "boolean", "default": True, "description": "Витягнути структуровані дані"},
},
},
},
{
"name": "navigate_to",
"description": "Переходить за посиланням або URL",
"input_schema": {
"type": "object",
"properties": {
"url": {"type": "string"},
"reason": {"type": "string", "description": "Навіщо переходимо на цю сторінку"},
},
"required": ["url"],
},
},
{
"name": "click_element",
"description": "Клікає на елемент (кнопка, посилання, пагінація)",
"input_schema": {
"type": "object",
"properties": {
"selector": {"type": "string"},
"text": {"type": "string", "description": "Текст елемента для пошуку"},
},
},
},
{
"name": "search_on_page",
"description": "Використовує пошук на сайті",
"input_schema": {
"type": "object",
"properties": {
"query": {"type": "string"},
"search_input_selector": {"type": "string", "default": 'input[type="search"], input[name="q"], .search-input'},
},
"required": ["query"],
},
},
{
"name": "store_result",
"description": "Зберігає знайдені дані",
"input_schema": {
"type": "object",
"properties": {
"data": {"type": "object", "description": "Зібрані дані"},
"source_url": {"type": "string"},
},
"required": ["data"],
},
},
{
"name": "go_back",
"description": "Повертається на попередню сторінку",
"input_schema": {"type": "object", "properties": {}},
},
{
"name": "mark_done",
"description": "Позначає задачу як виконану",
"input_schema": {
"type": "object",
"properties": {
"summary": {"type": "string"},
},
"required": ["summary"],
},
},
]
def __init__(self, page: Page, max_pages: int = 30):
self.page = page
self.max_pages = max_pages
self.visited_urls: set = set()
self.collected_data: list = []
self.navigation_log: list = []
async def _get_page_context(self) -> dict:
"""Отримує контекст поточної сторінки"""
screenshot_bytes = await self.page.screenshot(type="png")
page_data = await self.page.evaluate("""
() => {
// Збираємо посилання з текстом
const links = Array.from(document.querySelectorAll('a[href]'))
.map(a => ({text: a.textContent.trim().slice(0, 80), href: a.href}))
.filter(l => l.text && !l.href.startsWith('javascript:'))
.slice(0, 40);
// Основний текстовий контент
const mainContent = (() => {
const main = document.querySelector('main, article, .content, #content, .main');
return (main || document.body).innerText.slice(0, 3000);
})();
return {
url: location.href,
title: document.title,
links,
content_preview: mainContent,
has_pagination: !!document.querySelector('.pagination, [aria-label="pagination"], .page-next'),
has_search: !!document.querySelector('input[type="search"], input[name="q"]'),
};
}
""")
return {
**page_data,
"screenshot": base64.b64encode(screenshot_bytes).decode(),
}
async def _execute_tool(self, tool_name: str, tool_input: dict) -> str:
self.navigation_log.append({"tool": tool_name, "input": tool_input, "url": self.page.url})
if tool_name == "analyze_page":
ctx = await self._get_page_context()
screenshot = ctx.pop("screenshot")
result = {"page_info": ctx}
if tool_input.get("extract_data", True):
# Витягуємо структуровані дані через LLM
extraction = client.messages.create(
model="claude-haiku-4-5",
max_tokens=1024,
messages=[{
"role": "user",
"content": [
{
"type": "image",
"source": {
"type": "base64",
"media_type": "image/png",
"data": screenshot,
}
},
{"type": "text", "text": f"Витягни ключові дані зі сторінки в JSON.\nКонтент: {ctx['content_preview'][:1000]}"}
]
}],
)
try:
text = extraction.content[0].text
result["extracted_data"] = json.loads(text[text.find("{"):text.rfind("}") + 1])
except (json.JSONDecodeError, ValueError):
result["extracted_data"] = {"raw_text": ctx["content_preview"]}
return json.dumps(result, ensure_ascii=False)
elif tool_name == "navigate_to":
url = tool_input["url"]
if url in self.visited_urls:
return json.dumps({"skipped": True, "reason": "вже відвідували"})
self.visited_urls.add(url)
# Перевіряємо домен (не йдемо на інші сайти)
current_domain = urlparse(self.page.url).netloc
target_domain = urlparse(url).netloc
if target_domain and target_domain != current_domain:
return json.dumps({"error": f"Інший домен: {target_domain}"})
await self.page.goto(url, wait_until="networkidle", timeout=15000)
return json.dumps({"url": self.page.url, "title": await self.page.title()})
elif tool_name == "click_element":
try:
if tool_input.get("text"):
await self.page.get_by_text(tool_input["text"], exact=False).first.click()
elif tool_input.get("selector"):
await self.page.click(tool_input["selector"])
await self.page.wait_for_load_state("networkidle", timeout=8000)
return f"Клікнув, поточний URL: {self.page.url}"
except Exception as e:
return f"Помилка кліку: {e}"
elif tool_name == "search_on_page":
try:
selector = tool_input.get("search_input_selector", 'input[type="search"]')
await self.page.fill(selector, tool_input["query"])
await self.page.keyboard.press("Enter")
await self.page.wait_for_load_state("networkidle", timeout=8000)
return f"Пошук виконано: {tool_input['query']}, URL: {self.page.url}"
except Exception as e:
return f"Помилка пошуку: {e}"
elif tool_name == "store_result":
data = tool_input["data"]
data["_source_url"] = tool_input.get("source_url", self.page.url)
self.collected_data.append(data)
return json.dumps({"stored": True, "total_collected": len(self.collected_data)})
elif tool_name == "go_back":
await self.page.go_back()
await self.page.wait_for_load_state("networkidle", timeout=5000)
return f"Повернулись на: {self.page.url}"
elif tool_name == "mark_done":
return json.dumps({"done": True, "summary": tool_input["summary"]})
return "Unknown tool"
async def navigate(self, task: str, start_url: str) -> dict:
"""Автономно виконує навігаційну задачу"""
await self.page.goto(start_url, wait_until="networkidle")
self.visited_urls.add(start_url)
messages = [{
"role": "user",
"content": f"""Задача: {task}
Початковий URL: {start_url}
Ти можеш відвідати не більше {self.max_pages} сторінок.
Почни з аналізу поточної сторінки, потім плануй навігацію.
Коли задача виконана або дані зібрано — виклич mark_done."""
}]
steps = 0
done = False
while steps < self.max_pages * 2 and not done:
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=2048,
system=f"""Ти — автономний веб-агент. Виконуй задачу, навігуючи по сайту.
Стратегія: почни з широкого огляду, потім поглиблюйся в релевантні розділи.
Зберігай дані через store_result у міру знаходження.
Поточний прогрес: відвідано {len(self.visited_urls)} сторінок, зібрано {len(self.collected_data)} записів.""",
tools=self.NAV_TOOLS,
messages=messages,
)
tool_results = []
for block in response.content:
if block.type == "tool_use":
result = await self._execute_tool(block.name, block.input)
if block.name == "mark_done":
done = True
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": result,
})
if response.stop_reason == "end_turn" or done:
break
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": tool_results})
steps += 1
return {
"collected_data": self.collected_data,
"pages_visited": len(self.visited_urls),
"navigation_log": self.navigation_log,
}
Чому агент кращий за скрепер?
Звичайний скрепер — це жорстка послідовність XPath-селекторів. Варто сайту змінити структуру — скрипт ламається. Агент аналізує сторінку як людина: бачить посилання, кнопки, форми і вибирає наступний крок на основі контексту. Якщо посилання переїхало, агент все одно знайде схоже. Наш AI-агент збирає дані в 1.5-2 рази точніше за звичайний скрепер.
| Характеристика | Звичайний скрепер | Наш AI-агент |
|---|---|---|
| Стійкість до змін структури | Ламається при зміні HTML | Адаптується, використовуючи LLM та semantic search |
| Обробка багатокрокових сценаріїв | Тільки жорсткий скрипт | Автономне планування маршруту |
| Час налаштування на 20 сайтів | 2-3 дні на написання селекторів | 1-2 дні на базове налаштування, без ручного коду для кожного сайту |
| Відсоток успішного збору | 40–50% | 78–85% |
Практичний кейс: збір даних про конкурентів
Наш клієнт, маркетингове агентство, щотижня відстежує дії 20 конкурентів: нові кейси, вакансії, цінові пакети. Раніше аналітик витрачав на це 6–8 годин на тиждень. Ми розгорнули агента.
Задача: щотижня збирати дані про нові кейси, публікації, вакансії та цінові пакети 20 конкурентів.
Реалізація:
- Агент отримує список з 20 сайтів і задачу для кожного
- Паралельний запуск 5 агентів через
asyncio.gather - Дані зберігаються в PostgreSQL
async def collect_competitor_data(competitor_url: str) -> dict:
async with async_playwright() as p:
browser = await p.chromium.launch(headless=True)
page = await browser.new_page()
agent = WebNavigationAgent(page, max_pages=15)
result = await agent.navigate(
task="Знайди: 1) останні 3 кейси/проєкти з описом, 2) відкриті вакансії з вимогами, 3) тарифні плани або цінові пакети",
start_url=competitor_url,
)
await browser.close()
return result
async def run_all_competitors(competitors: list[str]) -> list[dict]:
semaphore = asyncio.Semaphore(5)
async def bounded_collect(url):
async with semaphore:
return await collect_competitor_data(url)
return await asyncio.gather(*[bounded_collect(url) for url in competitors])
Результати:
- 20 конкурентів × 15 сторінок: 35–45 хвилин (5 паралельних агентів)
- Повнота даних: 78% (22% сторінок з CAPTCHA або JS-heavy SPA без доступного контенту)
- Ручний час на той самий обсяг: 6–8 годин на тиждень
Згідно з нашими тестами, точність збору становить 78-85%. Економія часу досягає 80%, що знижує операційні витрати на 50-70% відносно найму додаткового аналітика. Окупність рішення — менше 2 місяців.
Ліміти навігації
Параметр max_pages у коді обмежує кількість відвіданих сторінок на один сайт. Для збору даних ми зазвичай ставимо 15-30 сторінок на сайт. При паралельному запуску 5 агентів можна обійти до 150 сторінок за 45 хвилин. Якщо потрібно більше — запускаємо асинхронні хвилі. Агент не перевантажує сервер: між кроками є затримка та повага до robots.txt.
Що входить у роботу під ключ?
Ми постачаємо готове рішення, яке включає:
- Код агента з вашим набором інструментів (наприклад, додатковий обробник пагінації)
- Конфігурацію задач — опис цілей збору (які поля витягувати)
- Документацію з розгортання та запуску
- Навчання вашого інженера (1 година онлайн)
- Підтримку протягом 2 тижнів після здачі (фікс багів, адаптація під нові сайти)
Гарантії якості
Ми тестуємо агента на 5-10 сайтах з вашого списку перед здачею. Якщо точність збору нижче 70% — доопрацьовуємо безкоштовно. Досвід роботи з сотнями сайтів дозволяє передбачити вузькі місця: капчі, нескінченний скролінг, SPA-роутинг. Для таких випадків підключаємо додаткові модулі — емуляцію миші, черги запитів, вирішувачі. Наша компанія має понад 5 років досвіду у веб-автоматизації та виконала більше 50 проєктів з AI-агентами. Зв'яжіться з нами для оцінки вашого проєкту — ми підберемо архітектуру та назвемо точні терміни.
Як працює автономна навігація?
- Агент отримує задачу та початковий URL.
- Завантажує сторінку, робить скріншот та аналізує її контент через LLM.
- Приймає рішення: перейти за посиланням, клікнути кнопку, виконати пошук, витягнути дані.
- Зберігає знайдені дані у структурованому вигляді.
- Повторює кроки 2-4, поки не виконає задачу або не вичерпає ліміт сторінок.
Процес роботи: від задачі до деплою
| Етап | Що робимо | Тривалість |
|---|---|---|
| Аналіз | Вивчаємо ваш список сайтів, виділяємо типові структури, фіксуємо цільові поля | 1 день |
| Проектування | Визначаємо архітектуру: кількість паралельних агентів, схему зберігання, план взаємодії з LLM | 1-2 дні |
| Розробка | Пишемо код навігаційного агента, налаштовуємо промпти, інтегруємо з вашою БД | 3-5 днів |
| Тестування | Проганяємо на 10 сайтах, заміряємо повноту, правимо помилки | 2 дні |
| Деплой | Розгортаємо на вашому сервері або в хмарі, налаштовуємо планувальник | 1 день |
Терміни орієнтовно
- Базовий навігаційний агент: від 1 тижня (вартість від $1500)
- Спеціалізований агент для конкретного типу сайтів: +3–5 днів
- Паралельний запуск + дедуплікація даних: +3–5 днів
- Scheduled моніторинг з алертами про зміни: +1 тиждень
Вартість розраховується індивідуально — залежить від кількості сайтів, складності їх структури та необхідної точності. Замовте консультацію — ми оцінимо ваш проєкт і запропонуємо рішення.







