ESPN DeportesFiscales identifican 18 pitcheos amañados en juicio ClaseThe Jerusalem PostTurkey says it could help meet Saudi military needs under defense pactוואלההקואליציה בהובלת סעודיה: סיכלנו מתקפות חות'יות נגד ערים בממלכהESPNOrioles activate Bautista after 14-month absence; Mancini retiresBBC NewsMatch of the DayRTP DesportoFC Porto e Benfica medem forças num clássico com liderança em jogoABC News (Australia)Live music fans from across Australia unite to save beloved outback venueThe IndependentRoyal family news live: Earl Spencer accuses King of ‘gaslighting’ him after palace’s response to explosive book claimsThe Hollywood ReporterTom Cruise Reveals How He Cut ‘Digger’ Transformation Time From Six Hours to Under OneANSA SportPiganzoli vince la crono del Giro di Lussemburgo e rafforza il primatoRFISous pression américaine, la Turquie multiplie les mesures visant des intérêts iraniensSDP EspectáculosJosé Ángel Bichir habla de su caída de un tercer piso y revela su estado
The Daily Newsstand · Free, Always
Saturday, September 19, 2026

Как попасть в ответы Perplexity AI и почему эта ИИ-модель берёт не весь текст с цитируемых страниц

Translate

Я прогнал 20 промптов через Perplexity API по три повтора, собрал 60 ответов и у каждой отсылки в тексте связал предложение ответа со страницей источника, на которую оно указывает. Дальше самое трудоёмкое: для каждой такой пары — предложение ответа и страница источника — я искал в HTML кусок текста, максимально похожий на утверждение из ответа. Из 1355 отсылок 948 привели на страницы, которые ещё открываются, и на 448 из них нашёлся пассаж — абзац, пункт списка или ячейка таблицы, — покрывающий хотя бы треть слов утверждения.

Главный результат для того, кто пишет страницы: на сработавшей странице задействовано медианно два кусочка текста из 85, длиной около 49 слов каждый. По числу кусков это 2%, по словам больше — медиана 6,4% текста страницы при среднем 11,6%: сработавший кусок текста длиннее типичного. Планировать имеет смысл не прочтение страницы целиком, а то, что из неё вытащат один самодостаточный абзац.

Всё началось с того, что в логах моего замера видно, какие URL Perplexity ставит в источники, но не видно, что именно со страницы поехало в ответ — весь текст, абзац или одна строка списка.

Perplexity показывает, какие страницы взял в источники, но не показывает, какой текст с них

У каждого источника в ответе Search API возвращается пять полей: title, url, date, last_updated и snippet — «короткая выдержка из статьи, относящаяся к запросу». Поля с полным текстом страницы, который ушёл в модель, в ответе нет вообще. Отдельная оговорка про имена: sources_cited, которое встречается дальше по тексту, — это поле моего лога, куда я нормализую источники ответа; в самом API Perplexity они приходят как search_results и citations. Путать удобное имя из своего кода со схемой чужого ответа — хороший способ потом полчаса искать несуществующее поле в документации.

Сопоставить два факта — что источник процитирован и что именно из него взято — руками нереально: 60 ответов дают больше тысячи номерных пометок.

Датасет: 60 ответов Perplexity (20 промптов по 3 повтора), модель sonar-pro, поиск включён у всех. В них 583 строки в sources_cited, за которыми стоит 220 уникальных URL, и 1407 номерных отсылок [N] в текстах ответов. После отсева коротких обрубков вроде «см. также» без содержательного предложения рядом в разбор ушло 1355 отсылок.

В API Perplexity есть лимит текста с одной страницы, но как её режут, не описано

Работа с фрагментом страницы, а не с её полным текстом, заложена в интерфейс Perplexity Search API впрямую: у клиента есть ручка на то, сколько токенов брать с одной найденной страницы. Параметр search_context_size со значениями low / medium / high управляет глубиной извлечения. У max_tokens_per_page формулировка в документации прямая — максимум токенов содержимого, извлекаемых с каждой результирующей страницы, — а рядом стоит max_tokens, тот же лимит общим счётом на все результаты сразу.

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

