SEO-комбайн на AI-агентах. seo.. seo. ИИ.. seo. ИИ. ии-агенты.. seo. ИИ. ии-агенты. ИИ-агенты для бизнеса.. seo. ИИ. ии-агенты. ИИ-агенты для бизнеса. нейросети.. seo. ИИ. ии-агенты. ИИ-агенты для бизнеса. нейросети. Поисковая оптимизация.

Как заменить Screaming Frog, Semrush и Ahrefs связкой LLM и API-шлюзов

В SEO-среде жив стереотип: LLM — это текстовый генератор, а технический аудит делается только классическим софтом. Аргумент звучит весомо: у языковой модели нет сетевого движка, она не умеет обходить дерево сайта и работать с протоколами в реальном времени. Замечание корректное, но вывод из него устарел. Проблема в том, что его авторы путают два уровня: LLM не должна собирать данные — она должна их оркестрировать. Современная агентская архитектура выносит сбор данных на внешние инструменты (tool calling), а модели оставляет планирование маршрута обхода, принятие решений на развилках и интерпретацию результатов. В таком виде связка закрывает значительную часть рутинных SEO-задач — от технического аудита до кластеризации семантики.

Ниже — разбор архитектуры, актуального стека 2026 года, рабочий код для каждого слоя и честный список того, что замена не закрывает.

1. Краулинг: LLM как оркестратор, а не парсер

Классический десктопный краулер (Screaming Frog, Netpeak Spider) работает линейно: HTTP-запросы, коды ответов, парсинг HTML, граф связей. ИИ в этой схеме исторически жил на финале: специалист выгружал CSV и просил ChatGPT найти паттерны мусорных URL и написать регулярки.

Агентский подход устроен иначе — модель вынесена за пределы цикла обхода: [Агент / LLM] — план задачи: «собрать URL, Title, ответ сервера» │ tool call ▼ [Скилл краулера: headless-браузер / scraping-API] │ обход дерева сайта ▼ [Локальное хранилище: SQLite / CSV] │ агрегат, а не сырой HTML ▼ [LLM] — анализ сводных данных, формирование выводов

Ключевое решение: сырой HTML не попадает в контекстное окно. Модель получает не тысячу страниц, а агрегированный отчёт о них. Это одновременно экономия токенов на 2–3 порядка и снятие ограничения на размер сайта.

Что это даёт относительно классического софта:

SPA и рендеринг. Сайты на React/Vue/Angular не требуют ручной настройки рендеринга — headless-браузер (Playwright) отрабатывает скролл, клики и асинхронную подгрузку нативно. Живучесть. Сменилась пагинация или селектор — обычный скрипт падает. Агент видит, что элементы не найдены, переформулирует стратегию поиска и продолжает сессию. Точечные повторные запросы. Вместо полного пересканирования сайта ради проверки одной страницы агент запрашивает только её.

Скилл краулера целиком — обратите внимание, что наружу уходит только сводка, а не сырые страницы:

# pip install playwright aiohttp beautifulsoup4
# playwright install chromium

import asyncio, sqlite3
from dataclasses import dataclass, asdict
from urllib.parse import urljoin, urlparse
from urllib.robotparser import RobotFileParser

import aiohttp
from bs4 import BeautifulSoup
from playwright.async_api import async_playwright

@dataclass
class PageRecord:
    url: str
    status: int
    title: str
    depth: int

