Назад к блогу

Last30Days: как на самом деле работает исследовательский агент с ранжированием по вовлеченности

Разбор Last30Days по исходному коду: как скилл ищет в Reddit, X, YouTube, Hacker News и Polymarket с локальной машины, ранжирует результаты, удаляет повторы и обрабатывает сбои.

Last30Days: как на самом деле работает исследовательский агент с ранжированием по вовлеченности
VISUAL INPUT last30days-research-agent

Last30Days: как на самом деле работает исследовательский агент с ранжированием по вовлеченности

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

Last30Days ищет на этих площадках с машины пользователя, сочетает отдельные инструменты для разных источников с локальным хранением учетных данных и сортирует результаты по вовлеченности. С такой простой схемой скилл набрал около 39 000 звезд на GitHub и поднялся на первое место в трендах.

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

Зачем нужна глобальная установка

У проекта два способа установки:

# Claude Code, через маркетплейс плагинов — автообновления
/plugin marketplace add mvanhorn/last30days-skill

# Все остальное — Codex, Cursor, Copilot, Gemini CLI, 50+ харнессов
npx skills add mvanhorn/last30days-skill -g

Флаг -g устанавливает скилл для пользователя, а не в отдельный проект. Без него настройку придется повторять в каждом проекте. С ним команда доступна из любого терминала. Для исследовательского инструмента, который нужен между проектами, глобальная установка подходит лучше локальной.

О том же изменении я писал в статье Claude Code Skills и Wayfinder. Скиллы перестали быть настройкой одного репозитория и стали устанавливаемыми версионируемыми пакетами для разных харнессов. Между анонсом v3.3 в мае и версией v3.11.1 в июле 2026 года в Last30Days приняли 175 запросов на слияние за 15 выпусков. Из них 122 прислали 52 участника сообщества.

Какие учетные данные нужны для поиска

У проекта нет центрального пула API или серверного парка для сбора данных. Reddit, Hacker News, Polymarket и GitHub работают сразу. При первой настройке устанавливаются бесплатные консольные инструменты для YouTube, arXiv и Techmeme. X может использовать локальные куки браузера, Bluesky требует пароль приложения, а TikTok, Instagram и LinkedIn работают через настроенный ключ ScrapeCreators. Ключи можно загрузить из .env или связки ключей macOS через scripts/setup-keychain.sh.

Запросы к источникам отправляет локальный процесс. Для X он может использовать авторизованную сессию браузера вместо серверного сборщика данных. Поиск все равно обращается к внешним площадкам и API, но не проходит через центральный сервис Last30Days. Проекту приходится поддерживать разные браузеры и операционные системы, которые по-разному хранят куки. Пользователь при этом сам отвечает за соблюдение правил площадки об автоматическом доступе к аккаунту.

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

Как ранжируются результаты Polymarket

В README сказано, что скилл “оценивает контент по метрикам вовлеченности: голоса, просмотры, объем торгов”. Интеграция с Polymarket в skills/last30days/scripts/lib/polymarket.py устроена точнее:

market_quality = (
    0.50 * vol_score +          # логарифм месячного объема, насыщение ~$9M
    0.25 * liq_score +          # логарифм ликвидности, насыщение ~$1.2M
    0.15 * movement_score +     # максимальное движение цены; 20% = насыщение
    0.10 * competitive_score    # рынки около 50/50 получают бонус
)
relevance = min(1.0, text_score * (0.75 + 0.25 * market_quality))

Итоговая оценка начинается с text_score, который умножается на коэффициент от 0.75 до 1.0. Качество рынка уточняет уже релевантный результат, но само по себе не создает текстовое совпадение. Поэтому предвыборный рынок на $50M не получает место в выдаче по запросу о библиотеке Python только благодаря высокому объему.

Объем и ликвидность имеют логарифмическое насыщение. В формуле min(1.0, log1p(volume) / 16) рынки на $9M и $90M оба достигают предельной оценки за объем. Без ограничения один рынок с крупными участниками мог бы подавить остальные сигналы. Движение цены тоже учитывает время: дневное изменение получает коэффициент 3, недельное 2, месячное 1.

