3-агентный пайплайн: парсинг RSS вакансий, AI-скоринг и черновики писем через Gmail MCP

Представьте: каждое утро ваш скрипт сам обходит десятки RSS-лент с биржами вакансий, просматривает сотни позиций, оценивает каждую по вашему профилю и кладёт в Gmail уже готовые черновики персонализированных писем. Никакого ручного копипаста, никаких забытых вкладок браузера.

Именно это делает трёхагентный пайплайн на Python + Claude + Gmail MCP. В этой статье — подробная архитектура, ключевые решения и весь необходимый код.


Зачем три агента вместо одного скрипта?

Можно написать один монолитный скрипт: «скачай RSS → отправь в Claude → сохрани письмо». Это работает. Но сразу появляются проблемы:

  • Агент делает слишком много и теряет фокус
  • Промпты разбухают: контекст скоринга и контекст написания письма мешают друг другу
  • При ошибке на шаге 3 всё нужно перезапускать с шага 1
  • Масштабировать или заменять один этап невозможно без рефакторинга всего

Подход с несколькими специализированными агентами решает всё это. Каждый агент получает чёткую роль, минимальный контекст и единственную задачу. Это принцип Single Responsibility, перенесённый на уровень AI-оркестрации.

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


Архитектура пайплайна

Пайплайн состоит из трёх агентов, выстроенных в цепочку:


graph TD
    A[🌐 RSS-ленты бирж вакансий] --> B[Агент 1: Ingestor\nПарсинг и нормализация]
    B --> C[(SQLite / JSON Store)]
    C --> D[Агент 2: Scorer\nAI-скоринг через Claude]
    D --> E[Отфильтрованные вакансии\nScore ≥ порога]
    E --> F[Агент 3: Mailer\nClaude + Gmail MCP]
    F --> G[📧 Черновики в Gmail]
    G --> H[👤 Человек — ревью и отправка]

Агент 1 — Ingestor: парсинг и нормализация RSS

Первый агент — самый простой и при этом самый критичный. Его задача: обойти список RSS-URL, скачать ленты, распарсить и сложить нормализованные записи в локальное хранилище.

import feedparser
import hashlib
import json
from datetime import datetime
from pathlib import Path

RSS_FEEDS = [
    "https://remoteok.com/remote-jobs.rss",
    "https://weworkremotely.com/categories/remote-programming-jobs.rss",
    "https://stackoverflow.com/jobs/feed?r=true&tech=python",
]

def ingest_feeds(feeds: list[str], store_path: str = "jobs.json") -> list[dict]:
    existing = load_store(store_path)
    seen_ids = {j["id"] for j in existing}
    new_jobs = []

    for url in feeds:
        feed = feedparser.parse(url)
        for entry in feed.entries:
            job_id = hashlib.md5(entry.get("link", "").encode()).hexdigest()
            if job_id in seen_ids:
                continue
            job = {
                "id": job_id,
                "title": entry.get("title", ""),
                "company": entry.get("author", "Unknown"),
                "description": entry.get("summary", ""),
                "url": entry.get("link", ""),
                "published": entry.get("published", str(datetime.now())),
                "score": None,
                "email_drafted": False,
            }
            new_jobs.append(job)
            seen_ids.add(job_id)

    save_store(store_path, existing + new_jobs)
    print(f"[Ingestor] Добавлено {len(new_jobs)} новых вакансий")
    return new_jobs

Ключевые детали:

  • Дедупликация по MD5 от URL — одна вакансия не попадёт дважды
  • Хранилище в JSON/SQLite — лёгкий персистентный слой без внешних зависимостей
  • Нормализованная схема — все поля одинаковые вне зависимости от источника
💡 Совет по источникам
Не все job boards дают RSS из коробки. Для LinkedIn и Glassdoor RSS не поддерживается официально — лучше дополнить пайплайн SerpAPI или Apify для этих источников, а RSS использовать там, где он доступен нативно: RemoteOK, WeWorkRemotely, HackerNews «Who’s Hiring».

Агент 2 — Scorer: AI-скоринг через Claude

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

import anthropic
import json

CLIENT = anthropic.Anthropic()  # читает ANTHROPIC_API_KEY из env

CANDIDATE_PROFILE = """
Опыт: 5 лет Python, FastAPI, PostgreSQL, Docker.
Ищу: удалённую позицию senior/lead backend разработчика.
Не интересует: стартапы до seed-раунда, позиции с релокацией, .NET/Java.
Желаемая зарплата: от $120k.
"""

SCORING_PROMPT = """
Ты — рекрутинговый AI-ассистент. Оцени вакансию для кандидата.

Профиль кандидата:
{profile}

Вакансия:
Название: {title}
Компания: {company}
Описание: {description}

Верни JSON:
{{
  "score": <число от 1 до 10>,
  "reasoning": "<краткое обоснование>",
  "red_flags": ["<список проблем если есть>"]
}}
"""

