Я подключил Avito к ChatGPT через MCP: он сравнивает объявления, цены и публичные профили продавцов

Я подключил Avito к ChatGPT через MCP: он сравнивает объявления, цены и публичные профили продавцов

Я сделал MCP-сервер, который позволяет ChatGPT и Claude разбирать публичные объявления Avito: доставать цену, площадь, продавца, похожие варианты и возможные дубли.

Сценарий простой: кидаешь ссылку на квартиру и спрашиваешь: "Цена нормальная? Что проверить перед просмотром?" Модель получает свежие данные не из памяти, а из открытых страниц Avito.

В статье покажу, как я устроил read-only доступ, почему не отдаю модели HTML, как считаю confidence по полям и почему "возможный дубль" нельзя называть доказанным дублем.

Что получилось:

- разбор одного объявления по ссылке;
- сравнение нескольких объявлений;
- поиск похожих вариантов;
- анализ публичного профиля продавца;
- watchlist для сохраненных поисков;
- явные предупреждения, если часть данных не загрузилась.

Реальный пример работы ChatGPT с инструментом.
Реальный пример работы ChatGPT с инструментом.

Почему именно MCP

Мне не хотелось делать отдельную аналитическую панель с фильтрами, таблицами и графиками. Для первого сценария это слишком тяжелый интерфейс. Человек обычно приходит не за отчетом, а за ответом на несколько простых вопросов:

- цена выглядит нормальной или завышенной;

- какие есть похожие объявления рядом;

- что видно по публичному профилю продавца;

- есть ли похожие карточки, которые могут быть дублями;

- какие детали стоит уточнить перед звонком или просмотром.

MCP хорошо ложится на такую задачу. Пользователь пишет обычный запрос в ChatGPT или Claude, модель понимает, что ей нужны внешние данные, вызывает tool call и получает результат в структурированном виде. Дальше она уже работает не "по памяти", а с конкретной выборкой.

Для меня это важное отличие. Я не пытаюсь встроить AI в еще один сайт. Я, наоборот, даю модели небольшой слой данных, чтобы она могла лучше работать внутри уже привычного диалога.

Архитектура в общих чертах

Внутри Avito Fetcher разделен на несколько инструментов. Основные инструменты чтения отвечают за отдельные типы запросов:

- разобрать одно объявление по ссылке;
- разобрать несколько ссылок и вернуть сравнимую структуру;
- получить поисковую выдачу по URL или параметрам;
- открыть карточки из выдачи и собрать подробности;
- посмотреть публичный профиль продавца и его активные объявления;
- найти похожие публикации;
- проанализировать рынок по поисковой выдаче.

Отдельно есть watchlist-контур для сохраненных поисков. Он не делает действий на Avito, но пишет в локальное хранилище проекта: сохраняет параметры поиска, историю увиденных карточек и позволяет потом сравнить новую выдачу с прошлой проверкой. Поэтому "read-only" здесь корректнее понимать именно как read-only относительно Avito.

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

Вместо этого каждый tool call возвращает нормализованный объект: карточки, цены, признаки объявления, ссылки, поля продавца, предупреждения, ошибки загрузки и служебную информацию о том, насколько полными получились данные.

Упрощенно ответ по объявлению можно представить так:

Я подключил Avito к ChatGPT через MCP: он сравнивает объявления, цены и публичные профили продавцов

Это не реальный контракт один в один, а пример того, к какой форме я стремился: модель должна видеть не только значение поля, но и источник. Цена из карточки и цена, вытащенная из текста, должны восприниматься по-разному. Площадь из отдельного атрибута надежнее, чем площадь, найденная регуляркой в описании. Возможный дубль не равен доказанному дублю.

Быстрый и подробный обход

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

Для части задач хватает быстрой выдачи. Например, нужно получить список объявлений по готовой ссылке с фильтрами: заголовок, цена, район, ссылка, короткие признаки из карточки. Это быстро и обычно достаточно для первичного сравнения.

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

Поэтому я не стал делать подробный режим единственным вариантом. В сервере есть разделение:

- быстрый режим возвращает то, что можно собрать из выдачи;
- подробный режим открывает карточки и достает больше полей;
- лимиты по количеству карточек не дают случайно превратить один запрос в длинный обход;
- в ответе явно сохраняются ошибки по отдельным карточкам, а не просто "что-то пошло не так".

Последний пункт оказался важнее, чем кажется. Для AI-ассистента плохой вариант - получить неполную выборку и не заметить этого. Лучше вернуть 12 карточек из 20, но явно указать, какие не загрузились и почему. Тогда модель может честно сказать: "вывод сделан по неполной выборке".

Структура ответа для модели

Я постепенно пришел к тому, что ответ MCP-инструмента должен быть написан не для человека, а для следующего шага рассуждения модели.

Человеку удобно читать текстовый вывод. Модели удобнее работать с устойчивой структурой:

- исходный запрос и нормализованный URL;
- список найденных объявлений;
- поля объявления с источниками;
- вычисленные поля, например цена за квадратный метр; - предупреждения по выборке;
- ошибки загрузки по конкретным URL;
- источники и confidence по отдельным полям;
- служебные признаки вроде статуса подробного обхода и причин совпадения у возможных дублей.

Например, если сервис считает цену за метр, он отдельно фиксирует, откуда взялась площадь. Если площадь найдена в structured attributes, уверенность выше. Если она появилась только в тексте описания, это уже менее надежно. Если площади нет вообще, цена за метр не считается, а не придумывается.

То же самое с продавцом. Публичный профиль может дать список активных объявлений, тип продавца и повторяющиеся признаки, но он не объясняет мотивы человека и не доказывает недобросовестность. В ответе инструмента это должно быть видно, иначе LLM легко сделает слишком уверенный вывод.

Я подключил Avito к ChatGPT через MCP: он сравнивает объявления, цены и публичные профили продавцов

Ошибки загрузки как часть данных

В обычном приложении ошибку часто можно спрятать: показать пустое состояние, попросить повторить попытку, записать лог. В MCP-сценарии так делать опасно. Если модель не видит, что часть данных не загрузилась, она продолжит рассуждать так, будто выборка полная.

Поэтому ошибки загрузки я стараюсь возвращать рядом с результатом:

- страница недоступна;
- объявление снято или изменилось;
- карточка открылась, но часть полей не найдена;
- выдача вернула меньше результатов, чем ожидалось;
- подробный обход остановлен по лимиту;
- структура страницы изменилась и парсер не смог достать поле.

Это делает ответ менее "красивым", зато честным. Модель может написать пользователю: "я сравнил объект только с теми объявлениями, которые удалось загрузить" или "по части карточек нет площади, поэтому медиана по цене за метр неполная".

Для анализа объявлений такая честность полезнее гладкого вывода.

Источники полей и confidence

С Avito легко попасть в ловушку ложной точности. На странице может быть цена, район, площадь и несколько похожих карточек. Кажется, что этого достаточно для уверенного вывода. На практике почти каждое поле имеет нюансы.

Площадь может быть указана в заголовке, в атрибутах или в тексте. Район может быть слишком общим. Этаж может отсутствовать. Продавец может вести несколько похожих объектов. Объявление может быть дублем, а может просто описывать похожую квартиру в том же доме.

Поэтому в ответе есть `field_sources` и `field_confidence`. Это не сложная математическая система, а инженерная дисциплина: если поле взято из явного блока на странице, это видно; если оно извлечено из заголовка или описания, это тоже видно.

Сейчас это устроено примерно так:

- для цены источник может быть `detail` или `search_card`;
- для площади приоритетнее `detail_sections`, а если ее там нет, сервис пробует `detail_title`, `search_title`, `detail_description` и `search_description`;
- `price_per_m2_rub` помечается как `computed`, потому что это расчет от цены и площади;
- `field_confidence` для отдельных полей принимает значения `confirmed`, `inferred` или `missing`;
- для дублей отдельно возвращаются `duplicate_score`, `duplicate_confidence`, `match_reasons` и `mismatches`.

Такие метки помогают модели не писать "это точно дубль" там, где корректнее сказать "похоже на возможный дубль, стоит проверить".

Эвристики дублей

Дубли оказались отдельной интересной задачей. Особенно в недвижимости, где один и тот же объект может появляться у разных продавцов, с немного разными описаниями, ценой и фотографиями.