Рынки около 50/50 получают бонус, поскольку в них сохраняется заметное расхождение ожиданий. Рынок на 97% уже близок к решенному исходу.

Дедупликация использует коэффициент Жаккара, а не эмбеддинги

Реализация в lib/dedupe.py начинается с описания """Within-source near-duplicate detection.""", то есть ищет почти одинаковые материалы внутри одного источника. Для сравнения используется коэффициент Жаккара:

def hybrid_similarity(text_a: str, text_b: str) -> float:
    return max(
        jaccard_similarity(get_ngrams(text_a), get_ngrams(text_b)),  # символьные 3-граммы
        token_jaccard(text_a, text_b),                                # токены без стоп-слов
    )

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

В том же файле сохранился отчет об ошибке:

Вакансии дедуплицируются только по точному URL: разные объявления на одной карьерной странице делят между собой тяжелые шаблонные куски (описание компании, TL;DR, соцпакет), которые ломают нечеткое сравнение текстов и схлопывают несвязанные роли (доска из 26 вакансий сжималась до 7).

Двадцать шесть вакансий превращались в семь из-за общего текста о компании. После исправления вакансии сравниваются по точному URL, а не через усложненное нечеткое сравнение. Такое сравнение полезно, когда идентификатора нет, но не должно заменять уже существующий.

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

Что еще ломается

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

Issue #887: общий термин проходит проверку сущности

В тикете приведен запрос AI code review bottleneck: can reviewers still judge AI-produced work. Парсер удаляет review, поскольку считает это слово указанием намерения. Оставшаяся сущность по-прежнему начинается с AI, поэтому _entity_grounded принимает материалы, у которых совпадает только этот первый термин. При воспроизведении рядом с техническими обсуждениями оказались реклама курса, китайская веб-драма и ролик по астрономии.

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

Issue #818: ошибка путей проявляется при глобальной установке

В установленной через npx версии v3.14 несколько инструкций в SKILL.md запускали skills/last30days/scripts/last30days.py относительно текущего рабочего каталога. В инструкции по созданию HTML также оставалось старое обозначение ${SKILL_ROOT} вместо ${SKILL_DIR}. Такие пути работают из клона репозитория, но ломаются при глобальной установке и запуске из постороннего каталога, например ~/work/some-enterprise-app.

Сам движок находится в глобальном каталоге скилла, но инструкция по-прежнему предполагает, что внутри текущего проекта есть клон репозитория.

Issue #463: ненулевой код хука вызывает предупреждение

Этот тикет закрыт. Хук SessionStart check-config.sh возвращал код 1, если после предыдущего запуска еще не было файла истории. Claude Code показывал неблокирующее предупреждение об ошибке хука, но сама сессия продолжала работать.

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

Как скилл ограничивает расходы

Агент может незаметно потратить бюджет на сотни запросов. Первый уровень контроля задает список источников:

  • EXCLUDE_SOURCES=tiktok,instagram жестко блокирует площадки на уровне ядра, еще до планирования вызовов.
  • Perplexity включается явно. TikTok, Instagram и LinkedIn требуют ключ ScrapeCreators и выбора источника. Настроенные службы веб-поиска могут выбираться автоматически, поэтому нужно следить за их ограничениями и стоимостью.

Параметр --preflight сообщает источник конфигурации, план доступа к куки браузера, будущие записи, необязательные команды, доступные источники и переопределенные адреса. Он не читает куки, не записывает настройки или отчет и не запускает исследование. Так можно безопасно проверить разрешения и выбор источников до настоящего запроса.

Еще один способ сэкономить появился в v3.16.0, в PR #827. Комментарии YouTube сначала загружаются бесплатным локальным инструментом yt-dlp. Платный ScrapeCreators используется только после настоящего сбоя локального инструмента и при наличии настроенного токена. Если комментариев просто нет, платный запрос не отправляется.


Источник: все архитектурные детали проверены по репозиторию last30days-skill на версии v3.18.4.