Как я создал RFP Search — агрегатор запросов предложений (RFP) на базе ИИ
Если вы когда-либо пытались найти государственные запросы предложений (RFP) на цифровые услуги, вы знаете, каково это. Возможности разбросаны по SAM.gov, порталам государственных закупок штатов, Grants.gov, ООН, ЕС и дюжине других мест. У каждого сайта свой интерфейс поиска, свой формат и свои особенности. Проверять их все вручную утомительно, поэтому я создал RFP Search, чтобы это делала программа.
Что он делает
RFP Search ежедневно сканирует 11 государственных и некоммерческих источников закупок, пропускает каждую возможность через модель ИИ для извлечения структурированных данных и предоставляет все через единый интерфейс поиска с фильтрами и интерактивной картой.
Источники включают SAM.gov, Grants.gov, техасский ESBD, калифорнийский Cal eProcure, флоридский MFMP, портал TED ЕС, Глобальный рынок ООН, NYC Open Data, Федеральный реестр, USAspending и Brave Search для более широкого охвата. Каждое утро вас ждут свежие результаты.
Архитектура
Вся система работает на Cloudflare. Три Worker, одна база данных D1 и ноль традиционных серверов. Я структурировал это как монорепозиторий с помощью Turborepo и npm workspaces.
У каждого из трех Worker есть четкая задача.
- rfp-web — это фронтенд, созданный с помощью React Router v7 (преемник Remix). Он отвечает за пользовательский интерфейс поиска, фильтры и интерактивную карту Leaflet с кластеризацией маркеров.
- rfp-api — это REST API на Hono, который обрабатывает поисковые запросы, пагинацию, статистику и геоданные для карты.
- rfp-scraper — это запланированный Worker, который запускает ночные cron-задания для получения и обработки RFP из всех источников.
Веб-Worker общается с API-Worker через Cloudflare Service Bindings. Никакого HTTP, никакого CORS, никакого публичного API-эндпоинта. Привязка вызывает API напрямую в сети Cloudflare, что быстрее и безопаснее.
Скрейпер
Это самая интересная часть. Каждый источник — это плагин, реализующий простой интерфейс. Хотите добавить новый источник? Создайте файл, реализуйте интерфейс, зарегистрируйте его и добавьте строку в базу данных. Вот и все.
Скрейпер работает в три этапа с задержкой (9:00, 9:15 и 9:30 UTC), чтобы оставаться в пределах лимитов процессорного времени Worker. Каждый этап обрабатывает подмножество источников.
Пакет 1 (9:00 UTC) SAM.gov, TED EU, USAspending
Пакет 2 (9:15 UTC) Texas ESBD, Cal eProcure, Brave Search, Grants.gov, Federal Register
Пакет 3 (9:30 UTC) Florida MFMP, UNGM, NYC Open DataКаждый плагин источника знает, как получать и анализировать собственный формат данных. Некоторые обращаются к REST API, некоторые парсят HTML, некоторые используют RSS-каналы. Архитектура плагинов позволяет изолировать эту сложность. Если источник меняет свой формат, мне нужно обновить только один файл.
Скрейпер также отслеживает сбои. Если источник три раза подряд выдает ошибку, он автоматически деактивируется, чтобы не тратить циклы и не засорять логи.
Извлечение с помощью ИИ
Необработанные списки RFP бывают беспорядочными. У некоторых есть подробные описания, у других — только заголовок и ссылка. Я использую Cloudflare Workers AI с Llama 3.1 70B Instruct для приведения всего к единой структуре.
Каждый RFP проходит один вызов ИИ, который извлекает структурированные поля (срок подачи, бюджет, местоположение, контактная информация), метаданные для принятия решений (требуемые сертификаты, тип контракта, возможность удаленной работы, предполагаемый размер команды, технологический стек), категории (веб-разработка, CMS, облачные технологии, ИИ, миграция и многое другое) и краткое резюме из 2-3 предложений.
Один вызов на RFP, один промпт, который делает все. Это позволяет снизить затраты на Workers AI, сохраняя при этом хорошее качество извлечения. Модель на удивление хорошо справляется с извлечением структурированных данных из неструктурированного текста государственных закупок.
База данных
Cloudflare D1 (управляемый SQLite на границе сети) хранит все. Основная таблица rfps содержит более 30 полей, охватывающих основные данные, поля, извлеченные ИИ, и метаданные для принятия решений.
Для поиска я использую FTS5 (Full-Text Search 5) SQLite с триггерами, которые автоматически синхронизируют поисковый индекс при вставке, обновлении или удалении строки. Никакой ручной переиндексации, никакой отдельной службы поиска. Все просто работает.
CREATE VIRTUAL TABLE rfps_fts USING fts5(
title, description, ai_summary, categories,
content=rfps, content_rowid=id
);Дедупликация использует уникальное ограничение по (source_name, external_id), поэтому, если одно и то же RFP появляется в нескольких запусках скрапинга, оно обновляется, а не дублируется.
Статус RFP (открыт, скоро истекает, закрыт) вычисляется во время запроса на основе поля срока подачи, а не хранится. Это означает, что статус всегда актуален без необходимости фоновой задачи для обновления устаревших записей.
Фронтенд
Пользовательский интерфейс создан с помощью React Router v7 на Cloudflare Workers, стилизован с помощью Tailwind CSS v4. Он включает строку поиска с полнотекстовым поиском по заголовкам, описаниям, резюме и категориям. Выпадающие фильтры позволяют сузить поиск по типу организации, категории, статусу срока подачи и предполагаемой стоимости.
Интерактивная карта использует Leaflet с кластеризацией маркеров. Каждое RFP с геокодированными данными о местоположении отображается как маркер. Сама геокодирование выполняется в скрейпере с использованием Nominatim (службы геокодирования OpenStreetMap) для преобразования строковых местоположений в пары широты/долготы.
Панель статистики вверху показывает количество открытых в данный момент RFP и активных источников, чтобы вы могли с первого взгляда оценить свежесть данных.
Что бы я сделал по-другому
Если бы я начинал заново, я бы, вероятно, добавил оповещения по электронной почте с самого начала. Сейчас вам нужно посещать сайт, чтобы проверить наличие новых возможностей. Простая ежедневная сводка для сохраненных поисков сделала бы его намного полезнее.
Я бы также больше инвестировал в тестирование надежности источников. Государственные веб-сайты меняются без предупреждения, и их скрапинг по своей сути ненадежен. Лучший мониторинг и автоматические оповещения при получении неожиданных данных от источника сэкономили бы время на отладке.
Попробуйте
Платформа доступна по адресу rfp.davidloor.com. Она ориентирована на возможности в области цифровых услуг, веб-разработки, CMS, облачных технологий и ИИ, но архитектура может поддерживать любую категорию RFP.