class CrawlerSkill:
    """Агент вызывает этот класс как tool. HTML наружу не уходит —
    наружу уходит только сводка и локальная БД."""

    def __init__(self, start_url: str, max_pages: int = 500,
                 delay: float = 1.0, render_js: bool = False):
        self.start = start_url
        self.max_pages = max_pages
        self.delay = delay
        self.render_js = render_js
        self.domain = urlparse(start_url).netloc
        self.robots = RobotFileParser(start_url + "/robots.txt")
        try:
            self.robots.read()
        except OSError:
            pass  # robots.txt недоступен — не падаем, продолжаем

    def _allowed(self, url: str) -> bool:
        return self.robots.can_fetch("SEOAgent", url)

    async def _extract_links(self, html: str) -> list[str]:
        soup = BeautifulSoup(html, "html.parser")
        for tag in soup(["script", "style", "noscript"]):
            tag.decompose()
        links = set()
        for a in soup.find_all("a", href=True):
            href = urljoin(self.start, a["href"].split("#")[0])
            if urlparse(href).netloc == self.domain and href.startswith("http"):
                links.add(href)
        return list(links)

    async def run(self) -> dict:
        db = sqlite3.connect("crawl.db")
        db.execute('''CREATE TABLE IF NOT EXISTS pages
                      (url TEXT PRIMARY KEY, status INT, title TEXT, depth INT)''')
        seen, queue, done = {self.start}, [(self.start, 0)], []
        async with aiohttp.ClientSession(
            headers={"User-Agent": "SEOAgent/1.0 (contact@example.com)"}) as s:

            if self.render_js:  # SPA — подключаем headless-браузер
                self._pw = await async_playwright().start()
                self._browser = await self._pw.chromium.launch()

            while queue and len(done) < self.max_pages:
                url, depth = queue.pop(0)
                if not self._allowed(url):
                    continue
                try:
                    if self.render_js:
                        page = await self._browser.new_page()
                        resp = await page.goto(url, timeout=30000)
                        html = await page.content()
                        status = resp.status if resp else 0
                        await page.close()
                    else:
                        async with s.get(url, timeout=30) as r:
                            html, status = await r.text(), r.status
                except (aiohttp.ClientError, asyncio.TimeoutError):
                    continue

                title = BeautifulSoup(html, "html.parser").title
                title = title.text.strip()[:200] if title else ""
                db.execute("INSERT OR REPLACE INTO pages VALUES (?,?,?,?)",
                           (url, status, title, depth))
                db.commit()
                done.append(asdict(PageRecord(url, status, title, depth)))

                if depth < 5:
                    queue += [(l, depth + 1) for l in await self._extract_links(html)
                              if l not in seen]
                    seen |= {l for l, _ in queue}
                await asyncio.sleep(self.delay)  # вежливость к серверу

        # Агенту возвращаем АГРЕГАТ, а не сырые страницы — экономия токенов
        report = {
            "crawled": len(done),
            "status_codes": dict(db.execute(
                "SELECT status, COUNT(*) FROM pages GROUP BY status").fetchall()),
            "duplicate_titles": [t for t, c in db.execute(
                "SELECT title, COUNT(*) c FROM pages GROUP BY title HAVING c > 1")],
            "sample_errors": db.execute(
                "SELECT url, status FROM pages WHERE status >= 400 LIMIT 20").fetchall(),
        }
        db.close()
        return report```

Предупреждение: по умолчанию этот скилл «вежливый» — он читает robots.txt, соблюдает задержки между запросами и использует честный User-Agent с контактом. Я сознательно не встраиваю в него обход антибот-систем — почему, объясню в следующем разделе.

## 2. Транспортный уровень: чем заменить Screaming Frog

Firecrawl — API, спроектированный под LLM-потребителей: сам обходит дерево сайта, рендерит JavaScript и отдаёт чистый Markdown вместо сырого HTML. Для публичных сайтов это пять строк вместо ста:

```# pip install firecrawl-py
from firecrawl import FirecrawlApp

app = FirecrawlApp(api_key="fc-***")
# /crawl обходит дерево сайта сам и отдаёт готовый Markdown
result = app.crawl_url(
    "https://example-catalog.ru/catalog",
    params={"limit": 200, "scrapeOptions": {"formats": ["markdown"]}},
)
pages = result["data"]          # список страниц в Markdown```

Crawl4AI — open-source альтернатива для self-hosted-сценариев: если инфраструктура не должна отдавать ваши данные третьей стороне, это рабочий вариант.

Browserless.io — удалённый Chrome по API: агент формулирует цель («дождись элемента X, кликни, верни результат»), а не селекторы.

Оговорюсь отдельно, потому что в популярной подаче эта тема выглядит лучше, чем есть. Сопоставление фингерпринтов, ротация User-Agent и агрессивный обход антибот-систем (Cloudflare и т. п.) — юридически серая зона: парсинг публичных данных обычно допустим, но обход средств защиты сайта и нарушение ToS — это риск для проекта, а не «фича» агента. Я сознательно исключил из стека сервисы, чьё основное назначение — именно обход защит, и ограничиваюсь публичными страницами, robots.txt и вежливыми rate limits. Это же правило действует и для приёма «ходить агентом на бесплатные виджеты чужих сервисов»: он нарушает ToS этих сервисов, и в статье, рассчитанной на инженерную аудиторию, подавать его как преимущество — путь к справедливой критике в комментариях.

## 3. Данные: Just-in-Time вместо терабайтных баз

Semrush и Ahrefs продают подписку на непрерывно обновляемые базы ключей и ссылок. Для конкретного проекта используется малая доля этих данных, но платите вы за всю базу.
Агентский подход — точечные запросы в момент задачи:

Сбор семантики: коннектор к DataForSEO или Keys.so. По команде «собери семантику категории X» агент получает JSON с ключами конкурентов, частотностью и CPC, фильтрует мусор и группирует по интенту. Кластеризацию по смыслу (а не по пересечению URL в ТОП-10) делает эмбеддинг-модель:

```# cluster.py — кластеризация семантики по смыслу, а не по пересечению URL
# pip install sentence-transformers scikit-learn
import csv
from sentence_transformers import SentenceTransformer
from sklearn.cluster import KMeans

with open("queries.csv", encoding="utf-8") as f:        # колонка: keyword
    queries = [row["keyword"] for row in csv.DictReader(f)]

model = SentenceTransformer("intfloat/multilingual-e5-base")
X = model.encode([f"query: {q}" for q in queries], normalize_embeddings=True)

k = max(3, len(queries) // 25)                          # ~25 запросов на кластер
labels = KMeans(n_clusters=k, n_init=10).fit_predict(X)

groups = {}
for q, label in zip(queries, labels):
    groups.setdefault(int(label), []).append(q)

with open("clusters.csv", "w", newline="", encoding="utf-8") as f:
    writer = csv.writer(f)
    for label, q in sorted(groups.items()):
        writer.writerow([label] + q)

Контекстный аудит ссылок: через доступные индексные API агент получает список бэклинков, а затем оценивает не только численный рейтинг донора, но и смысловую релевантность окружения — совпадают ли темы донора и акцептора на уровне эмбеддингов. Классический DR-отчёт этого принципиально не показывает:

import numpy as np
from sentence_transformers import SentenceTransformer

model = SentenceTransformer("intfloat/multilingual-e5-base")

def embed(text: str) -> np.ndarray:
    prefix = "passage: " if len(text) > 50 else "query: "
    return model.encode(prefix + text, normalize_embeddings=True)

# Страницы доноров и акцептора уже собраны скиллом из раздела 1
links = [
    {"url": "https://forum-auto.ru/t/123",
     "context": "обсуждение тормозной системы ВАЗ"},
    {"url": "https://kitchen-recipes.info/post/9",
     "context": "рецепты салатов"},
]
acceptor = embed("как выбрать тормозные колодки для ВАЗ")

for link in links:
    link["score"] = round(float(np.dot(acceptor, embed(link["context"]))), 3)

print(sorted(links, key=lambda x: -x["score"]))
# донор с score < 0.5 — вероятный спам в тематике, в аутрич его не тратим

Проверка перелинковки: тот же приём сравнения эмбеддингов донора и акцептора выявляет ситуации «анкор релевантен, но околоссылочный текст размывает тематику» — для этого не нужен дорогой софт, достаточно embedding-модели.

Для рунетовской специфики SpyWords и Keys.so дают данные по контекстной рекламе и видимости, которых в западных базах физически нет.

4. Оркестрация: агентский цикл и No-Code

Минимальный агентский цикл на tool calling — модель сама решает, когда вызвать краулер и с какими параметрами:

# pip install openai — API совместим с DeepSeek, OpenAI, vLLM и др.
import json
from openai import OpenAI
from crawler_skill import CrawlerSkill

client = OpenAI(base_url="https://api.deepseek.com", api_key="sk-***")

tools = [{
    "type": "function",
    "function": {
        "name": "crawl_site",
        "description": "Краулит сайт, возвращает сводку: коды ответов, "
                       "дубликаты title, страницы с ошибками",
        "parameters": {"type": "object", "properties": {
            "url": {"type": "string"},
            "max_pages": {"type": "integer", "default": 300},
            "render_js": {"type": "boolean", "default": False},
        }, "required": ["url"]},
    },
}]

def execute(name: str, args: dict) -> str:
    if name == "crawl_site":
        crawler = CrawlerSkill(args["url"], max_pages=args.get("max_pages", 300),
                               render_js=args.get("render_js", False))
        return json.dumps(crawler.run(), ensure_ascii=False)
    raise ValueError(f"Неизвестный tool: {name}")

messages = [{"role": "user",
             "content": "Проведи техаудит https://example.ru. Если сайт на JS — "
                        "включи рендеринг. По итогам дай топ-5 проблем."}]

while True:
    resp = client.chat.completions.create(model="deepseek-chat",
                                          messages=messages, tools=tools)
    msg = resp.choices[0].message
    messages.append(msg.model_dump(exclude_none=True))

    if not msg.tool_calls:
        print(msg.content)  # финальный аналитический отчёт
        break
    for call in msg.tool_calls:
        args = json.loads(call.function.arguments)
        messages.append({"role": "tool", "tool_call_id": call.id,
                         "content": execute(call.function.name, args)})```