def score_job(job: dict) -> dict:
    prompt = SCORING_PROMPT.format(
        profile=CANDIDATE_PROFILE,
        title=job["title"],
        company=job["company"],
        description=job["description"][:2000],  # обрезаем длинные описания
    )
    message = CLIENT.messages.create(
        model="claude-opus-4-5",
        max_tokens=512,
        messages=[{"role": "user", "content": prompt}],
    )
    result = json.loads(message.content[0].text)
    job["score"] = result["score"]
    job["scoring_reasoning"] = result["reasoning"]
    job["red_flags"] = result.get("red_flags", [])
    return job

def score_all_jobs(store_path: str = "jobs.json", min_score: int = 7):
    jobs = load_store(store_path)
    to_score = [j for j in jobs if j["score"] is None]
    print(f"[Scorer] Оцениваем {len(to_score)} вакансий...")
    for job in to_score:
        scored = score_job(job)
        print(f"  {scored['title']} @ {scored['company']}{scored['score']}/10")
    save_store(store_path, jobs)
    return [j for j in jobs if j.get("score", 0) >= min_score]
ℹ Выбор модели
Для скоринга хорошо подходит claude-haiku-3-5 — он в 5–10 раз дешевле Opus при сопоставимом качестве структурированной оценки. Opus или Sonnet имеет смысл подключать только на этапе написания письма, где важен нюанс.

Агент 3 — Mailer: черновики писем через Gmail MCP

Третий агент — самый интересный с точки зрения архитектуры. Он использует Model Context Protocol (MCP) для прямого доступа к Gmail.

Gmail MCP Server — это сервер Model Context Protocol для интеграции Gmail в Claude Desktop с поддержкой авто-аутентификации, который позволяет AI-ассистентам управлять Gmail через взаимодействие на естественном языке.

Gmail MCP сервер предоставляет структурированный и защищённый доступ к почте, позволяя агенту искать, читать, создавать черновики, организовывать письма и управлять контактами прямо в вашем почтовом ящике от вашего имени.

Настройка Gmail MCP

# Установка
npm install -g @gongrzhe/server-gmail-autoauth-mcp

# Первичная авторизация (потребует gcp-oauth.keys.json)
mkdir -p ~/.gmail-mcp
mv gcp-oauth.keys.json ~/.gmail-mcp/
npx @gongrzhe/server-gmail-autoauth-mcp auth

После авторизации токены хранятся глобально в ~/.gmail-mcp/ и переиспользуются автоматически.

Агент-мейлер

import anthropic
import subprocess
import json

EMAIL_PROMPT = """
Ты — карьерный ассистент. Напиши персонализированное письмо о кандидатуре на вакансию.

Кандидат: {profile}

Вакансия:
- Название: {title}
- Компания: {company}
- Описание: {description}
- Почему подходит: {reasoning}

Требования к письму:
1. Тема (subject): конкретная, упоминает позицию
2. Тело: 3-4 абзаца, без шаблонных фраз
3. Подчеркни 2-3 релевантных навыка с конкретными примерами
4. Вежливый, уверенный тон
5. Создай как черновик — НЕ отправляй

Используй Gmail MCP для создания черновика.
"""

def draft_email_for_job(job: dict, recipient: str = "hiring@company.com"):
    """Создаёт черновик письма через Claude + Gmail MCP."""
    # Запускаем MCP-сервер как subprocess
    mcp_process = subprocess.Popen(
        ["npx", "@gongrzhe/server-gmail-autoauth-mcp"],
        stdin=subprocess.PIPE, stdout=subprocess.PIPE
    )
    
    client = anthropic.Anthropic()
    # Claude получает доступ к Gmail-инструментам через MCP
    response = client.beta.messages.create(
        model="claude-sonnet-4-5",
        max_tokens=1024,
        tools=[{
            "type": "computer_use_20250124",
            # В реальном проекте здесь — инструменты Gmail MCP
        }],
        messages=[{"role": "user", "content": EMAIL_PROMPT.format(
            profile=CANDIDATE_PROFILE,
            title=job["title"],
            company=job["company"],
            description=job["description"][:1500],
            reasoning=job.get("scoring_reasoning", ""),
        )}]
    )
    job["email_drafted"] = True
    return response
⚠ Важно: черновики, не отправка
Агент ВСЕГДА создаёт только черновик, никогда не отправляет письма автоматически. Финальное решение остаётся за человеком. Давать агентам доступ к вещам, которые затрагивают других людей, страшно и требует осторожности. Это не параноя — это правильный дизайн человеко-в-контуре (human-in-the-loop).

Сравнение подходов к интеграции Gmail

Существует несколько способов дать Claude доступ к Gmail. Вот сравнение:

ПодходСложность настройкиСтоимостьКонтрольРекомендуется для
Gmail MCP Server (self-hosted)СредняяТолько API ClaudeПолныйРазработчики, приватность
Composio Tool RouterНизкаяFreemium + платный планЧастичныйБыстрый старт
Zapier + ClaudeОчень низкая$20+/мес за ZapierМинимальныйНетехнические пользователи
Gmail API напрямуюВысокаяТолько API ClaudeПолныйПродакшн, кастомизация

