实战 6 · ★★★★★ 自动找候选人并发邮件的招聘 agent
目标
做一个 agent:
- 输入职位描述(JD)
- 去 GitHub 搜符合技术栈的开发者
- 解析他们的 Profile README
- 用 Claude 评分匹配度 + 生成个性化招聘邮件草稿
- 草稿存本地,人工审核通过后才发送
关键设计:人工审核回路——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)
练习
- 加 Stack Overflow 数据源(搜标签声望高的用户)
- 用 embedding 粗筛 + LLM 精排,处理更多候选人
- 加 Slack 通知(草稿生成后推送到招聘群)
- 加候选人回复处理(收到邮件回复自动归档)