Как я делил страницу на абзацы и сравнивал их с ответом: код на Python

Страницу источника я режу не по переводам строк, а по закрывающим тегам </p>, </li>, </h1></h6>, </td>, </blockquote>. Перенос строки в вёрстке не значит ничего, а закрытый тег абзаца, пункта списка или ячейки — это граница единицы смысла. Правило целиком лежит в одной константе, и от неё напрямую зависит, сколько пассажей окажется на странице:

# Границы пассажа — только теги, за которыми стоит единица смысла: абзац, пункт
# списка, заголовок, ячейка, цитата. `</div>` сюда не входит намеренно: вёрстка
# оборачивает в div и обёртку, и её содержимое, поэтому один и тот же текст
# попадает в выборку дважды, а счётчик пассажей раздувается.
TAG_SPLIT = re.compile(r"</(?:p|li|h[1-6]|td|blockquote)>", re.I)
WORD = re.compile(r"[а-яёa-z0-9]+", re.I)


def passages(html):
    """Страница → список видимых кусков: абзац, пункт списка, заголовок, ячейка."""
    body = re.sub(r"<(script|style|noscript|svg)\b.*?</\1>", " ", html, flags=re.S | re.I)
    head = re.search(r"<body\b.*", body, flags=re.S | re.I)
    body = head.group(0) if head else body
    out = []
    for chunk in TAG_SPLIT.split(body):
        txt = htmllib.unescape(re.sub(r"<[^>]+>", " ", chunk))
        txt = re.sub(r"\s+", " ", txt).strip()
        if len(txt.split()) >= 5:
            out.append(txt)
    return out

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

def stems(text):
    s = set()
    for w in WORD.findall(text.lower().replace("ё", "е")):
        if w in STOP or len(w) < 4:
            continue
        s.add(w[:max(4, len(w) - 2)])
    return s


def similarity(a, b):
    """Доля слов утверждения, которые нашлись в пассаже.

    Нормируем на длину предложения ответа, а не на длину пассажа: иначе выигрывают
    короткие куски вроде пунктов меню — три общих слова из трёх дают единицу.
    """
    if not a or not b:
        return 0.0
    return len(a & b) / len(a)

Для каждого предложения ответа с отсылкой [N] я перебираю все кусочки текста страницы N и беру тот, у которого совпадение по основам слов максимальное. Порог вынесен во флаг запуска: разбор имеет смысл смотреть кривой по нескольким порогам, а не одним числом, — что и видно по следующей таблице.

Perplexity пересказывает, а не копирует: почти дословно совпали 12 фраз из 948

Из 948 отсылок, ведущих на открывшиеся страницы, половину слов утверждения и больше набирают 158 — 16,7%:

Покрытие утверждения одним пассажем

Отсылок

Доля

≥ 0,2

706

74,5%

≥ 0,3

448

47,3%

≥ 0,4

253

26,7%

≥ 0,5

158

16,7%

≥ 0,6

70

7,4%

≥ 0,7

27

2,8%

≥ 0,8

12

1,3%

Медиана покрытия по всем 948 парам — 0,286, среднее — 0,309. Типичное утверждение ответа собрано примерно из четверти своих слов, найденных в одном абзаце источника; остальное — формулировка модели или второй источник рядом. Дословного переноса почти нет: совпадение на 80% слов и выше дают 12 отсылок из 948, чуть больше процента. Нейросеть Perplexity пересказывает, а не копирует.

В ответ уходит абзац около 50 слов, и лежать он может в любой части страницы

Кусок текста, который реально совпал с утверждением, — это абзац, а не строчка навигации. В подвыборке от 0,3 (448 отсылок) его медианная длина 49 слов, квартили 29 и 85. При более строгом пороге 0,5 (158 отсылок) медиана 52 слова, квартили 34 и 146 — верхний квартиль уезжает вверх за счёт длинных табличных ячеек.

Показатель

Порог 0,3 (n=448)

Порог 0,5 (n=158)

Длина сработавшего пассажа, слов (25% / медиана / 75%)

29 / 49 / 85

34 / 52 / 146

