Skip to main content
Glama

Agentic Job Intelligence Pipeline (MCP + LLM)

Агентный конвейер, который использует Model Context Protocol для оркестрации внешних инструментов с целью структурированного получения данных, с уровнем оценки на основе LLM, который ранжирует неструктурированные описания вакансий относительно профиля кандидата. Давление на окно контекста обрабатывается каскадной стратегией «сначала метаданные» (pipeline.py), а те же инструменты доступны также настоящему агенту с вызовом инструментов, имеющему собственный цикл планирования (agent.py), и REST + WebSocket API (api.py). Полное описание — в docs/.

Запуск

pip install -r requirements.txt

python pipeline.py --benchmark     # token comparison, zero API calls
python pipeline.py --dry-run       # real MCP subprocess handshake, no LLM
export OPENAI_API_KEY=sk-...
python pipeline.py --top 8         # fixed 3-stage pipeline
python agent.py --dry-run          # agent tool discovery, no LLM calls
export OPENAI_API_KEY=sk-...
python agent.py --top 8            # tool-calling agent with a planning loop
python eval.py --prefilter-only    # stage-1 recall, deterministic half, no key needed
pytest -q                          # in-process MCP server, no key needed

uvicorn api:app --reload           # REST + WebSocket layer, http://localhost:8000
curl localhost:8000/health
curl -X POST localhost:8000/rank -H 'content-type: application/json' -d '{"use_llm": false}'

Related MCP server: trackly-cli

Измеренный результат

Корпус из 150 вакансий, шорт-лист из 8. prefilter() применяет фильтры по тегам/заголовку, уровню (отбрасывает senior, если опыт кандидата < 4 лет) и местоположению (предпочтительный город кандидата, нормализованные алиасы или удалёнка), что сокращает число выживших с 150 до 31:

Стратегия

Промпт-токены

против наивного

A — отправить все 150 полных описаний

67,360

—

B — сначала метаданные, затем получить 8

13,802

4.9× дешевле

C — prefilter → метаданные → получить 8

6,258

10.8× дешевле

Воспроизводится командой python pipeline.py --benchmark. Реальные числа из data/jobs.json этого репозитория на сегодняшний день, не заглушки. Подсчёт токенов использует энкодер o200k_base из tiktoken (тот, что на самом деле используют gpt-4o / gpt-4o-mini) — точный, не оценочный. Старая эвристика «символы ÷ 4» завышала стоимость наивной стратегии на 17.4% на этом корпусе; поле heuristic_vs_real_tokens в benchmark() воспроизводит это сравнение.

Архитектура

   MCP SERVER (stdio subprocess)              MCP CLIENT / pipeline.py
   ---------------------------------          ------------------------------------
   tool  list_jobs        -> metadata  <----  Stage 0  prefilter()   [0 tokens]
   tool  get_job_details  -> full text        Stage 1  shortlist     [~5k tokens]
   tool  get_candidate_profile                Stage 2  score         [~4k tokens]
   tool  corpus_stats
   resource  jobs://schema                    Meter tracks tokens per stage
   prompt    rank_jobs

Аргумент в пользу поэтапного извлечения

Наивный подход: отдать модели каждое полное описание и попросить ранжировать. Три проблемы.

  1. Стоимость — 79k промпт-токенов за запуск, и она растёт линейно с ростом корпуса.

  2. Потолок — после нескольких сотен вакансий это просто превышает окно контекста. Не медленно: невозможно.

  3. Качество — полнота при длинном контексте ухудшается в середине большого промпта, поэтому ранжирование становится хуже с добавлением новых кандидатов.

Поэтапное извлечение, сначала самый дешёвый фильтр:

Стадия

Механизм

Стоимость

Почему здесь

0

Детерминированный фильтр по тегам/заголовку/местоположению на Python

бесплатно

Не позволяйте модели читать то, что if может отбросить. 150 → 91.

1

LLM видит ~55 токенов метаданных на каждую вакансию, выбирает топ-8

~5k

Отсев с высокой полнотой. Предписано включать с запасом, потому что стадия 2 может отклонить.

2

Полные описания только для 8 выживших

~4k

Полная точность, оплачивается один раз, только там, где это меняет ответ.

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

Агентный уровень (agent.py)

pipeline.py — это фиксированный скрипт: сначала prefilter, затем всегда shortlist, затем всегда score. agent.py передаёт модели те же MCP-инструменты через OpenAI function calling и позволяет ей планировать собственный путь — настоящий агент с вызовом инструментов, а не жёстко заданная последовательность:

  • Структурированный финальный ответ в виде вызова инструмента. Агент не «отвечает в JSON и надеется» — завершение означает вызов синтетического инструмента submit_rankings, схема параметров которого является schemas.RankingResult. Неверные аргументы возвращаются как ошибка валидации, которую модель может прочитать и исправить, в течение ограниченного числа повторных попыток.

  • Сбои инструментов деградируют, а не роняют систему. Любое исключение MCP-инструмента становится обычным результатом инструмента {"error": ...}, который возвращается модели, чтобы она могла обойти неудачный вызов, а не обрушить весь запуск.

  • Модель, которая так и не сходится, всё равно возвращает что-то. Если она исчерпывает бюджет шагов/повторов без корректного вывода, агент откатывается к той же детерминированной логике prefilter → shortlist → score, что и pipeline.py, и сообщает fallback_used: true.

  • Временные ошибки API получают собственные повторные попытки через tenacity, отдельно от цикла повторов по схеме выше — плохое соединение и плохой ответ это разные виды сбоев.

Пошаговый цикл описан в docs/CODE_WALKTHROUGH.md.

