Как заменить 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 chromiumimport asyncio, sqlite3from dataclasses import dataclass, asdictfrom urllib.parse import urljoin, urlparsefrom urllib.robotparser import RobotFileParserimport aiohttpfrom bs4 import BeautifulSoupfrom playwright.async_api import async_playwright@dataclassclass PageRecord: url: str status: int title: str depth: intclass 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 FrogFirecrawl — API, спроектированный под LLM-потребителей: сам обходит дерево сайта, рендерит JavaScript и отдаёт чистый Markdown вместо сырого HTML. Для публичных сайтов это пять строк вместо ста:```# pip install firecrawl-pyfrom firecrawl import FirecrawlAppapp = FirecrawlApp(api_key="fc-***")# /crawl обходит дерево сайта сам и отдаёт готовый Markdownresult = 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-learnimport csvfrom sentence_transformers import SentenceTransformerfrom sklearn.cluster import KMeanswith 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 npfrom sentence_transformers import SentenceTransformermodel = 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)# Страницы доноров и акцептора уже собраны скиллом из раздела 1links = [ {"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 jsonfrom openai import OpenAIfrom crawler_skill import CrawlerSkillclient = 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-оркестратор для решений и интерпретации.*К сожалению, я не располагаю техническими возможностями для прогонов на нескольких боевых сайтах. Если у кого-то будет возможность прогнать это на живых проектах — буду признателен за результаты в комментариях. Но в любом случае заранее спасибо всем, кто найдёт время написать своё мнение о предлагаемом решении нейрокомбайна.*
ссылка на оригинал статьи https://habr.com/ru/articles/1096874/