Пассажей на самой странице: медиана (среднее)

85 (101,3)

76,5 (90,7)

Задействовано пассажей на странице, медиана

2

1

Страниц, где нашёлся хоть один такой пассаж

109 из 146

66 из 146

Отсюда и число из заголовка: у страниц, где матч нашёлся, медиана нарезки — 85 пассажей, в дело идут два. По всем 146 открывшимся страницам медиана чуть ниже, 80. Знаменатель здесь — 146 страниц, которые открылись из 194 участвующих в разборе; все 220 уникальных URL из sources_cited в эту статистику не входят, потому что на часть из них ни одна номерная отсылка не указывает.

Медиана позиции — 0,402 от начала документа при пороге 0,3 и 0,421 при 0,5. В первой трети страницы лежит 41,7% совпадений (порог 0,3) и 40,5% (порог 0,5). Перевес начала есть, но он в пределах двух раз, а не «читается только первый экран»: рабочие куски находятся по всей длине документа, включая последнюю треть.

Причём часть этого перевеса — не про поведение модели, а про то, чем набит хвост типовой страницы. В последних децилях лежат футер, блок перелинковки, форма подписки и контакты. Совпасть с содержательным утверждением ответа они не могут в принципе, поэтому провал последнего дециля (11 совпадений против 55 в первом) я бы не считал чистой мерой того, как глубоко Perplexity читает документ.

Децили позиции сработавшего пассажа в документе: 55 отсылок в первом дециле, 69 во втором, 11 в последнем при пороге 0,3

Позиция найденного пассажа по децилям документа, 448 отсылок при пороге 0,3.

Как работать с Perplexity, чтобы он цитировал страницу: каждый абзац понятен без соседних

Единица, которой оперирует Perplexity в моём разборе, — абзац на 30–50 слов, понятный без соседей. Не страница, не раздел и не полезный контент вообще. Абзац, который попадёт в ответ, должен держаться сам: без местоимений, отсылающих наверх по тексту, без «как я писал выше» и без указательных оборотов на месте названия. Факт, цифра и условие, при котором цифра верна, — в одном куске текста между двумя </p>.

Объём работы тут меньше, чем кажется, когда речь заходит о переписывании страниц. На сработавшей странице задействовано медианно два пассажа из 85 — цитируют не весь текст, а те два-три абзаца, где лежит фактический ответ на вопрос. Начинать имеет смысл с них, а не с равномерной вычитки сайта: найти на странице абзац, ради которого её вообще могут процитировать, и сделать его самодостаточным.

Позиция помогает, но не решает. В первой трети документа лежит 41,7% совпадений, в последней — 21,9% (в последних трёх децилях — 19%), и часть этого разрыва объясняется тем, что в хвосте страницы стоит футер, а не тем, что туда не доходят. Обещать удвоение шанса за один перенос абзаца вверх я бы не стал: 11 совпадений из 448 нашлись в самом последнем дециле, 30 — в предпоследнем. Хоронить конец страницы не нужно, достаточно не прятать туда главное.

Две ошибки в моём коде меняли результат сильнее, чем сами данные

Первая — в знаменателе метрики. Первая версия функции сравнения отличалась от рабочей ровно одной строкой:

def similarity(a, b):
    """a — основы слов утверждения ответа, b — основы слов пассажа страницы."""
    if not a or not b:
        return 0.0
    return len(a & b) / len(b)      # рабочая версия делит на len(a)

Доля слов пассажа, подтвердившихся в утверждении ответа. Я перезапустил эту версию на тех же логах и тех же страницах, чтобы числа не остались на память: при пороге 0,5 старая формула находит матч для 139 отсылок из 948 — 14,7%, ровно тот диапазон, который я и ждал увидеть. Правдоподобно. Но медианная длина найденного пассажа в этой выборке — 9 слов, а 124 матча из 139 короче 12 слов. Абзац в девять слов на живой странице — это подпись, пункт меню или заголовок раздела, из него нельзя взять целое утверждение.

