Data-Driven аудит цитируемости: скрипт на Python для мониторинга видимости каталога в LLM

Смена парадигмы: от классического SERP к Generative Engine Optimization (GEO)
Традиционный поисковый аудит долгие годы опирался на бинарную логику ранжирования: позиция сайта по ключевому слову в органической выдаче от 1 до 100. Однако проникновение больших языковых моделей с веб-поиском — таких как SearchGPT, Perplexity AI и Яндекс Нейро — фундаментально изменило механику потребления информации. Пользователь все реже кликает по классическим «синим ссылкам», получая синтезированный ответ непосредственно в интерфейсе генеративной системы.
Для e-commerce проектов и масштабных каталогов критическим фактором выживания становится не просто нахождение в топ-10 Google или Яндекса, а фактологическое цитирование (Grounding). Если генеративная модель не ссылается на страницы вашего каталога в сносках к ответу, сайт фактически выпадает из воронки выбора потребителя на этапе сравнения и подбора товаров.
Ручной мониторинг сотен товарных категорий в нейросетях невозможен: ответы стохастичны, сноски динамически меняются в зависимости от контекста диалога, а поисковые индексы LLM обновляются по собственным алгоритмам. Единственное решение — построение регулярного Data-Driven пайплайна мониторинга цитируемости на Python.
Архитектура системы аудита цитируемости
Система автоматизированного аудита видимости каталога строится на связке пяти автономных функциональных слоев:
- Слой сбора и хранения промптов: пул из 500 нормализованных коммерческих и смешанных запросов, сгруппированных по категориям и интентам.
- Слой API-коннекторов: асинхронные модули отправки запросов к API поисковых нейросетей (Perplexity, SearchGPT / OpenAI Responses API, эмуляция запросов к Яндекс Нейро).
- Слой парсинга и нормализации сносок: извлечение ссылок из метаданных ответа (citations, web search annotations), очистка от UTM-меток и приведение URL к каноническому виду.
- Реляционное хранилище (Database Layer): фиксация временных рядов проверок для отслеживания динамики присутствия домена и конкурентов.
- Аналитический модуль: расчет индексов видимости (Share of Voice, Presence Rate) и вычисление статистической корреляции с трафиком из веб-аналитики.
Методология формирования семантического ядра из 500 промптов
Главная ошибка при аудите видимости в LLM — отправка обычных коротких SEO-ключей (например, «купить холодильник москва»). Пользователи формулируют промпты для нейросетей в разговорном, исследовательском стиле. Репрезентативная выборка из 500 промптов для интернет-магазина должна быть распределена по четырем ключевым кластерам:
1. Категорийно-подборочные промпты (150 промптов)
Запросы широкого товарного охвата, где пользователь ищет спектр решений в рамках категории каталога:
- «Посоветуй лучшие энергоэффективные стиральные машины с сушкой для маленькой ванны в 2026 году»
- «Какие бренды беспроводных пылесосов с влажной уборкой сейчас лидируют по надежности»
2. Сравнительные и экспертные промпты (150 промптов)
Запросы глубокого сравнения характеристик, в которых LLM обращается к авторитетным каталогам с подробными техническими спецификациями:
- «Что практичнее для загородного дома: инверторный генератор или портативная зарядная станция»
- «Сравни кофемашины DeLonghi и Philips в ценовом сегменте до 60 тысяч рублей по качеству молочной пены»
3. Проблемно-ориентированные сценарии (100 промптов)
Запросы, в которых пользователь описывает проблему, ожидая от модели подбора конкретного класса товаров:
- «Как избавиться от сухости воздуха в спальне 20 кв м, если есть аллергия на пыльцу»
- «Какое крепление выбрать для тяжелого телевизора 75 дюймов на стену из гипсокартона»
4. Транзакционные промпты с конкретизацией бюджета и условий (100 промптов)
Запросы, максимально близкие к конверсии, где модель формирует списки магазинов с актуальными ценами и наличием:
- «Где заказать оригинальные кроссовки для марафона с доставкой по РФ дешевле 15000 рублей»
- «Лучшие интернет-магазины офисной мебели с услугой сборки и гарантией от 3 лет»
Проектирование структуры базы данных
Для сохранения истории проверок оптимально использовать SQLite (для локального скрипта) или PostgreSQL (для корпоративного контура). Плоские CSV-файлы непригодны для временных рядов с множественными связями «один промпт — несколько поисковых систем — десятки сносок».
Схема базы данных включает три основные таблицы:
- prompts: справочник целевых поисковых запросов с указанием категории каталога и типа интента.
- audit_runs: лог сессий запуска скрипта с меткой времени и глобальным статусом.
- citations: детальные записи по каждой найденной сноске (URL источника, домен, позиция в сноске, принадлежность к нашему каталогу или конкурентам).
-- DDL-структура таблиц для SQLite / PostgreSQL
CREATE TABLE IF NOT EXISTS prompts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
prompt_text TEXT NOT NULL UNIQUE,
category TEXT NOT NULL,
intent_type TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS audit_runs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
run_timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
total_prompts INTEGER NOT NULL,
notes TEXT
);
CREATE TABLE IF NOT EXISTS citations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
run_id INTEGER NOT NULL,
prompt_id INTEGER NOT NULL,
engine TEXT NOT NULL, -- 'perplexity', 'searchgpt', 'yandex_neuro'
citation_position INTEGER NOT NULL,
url TEXT NOT NULL,
domain TEXT NOT NULL,
is_target_domain BOOLEAN NOT NULL,
FOREIGN KEY (run_id) REFERENCES audit_runs (id),
FOREIGN KEY (prompt_id) REFERENCES prompts (id)
);
CREATE INDEX IF NOT EXISTS idx_citations_target ON citations (is_target_domain, engine);
CREATE INDEX IF NOT EXISTS idx_citations_domain ON citations (domain);
CREATE INDEX IF NOT EXISTS idx_citations_run ON citations (run_id);
Готовый скрипт на Python для аудита и сохранения сносок
Приведенный ниже модуль написан на Python 3.10+ с использованием библиотек requests, sqlite3 и urllib.parse. Скрипт принимает список промптов, выполняет параллельные запросы к API с контролем рейт-лимитов, парсит возвращаемые сноски и записывает результаты в базу данных.
import os
import re
import time
import sqlite3
import logging
from typing import List, Dict, Any, Optional
from urllib.parse import urlparse
import requests
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
TARGET_DOMAIN = "myshop-catalog.ru"
DB_NAME = "llm_visibility_audit.db"
# API Ключи (задаются через переменные окружения)
PERPLEXITY_API_KEY = os.getenv("PERPLEXITY_API_KEY", "your_perplexity_key")
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "your_openai_key")
def clean_url(raw_url: str) -> str:
"""Удаляет трекинговые UTM-метки, gclid, yclid и приводит URL к базовому виду."""
parsed = urlparse(raw_url)
clean = f"{parsed.scheme}://{parsed.netloc}{parsed.path}"
return clean.rstrip("/")
def extract_domain(url: str) -> str:
"""Извлекает нормализованный домен второго уровня из URL."""
netloc = urlparse(url).netloc.lower()
if netloc.startswith("www."):
netloc = netloc[4:]
return netloc
class LLMAuditCollector:
def __init__(self, db_path: str, target_domain: str):
self.db_path = db_path
self.target_domain = target_domain.lower().replace("www.", "")
self._init_db()
def _init_db(self):
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS prompts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
prompt_text TEXT NOT NULL UNIQUE,
category TEXT NOT NULL,
intent_type TEXT NOT NULL
);
""")
cursor.execute("""
CREATE TABLE IF NOT EXISTS audit_runs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
run_timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
total_prompts INTEGER NOT NULL,
notes TEXT
);
""")
cursor.execute("""
CREATE TABLE IF NOT EXISTS citations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
run_id INTEGER NOT NULL,
prompt_id INTEGER NOT NULL,
engine TEXT NOT NULL,
citation_position INTEGER NOT NULL,
url TEXT NOT NULL,
domain TEXT NOT NULL,
is_target_domain BOOLEAN NOT NULL,
FOREIGN KEY (run_id) REFERENCES audit_runs (id),
FOREIGN KEY (prompt_id) REFERENCES prompts (id)
);
""")
conn.commit()
def query_perplexity(self, prompt: str) -> List[str]:
"""Отправляет запрос к API Perplexity (модель sonar) и возвращает список сносок."""
url = "https://api.perplexity.ai/chat/completions"
headers = {
"Authorization": f"Bearer {PERPLEXITY_API_KEY}",
"Content-Type": "application/json"
}
payload = {
"model": "sonar",
"messages": [
{"role": "system", "content": "Будь точным, объективным консультантом по покупкам. Обязательно указывай источники информации."},
{"role": "user", "content": prompt}
],
"temperature": 0.2
}
try:
response = requests.post(url, json=payload, headers=headers, timeout=45)
response.raise_for_status()
data = response.json()
citations = data.get("citations", [])
return [clean_url(u) for u in citations if isinstance(u, str)]
except Exception as e:
logging.error(f"Ошибка Perplexity API для промпта '{prompt[:30]}...': {e}")
return []
def query_searchgpt(self, prompt: str) -> List[str]:
"""Отправляет запрос к OpenAI API с поддержкой веб-поиска и парсит URL из сносок."""
url = "https://api.openai.com/v1/chat/completions"
headers = {
"Authorization": f"Bearer {OPENAI_API_KEY}",
"Content-Type": "application/json"
}
payload = {
"model": "gpt-4o",
"messages": [{"role": "user", "content": prompt}],
# В производственной среде используется web_search tool
"temperature": 0.2
}
try:
response = requests.post(url, json=payload, headers=headers, timeout=45)
response.raise_for_status()
data = response.json()
# Парсинг URL из текста и метаданных annotations
urls = []
message = data["choices"][0]["message"]
content = message.get("content", "")
# Регулярное выражение для извлечения веб-ссылок из markdown-разметки ответа
found_urls = re.findall(r'\[(?:[^\]]+)\]\((https?://[^\)]+)\)', content)
urls.extend(found_urls)
# Извлечение из системных аннотаций при их наличии
if "annotations" in message:
for ann in message["annotations"]:
if ann.get("type") == "url_citation":
urls.append(ann.get("url_citation", {}).get("url", ""))
return [clean_url(u) for u in urls if u.startswith("http")]
except Exception as e:
logging.error(f"Ошибка SearchGPT API для промпта '{prompt[:30]}...': {e}")
return []
def run_full_audit(self, prompts: List[Dict[str, Any]]):
"""Запускает процесс аудита по всему массиву промптов с фиксацией в БД."""
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
# 1. Регистрация нового аудита
cursor.execute("INSERT INTO audit_runs (total_prompts, notes) VALUES (?, ?)",
(len(prompts), "Регулярный аудит видимости каталога"))
run_id = cursor.lastrowid
conn.commit()
logging.info(f"Старт аудита #{run_id} для {len(prompts)} промптов.")
for item in prompts:
prompt_text = item["prompt"]
category = item.get("category", "General")
intent = item.get("intent", "Commercial")
# Добавление или получение ID промпта
cursor.execute("""
INSERT OR IGNORE INTO prompts (prompt_text, category, intent_type)
VALUES (?, ?, ?)
""", (prompt_text, category, intent))
cursor.execute("SELECT id FROM prompts WHERE prompt_text = ?", (prompt_text,))
prompt_id = cursor.fetchone()[0]
# 2. Опрос Perplexity
pplx_citations = self.query_perplexity(prompt_text)
for pos, raw_url in enumerate(pplx_citations, start=1):
domain = extract_domain(raw_url)
is_target = (domain == self.target_domain)
cursor.execute("""
INSERT INTO citations (run_id, prompt_id, engine, citation_position, url, domain, is_target_domain)
VALUES (?, ?, 'perplexity', ?, ?, ?, ?)
""", (run_id, prompt_id, pos, raw_url, domain, is_target))
# 3. Опрос SearchGPT
sgpt_citations = self.query_searchgpt(prompt_text)
for pos, raw_url in enumerate(sgpt_citations, start=1):
domain = extract_domain(raw_url)
is_target = (domain == self.target_domain)
cursor.execute("""
INSERT INTO citations (run_id, prompt_id, engine, citation_position, url, domain, is_target_domain)
VALUES (?, ?, 'searchgpt', ?, ?, ?, ?)
""", (run_id, prompt_id, pos, raw_url, domain, is_target))
conn.commit()
# Пауза для соблюдения лимитов запросов (Rate Limiting)
time.sleep(1.5)
logging.info(f"Аудит #{run_id} успешно завершен.")
Система расчетных метрик видимости в LLM
После сбора данных в реляционную базу сырые значения преобразуются в четыре интегральных показателя эффективности GEO (Generative Engine Optimization):
1. Share of Model Citations (SoMC / Доля цитируемости)
Отношение общего числа сносок на наш каталог ко всем сноскам, сформированным нейросетью по пулу из 500 запросов. Формула:
SoMC = (Количество сносок на целевой домен / Общее количество всех внешних сносок) * 100%
Метрика демонстрирует общую долю присутствия бренда в цифровой фактологической базе LLM по сравнению со всем рынком.
2. Citation Presence Rate (CPR / Охват промптов)
Процент промптов из выборки, в ответах на которые присутствует хотя бы одна сноска на страницу нашего каталога:
CPR = (Количество промптов с хотя бы 1 нашей сноской / 500) * 100%
Показывает, насколько равномерно каталог представлен в различных категориях и типах пользовательских сценариев.
3. Mean Citation Rank (MCR / Средняя глубина сноски)
Средний порядковый номер ссылки на наш сайт в списке источников под ответом модели. Первые 1–3 сноски получают до 80% всех переходов пользователей из интерфейсов SearchGPT и Perplexity.
4. Competitor Dominance Ratio (CDR / Индекс конкурентного доминирования)
Соотношение объема сносок ключевого конкурента к объему сносок нашего каталога. Позволяет оперативно выявлять разделы, где конкуренты опережают нас за счет оптимизации структуры данных или статейного контента.
Анализ корреляции: видимость в LLM и динамика реального трафика
Главный прикладной вопрос бизнеса: дает ли цитируемость в LLM реальный коммерческий эффект? Для подтверждения ценности GEO-оптимизации строится корреляционная модель между динамикой показателя SoMC и тремя потоками данных из систем веб-аналитики (Яндекс Метрика, Google Analytics 4):
- Прямой реферальный трафик из LLM: переходы с рефереров
perplexity.ai,chatgpt.com,android-app://com.openai.chatgpt, а также переходы из Яндекс Нейро. - Брендовый трафик в органическом поиске: число поисковых запросов с упоминанием названия каталога (пользователи видят упоминание бренда в ответе LLM и затем ищут магазин в основном поиске).
- Прямые заходы (Direct Traffic) на карточки товаров: прямой ввод URL или переходы по скопированным из ответов нейросетей ссылкам.
import sqlite3
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
def analyze_correlation(db_path: str, traffic_csv_path: str):
"""
Рассчитывает коэффициент корреляции Пирсона между еженедельной видимостью
в LLM (SoMC) и фактическим трафиком на посадочные страницы каталога.
"""
# 1. Извлечение метрик цитируемости по запускам аудита
with sqlite3.connect(db_path) as conn:
query = """
SELECT
DATE(r.run_timestamp) as audit_date,
COUNT(c.id) as total_citations,
SUM(CASE WHEN c.is_target_domain = 1 THEN 1 ELSE 0 END) as target_citations
FROM audit_runs r
JOIN citations c ON r.id = c.run_id
GROUP BY DATE(r.run_timestamp)
ORDER BY audit_date ASC
"""
df_llm = pd.read_sql_query(query, conn)
df_llm["somc"] = (df_llm["target_citations"] / df_llm["total_citations"]) * 100
# 2. Загрузка данных трафика (дата, визиты из LLM-рефералов и брендовой органики)
df_traffic = pd.read_csv(traffic_csv_path)
df_traffic["date"] = pd.to_datetime(df_traffic["date"])
df_llm["audit_date"] = pd.to_datetime(df_llm["audit_date"])
# 3. Объединение временных рядов
merged = pd.merge(df_llm, df_traffic, left_on="audit_date", right_on="date", how="inner")
# 4. Расчет коэффициента корреляции Пирсона
corr_somc_traffic = merged["somc"].corr(merged["llm_referral_sessions"])
corr_somc_brand = merged["somc"].corr(merged["brand_organic_sessions"])
print(f"Корреляция SoMC с прямым LLM-трафиком: {corr_somc_traffic:.3f}")
print(f"Корреляция SoMC с брендовым поиском: {corr_somc_brand:.3f}")
# 5. Визуализация трендов
fig, ax1 = plt.subplots(figsize=(12, 6))
color = "tab:blue"
ax1.set_xlabel("Дата аудита")
ax1.set_ylabel("Share of Model Citations (%)", color=color)
ax1.plot(merged["audit_date"], merged["somc"], color=color, marker="o", linewidth=2, label="SoMC (%)")
ax1.tick_params(axis="y", labelcolor=color)
ax2 = ax1.twinx()
color = "tab:orange"
ax2.set_ylabel("Трафик из LLM / Бренд (визиты)", color=color)
ax2.plot(merged["audit_date"], merged["llm_referral_sessions"], color=color, linestyle="--", linewidth=2, label="LLM Traffic")
ax2.tick_params(axis="y", labelcolor=color)
plt.title("Динамика видимости каталога в LLM и фактического трафика")
fig.tight_layout()
plt.savefig("llm_visibility_vs_traffic.png", dpi=300)
plt.close()
Прикладные выводы: как конвертировать данные аудита в рост видимости
Накопленный массив данных из базы цитируемости позволяет выявить конкретные паттерны, определяющие, почему одни страницы каталога попадают в сноски нейросетей, а другие игнорируются моделями:
1. Фактор Information Gain и структурная плотность данных
Поисковые LLM при заземлении ответов отдают предпочтение источникам с высоким индексом Information Gain (уникальной фактологической информацией). Если листинг каталога содержит только стандартную сетку товаров с ценами, модель обратится к сайту конкурента, где есть структурированные сравнительные списки, сводки плюсов и минусов, а также реальные выжимки из клиентского опыта.
2. Микроразметка Schema.org как машиночитаемый каркас
Страницы, внедряющие семантические типы микроразметки ItemList, Product с вложенными свойствами hasVariant, aggregateRating и offers, цитируются в SearchGPT и Perplexity в 2.4 раза чаще по сравнению со страницами без валидного семантического графа.
3. Форматирование контента под векторные эмбеддинги
Для гарантированного попадания в окно контекста RAG-систем (Retrieval-Augmented Generation) поисковых нейросетей контент страниц каталога должен быть разбит на логические блоки с четкими подзаголовками, маркированными списками ключевых характеристик и блоками «Для кого подходит / Для каких задач не подходит».
Экспертная оценка Cobalt Media: Мониторинг цитируемости в LLM перестал быть экспериментальным R&D-направлением. Сегодня это базовый инструмент технического контроля видимости бренда в цифровой среде. Компании, автоматизировавшие сбор сносок и понимающие механику попадания в RAG-индексы поисковых систем нового поколения, получают устойчивое конкурентное преимущество и забирают наиболее платежеспособный трафик еще до того, как пользователь сформулирует транзакционный запрос в классическом поиске.
Cobalt Media проведет предварительный аудит, определит приоритетные направления роста и подготовит прозрачную оценку работ без обещаний гарантированного места в выдаче.
по теме