Если кода хочется меньше — платформы оркестрации:

Dify — open-source, self-host бесплатный; визуальный конструктор цепочек «URL → Firecrawl → LLM → отчёт».

n8n — self-hosted автоматизация с AI-узлами. Скелет сценария ежедневного аудита (импортируется как основа; точные имена узлов сверяю с актуальной библиотекой перед запуском):

```{
  "name": "SEO-аудит по расписанию",
  "nodes": [
    { "name": "Cron", "type": "n8n-nodes-base.scheduleTrigger" },
    { "name": "Список сайтов", "type": "n8n-nodes-base.googleSheets",
      "parameters": { "operation": "read", "range": "sites!A2:A" } },
    { "name": "Firecrawl", "type": "n8n-nodes-base.httpRequest",
      "parameters": { "method": "POST", "url": "https://api.firecrawl.dev/v1/crawl",
                      "jsonBody": "={{ {"url": $json.site, "limit": 200} }}" } },
    { "name": "LLM", "type": "@n8n/n8n-nodes-langchain.openAi",
      "parameters": { "model": "deepseek-chat",
                      "messages": [{ "role": "user",
                        "content": "Найди SEO-проблемы, верни JSON: issue, severity, fix" }] } },
    { "name": "Telegram", "type": "n8n-nodes-base.telegram" }
  ],
  "connections": {
    "Cron": { "main": [[{ "node": "Список сайтов" }]] },
    "Список сайтов": { "main": [[{ "node": "Firecrawl" }]] },
    "Firecrawl": { "main": [[{ "node": "LLM" }]] },
    "LLM": { "main": [[{ "node": "Telegram" }]] }
  }
}```

CrewAI / LangGraph — если всё-таки хочется кода: многоролевая оркестрация для более сложных сценариев.

## 5. Модели в 2026 году: что изменилось

Важное обновление к реалиям, потому что прошлогодние советы здесь уже не работают:
Бесплатный Gemini больше не «абсолютно бесплатный». В декабре 2025 года Google урезал квоты Free Tier на 50–92% — рассчитывать на него как на рабочую лошадку для массового парсинга уже нельзя.

DeepSeek — по-прежнему самый дешёвый вариант. V4 Pro: $0.44 за миллион входных и $0.88 за миллион выходных токенов, с удвоением цены в пиковые часы. Для задач аудита, где вход многословнее выхода, это оптимальный профиль расходов.

Локальные модели (Llama, Qwen через Ollama/vLLM) — разумный вариант для чувствительных данных: их можно развернуть на своём сервере и исключить отправку клиентского контента во внешние API.

Предупреждение: экономия на модели легко превращается в перерасход на токенах, если агент начинает читать сырые страницы. В архитектуре из раздела 1 это исключено конструкцией — модель видит только агрегаты. Если же вы даёте LLM полный HTML каждой страницы, счёт за API растёт линейно с размером сайта, и на крупных проектах переплата обгоняет годовую лицензию любого классического софта.

## 6. Ограничения, которые никуда не делись

**Честный список того, что агентская модель НЕ закрывает:**

Highload-краулинг. Портал на 500 000+ URL в многопоточном режиме классический краулер пройдёт быстрее и стабильнее: накладные расходы на оркестрацию каждой итерации у агентов выше.

Стоимость глубокой аналитики. Скилл «чистого» паука дешёвый, но как только модель начинает читать и осмыслять код страниц на лету, расходы на токены на масштабах реальных сайтов могут превысить стоимость годовой лицензии классического софта.

Гарантии. Агент — вероятностная система. Для критичных процессов (мониторинг доступности, SLA-отчётность) детерминированный скрипт остаётся правильным выбором; агент лучше для аналитики и первичной разведки.

### Вывод

Правильная ментальная модель такая: LLM — не инструмент сбора данных, а планировщик и аналитик, а данные приходят из специализированных API и headless-браузеров. Для команд с ограниченным бюджетом и большим количеством рутинных SEO-задач агентский стек экономит и деньги, и часы ручной работы. Для высоконагруженного обхода и задач с жёстким SLA классический софт по-прежнему в деле.

Рациональный план — не «заменить всё», а гибрид: детерминированный сбор сырых метрик + LLM-оркестратор для решений и интерпретации.

*К сожалению, я не располагаю техническими возможностями для прогонов на нескольких боевых сайтах. Если у кого-то будет возможность прогнать это на живых проектах — буду признателен за результаты в комментариях. Но в любом случае заранее спасибо всем, кто найдёт время написать своё мнение о предлагаемом решении нейрокомбайна.*

Автор: Yegoryev

Источник