Дальше я вытащил такие строки руками, и паттерн оказался один. Заголовок H2 на странице rookee.ru про выбор брендов нейросетями — «Может ли AI всегда рекомендовать один и тот же бренд?», десять слов — старая метрика засчитала лучшим источником для четырёх разных предложений из разных ответов, один раз со сходством 1,0. Ни одно из четырёх не пересказывало вопрос из заголовка: они говорили про упаковку бренда как сущности, про внешние сигналы доверия, про сценарии использования. Заголовок просто содержал общие слова темы — бренд, рекомендовать, AI, — и этого хватало, чтобы обыграть по счёту настоящие абзацы. Всего таких идеальных единиц набралось 12, и среди них прайс-строки вроде «02 GEO в нейросетях NEW от 80 000 ₽/мес».

Причина арифметическая. len(a & b) / len(b) делит число общих основ на длину пассажа, и чем пассаж короче, тем легче ему набрать высокую долю: у заголовка из десяти основ два-три совпадения дают 0,3 и выше. У настоящего абзаца из полусотни слов те же два-три совпадения размываются длинным знаменателем. Метрика искала не пассаж, который отвечает на утверждение, а самый короткий пассаж, где нашлось хоть что-то общее. После правки знаменателя на длину предложения ответа медиана длины сработавшего пассажа при том же пороге 0,5 выросла с 9 слов до 52, а число матчей — со 139 до 158. Воспроизвести старый результат может кто угодно: поменять len(a) обратно на len(b) и прогнать заново.

Смена знаменателя метрики: при нормировке на длину пассажа медиана 9 слов и 139 матчей, при нормировке на длину утверждения — 52 слова и 158 матчей

Одна строка в метрике: что попадает в выборку до и после смены знаменателя.

Вторая ошибка про другое. В первой версии константы TAG_SPLIT среди границ пассажа стоял </div>, и это неверно по смыслу: div не единица содержания, а контейнер вёрстки. Страница оборачивает в div и блок, и его содержимое, поэтому один и тот же текст попадал в выборку несколько раз, а счётчик пассажей рос. На 146 открывшихся страницах медиана числа пассажей падает с 94,5 до 80, как только div уходит из регулярки. Медианная страница меняется мало — отношение 1,05, — но на вёрстке с глубокой вложенностью разница в разы: у ai.pixeltools.ru 15 пассажей против 185 со старым правилом, у одной из страниц megagroup.ru — 14 против 107. Таких страниц, где старое правило раздувало счёт вдвое и сильнее, десять из 146.

Отсюда важная оговорка к 85-и кусочкам текстов на странице: эта цифра измеряет мою нарезку, а не устройство Perplexity. Сравнивать его с чужими разборами без сверки правила резки бессмысленно — сойдутся два разных числа об одном и том же тексте.

Через месяц почти треть ссылок из ответов Perplexity ведёт на мёртвые страницы

Из 194 URL, участвующих в разборе, сегодня открылись 146 — остальные 48 отдают 404, редирект в заглушку или таймаут. В пересчёте на отсылки это 407 из 1355: почти треть номерных пометок в ответах указывает на страницу, которой уже нет. Если брать все 220 уникальных URL из sources_cited, а не только те, на которые ссылаются отсылки, картина та же — открылось 167.

Между замером и проверкой прошёл ровно месяц: 17 августа я снял ответы и источники, 18 сентября выкачал HTML для разбора. Ссылка внутри ответа Perplexity не архивируется вместе с ответом, она живёт ровно столько, сколько живёт страница.

Практический вывод тут не для автора страниц, а для тех, кто сам гоняет такие замеры: HTML источников надо сохранять в момент прогона. Через месяц треть выборки просто не откроется, и восстановить её будет неоткуда.

Что нужно понимать. Эти данные — всего один прогон на русскоязычных коммерческих запросах. На других языках, в другой тематике и на другой вёрстке распределение может выглядеть иначе, второго прогона для сравнения у меня нет. Проверить на своей нише несложно: нужны ответы с номерными отсылками, HTML источников, снятый сразу, и тридцать строк кода из этой статьи — весь метод здесь и состоит из нарезки по тегам и одной дроби.

View the original on Хабр

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.