Уровень REST + WebSocket (api.py)

Сервис FastAPI оборачивает MCP-инструменты, pipeline.py и agent.py, чтобы они были доступны по HTTP, а не только как CLI-скрипты:

Endpoint

Что делает

GET /health

проверка работоспособности

GET /jobs, GET /jobs/{id}

список метаданных / полные детали; та же инвариантность без описаний, что и у MCP-инструмента

GET /stats

статистика корпуса

POST /rank

запуск конвейера ранжирования (agentic — цикл планирования по умолчанию, либо фиксированный поэтапный конвейер); use_llm: false выполняет только бесплатную детерминированную половину

WS /ws/rank

то же, что POST /rank, но передаёт по одному событию на каждый шаг агента по мере выполнения, вместо одного ответа в конце

Одна сессия MCP stdio открывается один раз при запуске и разделяется за блокировкой (api.MCPSession), а не порождает подпроцесс на каждый запрос — это осознанное упрощение по сравнению с настоящим пулом соединений, о чём сказано в docstring модуля api.py, и это не выдаётся за реальную распределённую систему. Каждый запрос получает корреляционный идентификатор (request_id), проходящий через логи и все потоковые события, чтобы запуск можно было проследить через асинхронные переходы.

Заметки по MCP, которые стоит знать назубок

  • Зачем это существует: N моделей × M интеграций превращается в N + M. Один протокол, JSON-RPC 2.0 поверх stdio или Streamable HTTP.

  • Инструменты против ресурсов против промптов: управляемые моделью / приложением / пользователем. Правильно разобраться с этим трио — частый способ выделиться на собеседовании.

  • Форма CallToolResult: content (блоки), structured_content (типизированный, оборачивается в {"result": ...} для возврата не-объектов), is_error. См. pipeline.call().

  • Дизайн инструментов — это дизайн API для нечеловеческого вызывающего. list_jobs и get_job_details разделены, потому что именно это разделение обеспечивает поэтапное извлечение. Docstring'и и есть описание инструмента, которое читает модель — расплывчатый docstring, неправильный выбор инструмента.

  • Пакетные параметры вместо скалярных: get_job_details(job_ids: list[str]) стоит одного кругового обращения; get_job_detail(job_id: str) — восьми.

Известные ограничения

  • Полнота шорт-листа (сохраняет ли стадия 1 размеченные как релевантные вакансии, пережившие prefilter?) требует настоящего OPENAI_API_KEY для измерения — python eval.py --top 8 запускает его; здесь не запускалось из-за стоимости.

  • Корпус синтетический. Реальные вакансии грязнее — HTML, дубликаты, устаревшие объявления.

  • Нет кэширования между запусками, поэтому повторные вызовы снова платят за стадию 1.

Полнота стадии 1 — измеренная, а не предполагаемая

В data/relevance_labels.json — 20 ID вакансий, которые человек счёл бы релевантными кандидату, выбранные по документированным воспроизводимым критериям (см. файл). eval.py проверяет две вещи отдельно:

  • prefilter_recall — сколько из 20 размеченных релевантных вакансий переживают детерминированный prefilter? Бесплатно, без API-ключа: python eval.py --prefilter-only → 20/20, recall 1.0. Критерии разметки — строгое подмножество собственных фильтров prefilter, так что это подтверждает, что prefilter не молча отбрасывает целевую роль, а не просто предполагает это.

  • shortlist_recall — сколько из них также переживают шорт-лист LLM при том top, с которым вы реально запускаете? python eval.py --top 8 — требует OPENAI_API_KEY, реального вызова модели, поэтому в этом репозитории не запускается; запустите сами, когда у вас будет ключ.

Ваши задачи

  1. prefilter() — добавьте фильтры по уровню и местоположению. Перезапустите --benchmark, зафиксируйте число. Готово: 150 → 31 выживших, сокращение в 10.8× по сравнению с наивным.

  2. Замените approx_tokens на реальный подсчёт через tiktoken; отметьте, насколько эвристика ÷4 была неточна. Готово: эвристика завысила на 17.4%.

  3. Создайте размеченный набор из 20 вакансий и измерьте полноту стадии 1. Готово для бесплатной половины (prefilter_recall = 1.0); платная половина (shortlist_recall) подключена в eval.py --top 8, запустите со своим ключом.

  4. Подключите сервер в MCP-конфиг Claude Desktop и вызовите его вручную. Пример конфига и инструкции по перезапуску — в docs/OVERVIEW.md — фактическая регистрация происходит в вашем собственном приложении Claude Desktop, а не в этом репозитории.

Документация

  • docs/OVERVIEW.md — что это за проект, какую проблему он решает и почему, архитектура, подключение Claude Desktop, локальная тестируемость, известные ограничения.

  • docs/CODE_WALKTHROUGH.md — каждый модуль, функция за функцией.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    A
    maintenance
    MCP server that exposes job search data from multiple boards, enabling clients to query and manage job listings via natural language.
    7
    MIT
  • A
    license
    Not graded
    quality
    A
    maintenance
    MCP server for job search and application tracking, enabling AI agents to search jobs, get details, manage applications, and find contacts across 128K+ jobs and 1,900+ companies.
    781 npm
    3
    MIT
  • A
    license
    Not graded
    quality
    D
    maintenance
    A custom MCP server that exposes a jobs database to any MCP-compatible LLM client, allowing users to ask in plain English to search, filter, and match job openings.
    MIT
  • A
    license
    Not graded
    quality
    C
    maintenance
    MCP server that aggregates and deduplicates job listings from multiple public sources, ranks them against a user's resume, and exposes tools for searching, viewing details, explaining fit, and tracking applications.
    MIT