Как заменить 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


