Skip to content

实战 6 · ★★★★★ 自动找候选人并发邮件的招聘 agent

目标

做一个 agent:

  1. 输入职位描述(JD)
  2. 去 GitHub 搜符合技术栈的开发者
  3. 解析他们的 Profile README
  4. 用 Claude 评分匹配度 + 生成个性化招聘邮件草稿
  5. 草稿存本地,人工审核通过后才发送

关键设计:人工审核回路——agent 不直接发邮件,所有邮件必须人工确认。这是第 8 章讲的"越权执行"防御。

架构

职位描述

GitHub Search API → 筛选 followers/repos

Top 20 候选人 → 获取 Profile README

正则 + Claude 解析 → 结构化 JSON

Claude 匹配度评分 → Top 5

Claude 生成邮件草稿 → drafts/

人工审核 → approved/ → Resend 发送

记录日志 + 退订列表

准备

bash
pip install PyGithub anthropic resend python-dotenv

.env

GITHUB_TOKEN=ghp_...
ANTHROPIC_API_KEY=sk-ant-...
RESEND_API_KEY=re_...
EMAIL_FROM=recruiter@yourdomain.com

代码

python
import os
import re
import json
import anthropic
import resend
from github import Github
from pathlib import Path
from dotenv import load_dotenv

load_dotenv()
client = anthropic.Anthropic()
gh = Github(os.getenv("GITHUB_TOKEN"))

DRAFTS_DIR = Path("drafts")
APPROVED_DIR = Path("approved")
SENT_LOG = Path("sent_log.json")
DRAFTS_DIR.mkdir(exist_ok=True)
APPROVED_DIR.mkdir(exist_ok=True)

# 已发送候选人(防重复 + 退订)
sent_list = json.loads(SENT_LOG.read_text()) if SENT_LOG.exists() else []

def search_candidates(jd_skills, location="china", min_followers=50, limit=20):
    """GitHub 搜候选人。"""
    query = f"language:{jd_skills[0]} location:{location} followers:>{min_followers} sort:followers-desc"
    users = gh.search_users(query)[:limit]
    return [u for u in users if u.login not in sent_list]

def get_readme(username):
    """获取用户 Profile README。"""
    try:
        repo = gh.get_repo(f"{username}/{username}")
        return repo.get_readme().decoded_content.decode("utf-8")[:5000]
    except Exception:
        return ""

def parse_resume(readme, username):
    """正则 + Claude 解析 README,输出结构化 JSON。"""
    emails = re.findall(r"[\w.+-]+@[\w-]+\.[\w.-]+", readme)
    
    if not readme:
        return {"username": username, "emails": emails, "tech_stack": [], "years": None}
    
    resp = client.messages.create(
        model="claude-sonnet-4-5-20250929",
        max_tokens=512,
        messages=[{"role": "user", "content": f"""从 GitHub README 提取结构化信息,输出 JSON:
{{"tech_stack": ["python", "react"], "years_exp": 5, "role": "后端工程师", "highlights": ["..."]}}

README:
{readme}
"""}]
    )
    try:
        info = json.loads(resp.content[0].text)
    except Exception:
        info = {}
    info["username"] = username
    info["emails"] = emails
    return info

def score_match(info, jd):
    """Claude 评分匹配度。"""
    resp = client.messages.create(
        model="claude-sonnet-4-5-20250929",
        max_tokens=256,
        messages=[{"role": "user", "content": f"""评估候选人与职位的匹配度,输出 JSON:
{{"score": 85, "reason": "技术栈高度匹配,5 年 Python 经验"}}

候选人:{json.dumps(info, ensure_ascii=False)}
职位:{jd}
"""}]
    )
    try:
        return json.loads(resp.content[0].text)
    except Exception:
        return {"score": 0, "reason": "解析失败"}

