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