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

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

Именно это стало отправной точкой для идеи простого Chrome-расширения, которое добавляет слой аналитики поверх списка проектов Upwork и позволяет быстрее принимать решение, стоит ли откликаться на вакансию.

Идея: быстрые инсайты перед вдумчивым чтением вакансий

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

Requirement maturity отражает качество постановки задачи: как хорошо заказчик осознаёт чего хочет, насколько описание непротиворечиво, и можно ли из него получить чёткое понимание того, что нужно получить в результате.

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

Для fixed-price задач добавляются ещё две метрики:

Execution feasibility — насколько задача вообще реализуема в заданных условиях: насколько легко её можно решить, и не скрывает ли описание какие-либо принципиальные проблемы.

Scope risk — вероятность того, что требования будут расширяться или меняться в процессе работы. Обычно это проявляется в размытых формулировках и постоянных уточнениях со стороны заказчика.

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

Архитектура системы

Система построена на достаточно прямолинейном стеке: бэкенд на Rust, PostgreSQL как основное хранилище и Chrome-расширение на TypeScript. В качестве LLM-сервера используется Ollama с локальными моделями, работающими на CPU. Доступ к ним осуществляется через обычные HTTP-запросы.

После открытия страницы описания проекта данные извлекаются из DOM и отправляются на бэкенд, где запускаются две асинхронных задачи.

Первая использует LLM для вычисления всех метрик кроме uniqueness и возвращает результат в виде JSON (в данный момент использую модель qwen3.5:4b).

Вторая задача отвечает за вычисление uniqueness с помощью сравнения embeddings. Для каждой вакансии строится embedding (для этого использую модель bge-m3), после чего считается среднее косинусное расстояние до нескольких ближайших соседей. Это расстояние интерпретируется как мера уникальности вакансии в текущем распределении рынка.

Пересчёт границ нормализации

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

Chrome-расширение и система извлечения данных

Для извлечения данных со страниц деталей проекта был реализован отдельный движок, основанный на TypeScript типах и генерации JSON Schema через ts-json-schema-generator. Поверх этой схемы через кастомные атрибуты добавляется слой метаданных, в первую очередь CSS-селекторы, которые описывают, как именно извлекать данные из DOM.

Описание TypeScript-типов, обогащённое аттрибутами, переводится в JSON-схему, которая затем используется и как инструкция для извлечения данных и как стандартный механизм валидации результата.

Проблема изменчивой вёрстки и динамические схемы

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

Поэтому я решил отделить схемы от релиза расширения: они генерируются на этапе сборки, публикуются на сервере и динамически подгружаются в local storage. При изменении схемы достаточно обновить JSON на сервере, и все клиенты в течение короткого времени подтягивают новую версию без необходимости обновлять расширение.

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

Поверх интерфейса Upwork расширение добавляет дополнительную панель, которая показывает собранные метрики. Также отображаются bids — это информация о диапазоне ставок (почасовых или фиксированных), которая в бесплатной версии платформы недоступна напрямую (только для Upwork Plus).

Для того чтобы построить метрики, вакансия должна быть хотя бы раз кем-то открыта в детализированном виде (расширение само никогда не инициирует запросы к Upwork, работая только в пассивном режиме). Данные извлекаются и отправляются на бэкенд, после чего их подхватывают задачи для генерации метрик. После того, как данные проекта обработаны, следующие пользователи смогут увидеть их метрики, нажав Ctrl и наведя курсор на вакансию.

Кодирование с помощью ИИ

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

Основным инструментом стал Cursor Pro с моделью Composer 2.5. Более мощные модели вроде Opus 4.8 High также тестировались, но из-за лимитов Pro-плана основной рабочей моделью осталась Composer.

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

Но не всё так радужно, как хотелось бы. Как можно было легко предположить, проблемой оказалась не генерация кода, а поддержание должного уровня качества во время разрастания кодовой базы. Без code review и тщательного надзора архитектура проекта довольно быстро начинает деградировать: решения наслаиваются друг на друга, появляются несогласованность и дублирования, далее модель начинает опираться на свои прошлые ошибки, и кодовая база постепенно замусоривается. В итоге, как и ожидалось, AI не может заменить разработчика, но скорее смещает его роль в сторону Principal Engineer / Architect, который разбивает задачи, выбирает структуры данных и алгоритмы, а также проверяет полученные решения.

Пример архитектурного дрейфа

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

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

Корень проблемы оказался в ограничениях окружения. В процессе валидации использовался пакет ajv с механизмом генерации JavaScript-кода. В контексте Upwork это оказалось неприемлемо из-за ограничений и политики безопасности, поэтому код валидации генерировался и встраивался во время сборки, чтобы избежать рантайм-генерации.

После анализа этой ситуации ajv был заменён на интерпретируемый cfworker/json-schema, который работает медленнее, но не использует генерацию кода, сохраняя возможность динамического обновления.

Ссылка для ознакомления

Расширение называется GigScout и доступно в Chrome Store.