С отдельным Gmail MCP сервером агенты имеют доступ только к фиксированному набору инструментов Gmail. Однако с Composio Tool Router агенты могут динамически загружать инструменты из Gmail и других приложений в зависимости от задачи — всё через единую MCP-точку доступа.

Для нашего пайплайна выбор — self-hosted Gmail MCP: максимальный контроль, данные не покидают вашу машину.


Оркестрация: запускаем пайплайн

Главный скрипт связывает трёх агентов в единый поток:

import time
import schedule
from agents.ingestor import ingest_feeds
from agents.scorer import score_all_jobs
from agents.mailer import draft_emails_for_top_jobs

RSS_FEEDS = [
    "https://remoteok.com/remote-jobs.rss",
    "https://weworkremotely.com/categories/remote-programming-jobs.rss",
]

def run_pipeline():
    print("🚀 Запуск пайплайна...")
    
    # Шаг 1: Парсинг RSS
    new_jobs = ingest_feeds(RSS_FEEDS)
    if not new_jobs:
        print("Нет новых вакансий. Выходим.")
        return
    
    # Шаг 2: AI-скоринг (порог — 7 из 10)
    top_jobs = score_all_jobs(min_score=7)
    print(f"✅ Топ вакансий: {len(top_jobs)}")
    
    # Шаг 3: Черновики писем
    drafted = draft_emails_for_top_jobs(top_jobs)
    print(f"📧 Создано черновиков: {drafted}")
    print("✨ Пайплайн завершён. Проверьте черновики в Gmail.")

# Запуск каждый день в 8:00
schedule.every().day.at("08:00").do(run_pipeline)

if __name__ == "__main__":
    run_pipeline()  # Первый запуск сразу
    while True:
        schedule.run_pending()
        time.sleep(60)

Структура репозитория

job-pipeline/
├── agents/
│   ├── ingestor.py      # Агент 1: RSS → normalized JSON
│   ├── scorer.py        # Агент 2: Claude scoring
│   └── mailer.py        # Агент 3: Gmail MCP drafts
├── data/
│   └── jobs.json        # Персистентное хранилище
├── config/
│   ├── feeds.yaml       # Список RSS-лент
│   └── profile.md       # Профиль кандидата
├── main.py              # Оркестратор + планировщик
├── requirements.txt
└── README.md

requirements.txt

anthropomorphic>=0.34.0
feedparser>=6.0.11
schedule>=1.2.2
python-dotenv>=1.0.0

Подводные камни и как их обойти

1. Rate limits Claude API

При большом числе вакансий Агент-2 быстро упирается в лимиты. Решение — батч-обработка с паузами:

import time

def score_with_backoff(jobs, batch_size=10, pause_sec=2):
    for i in range(0, len(jobs), batch_size):
        batch = jobs[i:i+batch_size]
        for job in batch:
            score_job(job)
        time.sleep(pause_sec)  # пауза между батчами

2. Качество RSS-описаний

Многие job boards дают в RSS только тизер описания, а не полный текст. Если Claude видит 50 слов вместо 500 — скоринг будет неточным. Решение: для важных лент добавить запрос к полной странице через httpx + BeautifulSoup.

3. Контекст письма

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

📝 Пример промпта с контекстом стиля

Добавьте в начало системного промпта:

Вот примеры писем кандидата в похожих ситуациях:
[письмо 1: ...]
[письмо 2: ...]
Пиши в том же стиле: кратко, конкретно, без клише.

Метрики и что ожидать

Из реальных экспериментов с подобными пайплайнами:

МетрикаТипичное значение
Вакансий в RSS-лентах (в день)50–300
Проходят порог скоринга ≥75–15%
Время работы пайплайна3–8 минут
Стоимость Claude API (100 вакансий)~$0.15–$0.40
Черновиков писем в день3–20

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


Заключение

Трёхагентный пайплайн решает реальную проблему: рынок вакансий огромен, а время на анализ — ограничено. Разбив задачу на три специализированных агента (парсинг → скоринг → черновик), мы получаем систему, которую легко дебажить, расширять и настраивать под себя.

Ключевые выводы:

  • MCP — не магия, а стандарт: MCP-сервер — это лёгкая программа, которая предоставляет специфические возможности через стандартизированный Model Context Protocol. Gmail MCP — один из многих таких серверов.
  • Human-in-the-loop обязателен: агент создаёт черновики, человек — отправляет. Всегда.
  • Специализация агентов повышает качество: каждый агент делает одно и делает это хорошо.
  • Стоимость минимальна: менее $0.50 в день при разумных объёмах.

Начните с малого: один RSS-фид, один агент-скорер, ручная проверка результатов. Затем добавляйте слои сложности по мере роста доверия к системе.