Я не считаю дубли доказанным фактом. В текущем подходе это именно candidates: похожие карточки, которые стоит открыть и сравнить вручную.

Сравнение строится на наборе признаков:

- количество комнат;
- площадь;
- этаж;
- этажность дома;
- цена и близость цены;
- локация как строка из карточки;
- пересечение слов в заголовке;

Поиск кандидатов запускается в том же городском/категорийном пути, который получается из URL исходного объявления, а дальше карточки ранжируются по этим признакам. У признаков разный вес: площадь влияет сильнее цены, этаж и этажность дают дополнительные баллы, пересечение заголовка помогает, но само по себе мало что доказывает. Даже высокий `duplicate_score` не должен превращаться в финальный вердикт. В интерфейсе для модели такие результаты должны называться "возможные дубли" или "похожие публикации", а не "тот же объект".

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

Я подключил Avito к ChatGPT через MCP: он сравнивает объявления, цены и публичные профили продавцов

Пользовательский контекст

Первый сценарий, на котором я проверяю качество, - недвижимость. Там много полей, которые плохо читаются по одной карточке: цена за метр, сопоставимость района, этаж, состояние, продавец, похожие квартиры рядом.

Запрос может выглядеть совсем просто:

> Проверь это объявление по рынку, найди похожие варианты и скажи, на что обратить внимание перед просмотром.

Хороший ответ в таком сценарии не должен решать за человека, покупать объект или нет. Он должен собрать первичную картину:

- как цена выглядит относительно похожих объявлений;
- есть ли достаточно близкая выборка;
- какие объявления стоит открыть рядом;
- где данные неполные;
- какие вопросы задать продавцу.

Те же инструменты можно применять и в других категориях Avito: поиск, сравнение карточек, просмотр публичных объявлений продавца. Но недвижимость пока самый показательный полигон, потому что там цена ошибки выше, а данные в карточках достаточно разнообразные.

Ограничения, которые важно проговорить

Avito Fetcher работает только с общедоступными данными. Он не логинится в Avito, не читает личные сообщения, не видит закрытую информацию и не делает действий от имени пользователя.

По отношению к Avito это read-only MCP-сервер. Он не публикует объявления, не редактирует карточки, не пишет продавцам, не нажимает кнопки и не пытается автоматизировать поведение пользователя на площадке. При этом внутри проекта есть локальные записи для служебных сценариев вроде сохраненных поисков.

Это не официальный API Avito. Сервис зависит от структуры открытых страниц. Если меняются разметка, фильтры или доступность полей, парсеры приходится адаптировать.

Инструмент не предназначен для массового обхода, обхода ограничений площадки или копирования базы объявлений. Это экспериментальный слой контекста для индивидуального анализа.

Это не юридическая, техническая или риелторская проверка. Возможные дубли, странности в профиле продавца и отклонение цены от выборки - это сигналы для дальнейшей проверки, а не доказательства.

И еще одно ограничение: подробный обход объявлений всегда медленнее обычной выдачи. Если открыть много карточек и попытаться достать из каждой все поля, пользователь заметит задержку. Поэтому приходится выбирать между полнотой и скоростью, а не притворяться, что можно всегда получать идеальный результат мгновенно.

Что оказалось самым полезным в разработке

Главный вывод для меня: MCP-инструмент для LLM нужно проектировать не как парсер страниц, а как поставщика контекста для рассуждения.

Если просто вернуть данные, модель все равно может сделать слишком смелый вывод. Если вернуть данные вместе с источниками, ошибками, ограничениями и уровнем уверенности, у нее появляется шанс ответить аккуратнее.

В этом смысле Avito Fetcher для меня не про "AI сам найдет лучшую квартиру". Скорее про более скучную, но полезную задачу: сократить ручное сравнение, убрать часть переключения между вкладками и дать модели достаточно фактов, чтобы она не фантазировала там, где нужны конкретные объявления.

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

Конец

P.S. О развитии Avito Fetcher, новостях из мира ИИ и опыте интеграции AI-инструментов я рассказываю в своем Телеграм-канале. Если интересно, можете заглянуть!

1