def generate_email(info, jd, score):
    """生成招聘邮件草稿。"""
    resp = client.messages.create(
        model="claude-sonnet-4-5-20250929",
        max_tokens=512,
        messages=[{"role": "user", "content": f"""写一封个性化招聘邮件。要求:
- 称呼候选人名字({info['username']}
- 提到他的具体技术栈或项目({info.get('tech_stack', [])}
- 简要介绍职位({jd}
- 不超过 200 字
- 包含退订链接占位符 [UNSUBSCRIBE]
- 真实发件人地址:{os.getenv('EMAIL_FROM')}

候选人信息:{json.dumps(info, ensure_ascii=False)}
匹配度:{score['score']}/100
"""}]
    )
    return resp.content[0].text

def recruit(jd):
    """主流程。"""
    skills = ["python", "react", "typescript"]  # 从 JD 提取,这里简化
    
    # 1. 搜候选人
    candidates = search_candidates(skills)
    print(f"找到 {len(candidates)} 个候选人")
    
    # 2. 解析 + 评分
    scored = []
    for u in candidates:
        readme = get_readme(u.login)
        info = parse_resume(readme, u.login)
        if not info["emails"]:
            continue  # 没公开邮箱,跳过
        match = score_match(info, jd)
        scored.append({"info": info, "match": match, "url": u.html_url})
    
    # 3. 取 Top 5
    scored.sort(key=lambda x: x["match"]["score"], reverse=True)
    top5 = scored[:5]
    
    # 4. 生成草稿
    for item in top5:
        email_body = generate_email(item["info"], jd, item["match"])
        draft_path = DRAFTS_DIR / f"{item['info']['username']}.md"
        draft_path.write_text(
            f"# To: {item['info']['emails'][0]}\n"
            f"# Score: {item['match']['score']}\n"
            f"# Reason: {item['match']['reason']}\n"
            f"# GitHub: {item['url']}\n\n"
            f"{email_body}\n",
            encoding="utf-8"
        )
        print(f"草稿已生成: {draft_path}")
    
    print(f"\n生成 {len(top5)} 份草稿,在 drafts/ 目录下。")
    print("人工审核后移到 approved/ 目录,运行 send_approved.py 发送。")

if __name__ == "__main__":
    jd = "招聘 Python 后端工程师,3 年以上经验,熟悉 FastAPI / PostgreSQL,有 LLM 应用经验加分"
    recruit(jd)

发送脚本(send_approved.py

python
import os
import json
import resend
from pathlib import Path
from dotenv import load_dotenv

load_dotenv()
resend.api_key = os.getenv("RESEND_API_KEY")
SENT_LOG = Path("sent_log.json")
sent_list = json.loads(SENT_LOG.read_text()) if SENT_LOG.exists() else []

for draft in Path("approved").glob("*.md"):
    content = draft.read_text(encoding="utf-8")
    lines = content.split("\n")
    to_email = lines[0].replace("# To: ", "").strip()
    body = "\n".join(lines[5:])  # 跳过 header
    
    # 检查退订列表
    if to_email in sent_list:
        print(f"跳过(已发送过): {to_email}")
        continue
    
    # 发送
    resend.Emails.send({
        "from": os.getenv("EMAIL_FROM"),
        "to": to_email,
        "subject": "招聘机会 - Python 后端工程师",
        "text": body
    })
    
    sent_list.append(to_email)
    draft.unlink()  # 删草稿
    print(f"已发送: {to_email}")

SENT_LOG.write_text(json.dumps(sent_list, ensure_ascii=False, indent=2))

合规要点

CAN-SPAM(美国)

  • 必须含退订链接
  • 真实发件人地址
  • 主题不欺骗

GDPR(欧盟)

  • 需"合法利益"依据
  • 可被要求删除
  • 不随意跨境传输

最佳实践

  • 只联系公开 README 留邮箱的人(隐含 opt-in)
  • 每封必含退订链接
  • 30 天内同一人最多 1 封
  • 记录数据来源与发送日志(sent_log.json

练习

  1. 加 Stack Overflow 数据源(搜标签声望高的用户)
  2. 用 embedding 粗筛 + LLM 精排,处理更多候选人
  3. 加 Slack 通知(草稿生成后推送到招聘群)
  4. 加候选人回复处理(收到邮件回复自动归档)

基于 CC BY-SA 4.0 发布