内容
# 工作岗位侦察员 🔭 — 个人求职智能助理
**Kaggle "AI Agents: Intensive Vibe Coding Capstone" — 智能助理赛道.**
## 问题
求职大多是机械的:在十几个求职网站之间切换标签页,重新阅读每个帖子的相同模板,并重写一封求职信,内容基本相同,只是顺序不同。真正需要判断的部分 — 这个职位是否适合这个人,具体取决于他们的技能、偏好和底线 — 在花费的时间中占比较小。一种诱人的解决方案是实现端到端的自动化,包括提交。工作岗位侦察员(JobScout)自动执行了除最后一步以外的所有操作。
## 解决方案
JobScout 会询问你关于求职的需求,通过**一个自定义的 MCP 服务器**搜索多个求职网站,根据你的(经过 PII 屏蔽的)简历为每个职位打分,并为强匹配的职位草拟定制的申请材料,然后**在人工审批关口停止**。它永远不会自动提交申请。这是一个功能:大多数求职平台禁止自动提交,自动批量发送的通用申请会损害候选人的利益,而人工审批关口是 JobScout 安全设计的核心。
> 构建过程是一个故事:首先编写了规格说明书(`specs/`),然后由 AI 编码代理根据它实现。结构可以扩展,但氛围很重要。
---
## 架构
```
┌──────────────────────────────────────────────┐
│ 协调器(src/orchestrator.py) │
│ 智能代理循环:接收 → 搜索 → 过滤 → │
│ 评分 → 草拟 → 人工审批关口 → 记忆 │
└──────┬──────────────────────┬────────────────┘
Claude API│ │ MCP(stdio)
┌───────────────▼───────┐ ┌──────────▼──────────────────┐
│ 专业子代理 │ │ 工作岗位搜索 MCP 服务器 │
│ · 搜索(无 LLM) │ │ search_jobs │
│ · 评分 + 技能 │ │ get_job_details │
│ · 草拟 + 技能 │ │ list_sources │
└───────────────────────┘ └──────────┬──────────────────┘
│ 并发扇出,GET 请求
┌──────────┬──────────┼──────────┬───────────┬─────────┬─────────┐
RemoteOK The Muse Remotive Arbeitnow Greenhouse Lever Ashby
(+ JSearch, Adzuna, USAJOBS 免费密钥 · LinkedIn 可选,见 ⚠️)
安全性约束贯穿整个系统(src/guardrails.py):
PII 屏蔽 ⇄ 解屏蔽 · 审计日志(logs/audit.jsonl)· 人工审批关口硬停止
内存(.jobscout_memory.json)跨会话去重。
```
MCP 服务器规范化每个求职网站为一个 `Job` 模式,并通过 URL 和(标题,公司)去重 — 智能代理与**一个**工具表面集成,无论其后面有多少个求职网站(NxM 集成问题)。
## 课程概念演示
| 概念 | 位置 |
|---|---|
| **多智能体系统** | 协调器 + 搜索/评分/草拟/审查/洞察子代理(`src/agents/`) |
| **自定义 MCP 服务器** | `mcp-server/job_search_server.py`,stdio 传输,官方 Python SDK |
| **安全功能** | 人工审批关口,PII 屏蔽,审计日志,最小权限,确定性过滤器,提示注入防御(`src/guardrails.py`) |
| **智能体技能** | 五个 SKILL.md 技能,具有渐进式披露(`skills/`) |
| **内存** | 跨会话已见/已批阅跟踪(`src/memory.py`) |
| **评估 + 测试** | pytest 用于确定性部分,LLM 作为评判员用于评分智能体(`tests/`,`evals/`) |
| **可部署性** | `Dockerfile` + 生产路径 |
## 安装
需要 Python 3.11+ 和 Anthropic API 密钥。
```bash
git clone <此仓库> && cd <此仓库>
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env # 然后将 ANTHROPIC_API_KEY 放入 .env
```
可选:可以将简历放在 `profile/resume.pdf`。为了更丰富的评分,还可以添加 LinkedIn 导出文件、过去的求职信或推荐信到 `profile/documents/`(参见 `profile/documents/README.md`)— 所有这些都与简历合并,经过相同的屏蔽后进行分析。在 **👤 个人资料** 页面上,有一个 "🔍 预览 PDF 文本提取" 面板,您可以检查实际从 PDF 中提取了什么文本,以便在 LLM 之前进行捕获。
**成本提示:** 智能体默认使用 `claude-opus-4-8`。对于开发、演示运行和评估迭代,设置 `JOBSCOUT_MODEL=claude-haiku-4-5` 在 `.env` 中 — 一个完整的运行通常花费几美分而不是几十美分。结构化输出在 Haiku 上工作相同;代码自动跳过 `thinking` 参数,因为 Haiku 不支持扩展思考。
## 使用
### 网页 UI(推荐)
```bash
streamlit run app.py
```
三个页面,与 CLI 下的相同流水线(相同的 MCP 服务器、安全约束和智能体):
- **👤 个人资料** — 完整的接收表单:联系信息,简历 PDF 上传 + 补充文档,一个 "🔍 预览 PDF 文本提取" 面板,用于验证实际从 PDF 中提取了什么文本,然后才交给 LLM,求职信语气(`communication_style`:直接 / 协作 / 热情),目标职位,偏好,底线,一个 **仅显示过去 N 天的职位** 的过滤器,每个维度评分权重,草拟阈值,和职位来源(包括 Lever/Ashby 公司令牌和 LinkedIn 服务条款确认)。保存到 `profile/profile.yaml`(gitignored)。
- **🚀 运行 JobScout** — 一键运行搜索 → 确定性过滤 → 简历分析 → 按维度评分,带有实时进度,可选 **"提前停止,一旦找到 N 个"** 目标,以便它继续评分更多职位(直到您的成本上限),而不是固定批次。每个匹配项都会展开为一个维度评分明细,一个已经经过二次审阅的求职信草稿 + 简历调整,一个针对职位发布的 **关键词覆盖检查** (无 LLM 调用 — 发布者自己的关键词中实际上有哪些进入了求职信),以及 **批准 / 拒绝 / 跳过** 按钮 — 作为 UI 的人工审批关口。
- **📚 历史记录** — 曾经评分过的每个职位,带有评分,决定,您做出决定的日期,原始 **发布者** 用于聚合来源(例如 `jsearch (Glassdoor)`),以及保存的求职信(`.jobscout_records.json`,gitignored),以及 JSON 导出。每个条目都会展开为其完整的维度评分明细和草稿,因此摘要表的评分始终有一个可钻取的详细视图 — 按决策(包括 `未决定`)过滤以查看未看护的 `--auto` 运行留下的内容。
### CLI
```bash
# 首次运行会采访您并写入 profile/profile.yaml(gitignored):
python -m src.orchestrator
# 仅搜索 + 确定性过滤 — 无 LLM 调用,无密钥:
python -m src.orchestrator --dry-run
# 限制每次运行的 LLM 评分调用次数(默认为 6):
python -m src.orchestrator --max-score 10
# 保持评分(直到 --max-score),直到 5 个职位达到 draft_threshold,
# 而不是在固定批次后停止:
python -m src.orchestrator --min-matches 5 --max-score 40
# 无人工交互(例如,每天运行一次 cron 任务):无交互提示。
# 草拟的软件包将保存到 .jobscout_records.json 中,无需决策 —
# 稍后在 Streamlit UI 中查看和批准/拒绝/跳过,与其他运行相同。
# 永远不会自动批准;这只会延迟人工关口,不会删除它。
python -m src.orchestrator --auto --min-matches 5 --max-score 40
```
完整的运行:并发搜索所有启用的求职网站,通过 MCP → 删除已见过的职位、错误的工作类型、底线、工资下限缺失,以及(如果设置 `preferences.max_posting_age_days`)比该天数更老的职位 **在代码中,在任何 LLM 调用之前** → 分析您屏蔽的简历 → 按维度评分每个职位,并带有理由 → 为达到阈值的职位草拟求职信 + 简历调整 → 第二个具有新鲜上下文的审阅智能体批评草稿(不支持的声明、遗漏的关键词、通用措辞、语气)并返回修订后的信件 → 在审批关口呈现每个软件包,包括审阅者的笔记。
**仅最近发布的职位过滤器。** 设置 `preferences.max_posting_age_days`(个人资料页面或直接在 `profile.yaml` 中)以删除任何旧于 N 天的职位 — 确定性,在任何 LLM 调用之前,与底线/工作类型/工资过滤器相同类别。如果职位没有指定日期,则也会丢弃 — 故意:未声明的日期不是一个可靠的 "足够新" 的指标。
**每天运行。** JobScout 没有内置调度器 — `--auto` 使一次运行可以安全调度,但调度本身的决定是关于您的机器的决定,而不是关于存储库的,因此它是可选的,通过您的 OS 自己的工具。无论哪种方式,请随时打开 Streamlit **历史记录** 页面以查看它找到了什么(按 `未决定` 过滤) — 运行本身永远不会批准或提交任何内容。
#### macOS:`launchd`(推荐 cron)
在 `~/Library/LaunchAgents/com.jobscout.dailyrun.plist` 中创建:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.jobscout.dailyrun</string>
<key>ProgramArguments</key>
<array>
<string>/path/to/jobscout/.venv/bin/python</string>
<string>-m</string>
<string>src.orchestrator</string>
<string>--auto</string>
<string>--min-matches</string>
<string>5</string>
<string>--max-score</string>
<string>40</string>
</array>
<key>WorkingDirectory</key>
<string>/path/to/jobscout</string>
<key>StartCalendarInterval</key>
<dict>
<key>Hour</key>
<integer>8</integer>
<key>Minute</key>
<integer>0</integer>
</dict>
<key>StandardOutPath</key>
<string>/path/to/jobscout/logs/launchd_stdout.log</string>
<key>StandardErrorPath</key>
<string>/path/to/jobscout/logs/launchd_stderr.log</string>
<key>RunAtLoad</key>
<false/>
</dict>
</plist>
```
用您的存储库的绝对路径替换 `/path/to/jobscout`(必须完全匹配 — launchd 不会扩展 `~` 或相对路径)。常见更改:
| 想要... | 编辑 |
|---|---|
| 在不同时间运行 | `StartCalendarInterval` → `Hour` (0-23)/ `Minute` |
| 一天运行多次 | 将 `StartCalendarInterval` 更改为 **数组** `{Hour, Minute}` 字典 |
| 在达到目标之前查找更多/更少的匹配项 | `--min-matches` 值 |
| 如果目标很少达到,提高成本上限 | `--max-score` 值 |
| 改回固定批次而不是目标导向 | 删除 `--min-matches`/`5` 对 |
然后加载它(执行一次,并每次编辑后):
```bash
launchctl unload ~/Library/LaunchAgents/com.jobscout.dailyrun.plist 2>/dev/null # 如果第一次运行此命令时出错,则可以忽略
launchctl load ~/Library/LaunchAgents/com.jobscout.dailyrun.plist
```
其他值得了解的命令:
```bash
# 暂停它(保持已安装,只是不会触发):
launchctl unload ~/Library/LaunchAgents/com.jobscout.dailyrun.plist
# 恢复它:
launchctl load ~/Library/LaunchAgents/com.jobscout.dailyrun.plist
# 检查它是否已注册(纯 - 在 PID 列中 = 已加载,但当前未运行):
launchctl list | grep jobscout
# 立即触发它,而无需等待预定时间 —
# 真正测试更改的方式,因为它会执行实际的 plist:
launchctl start com.jobscout.dailyrun
# 完全删除它:
launchctl unload ~/Library/LaunchAgents/com.jobscout.dailyrun.plist
rm ~/Library/LaunchAgents/com.jobscout.dailyrun.plist
# 监视它:
tail -f /path/to/jobscout/logs/launchd_stdout.log
```
**睡眠/唤醒:** 现代 macOS 通常会在 Mac 从睡眠中唤醒后立即运行错过的 `launchd` 作业 —
但完全关闭的 Mac 只会跳过那一天;没有任何东西会追溯性地补上。
#### Linux / 其他地方:cron
```cron
# crontab -e — 每天早上 8 点,从存储库根目录
0 8 * * * cd /path/to/jobscout && .venv/bin/python -m src.orchestrator --auto --min-matches 5 --max-score 40 >> logs/cron.log 2>&1
```
与标志相同,行为相同 — cron 只不会像 `launchd` 那样尝试补上错过的运行。
### 测试和评估
```bash
pytest # 确定性部分:解析、屏蔽、去重、内存
python -m evals.judge # 评分智能体评估:黄金集 + LLM 作为裁判
```
评估黄金集包含一个 **提示注入案例** — 一个工作描述命令模型将其评为 100/100。评估仅在注入失败时通过。
### 直接检查 MCP 服务器
```bash
npx @modelcontextprotocol/inspector .venv/bin/python mcp-server/job_search_server.py
```
## 安全设计
1. **人工审批关口** — 智能代理 * 没有代码路径 * 提交申请。
这缓解了 Confused Deputy 问题:JobScout 运用您的权限搜索和草拟,从不运用您的权限申请。
2. **PII 屏蔽(上下文卫生)** — 您的姓名/电子邮件/电话/地址在任何 LLM 调用之前变为 `{{CANDIDATE_NAME}` 样式的占位符;实际值仅在最终本地渲染时重新注入,从不通过模型。
3. **最小权限** — MCP 服务器是只读的(仅 GET 请求 HTTP),Docker 镜像以非 root 用户运行。
4. **审计日志** — 每个工具和 LLM 调用都追加到 `logs/audit.jsonl` 并带有时间戳、执行者、工具和输入。
5. **没有硬编码的凭证** — 仅限 env vars;`.env` 是 gitignored 且 `.env.example` 记录变量,无值。
6. **确定性底线/阈值逻辑** — 纯 Python,因此恶意的工作发布者无法提示注入以绕过您的过滤器。
7. **不受信任的输入处理** — 工作描述被包装在 `<job_posting>` 标签中,智能代理被指示忽略其中嵌入的指令。
8. **重复反造作约束** - "什么都不发明,即使为了加强案例" 和 "承认真正的差距而不是隐藏它" 在所有五个技能文件中单独重述,不集中在一个地方。每个技能调用都是单独的 LLM 调用,仅限于其自己的系统提示,因此没有共享上下文,一个技能的纪律不会 "延续" 到另一个。
### ⚠️ LinkedIn 免责声明 — 请在启用前阅读
LinkedIn **没有官方的职位 API**,其 [用户协议](https://www.linkedin.com/legal/user-agreement) 禁止自动访问。 可选的 LinkedIn 来源存在是因为许多职位发布 — 尤其是实习生职位 — 首先出现在那里,但 **启用它是一种自觉、自担风险的决定,JobScout 不会为您做出这种决定**:它保持关闭,直到您在启用的板块中添加 `linkedin` **并且** 勾选明确的确认框(UI)/ 在向导中回答是(CLI),这将设置 `sources.linkedin_tos_acknowledged: true`。
JobScout 为降低风险所做的工作:
- **从不触及您的 LinkedIn 账户。** 没有登录,没有 cookie,没有凭证 — 只有公共访客端点一个已注销的浏览器可以看到的内容,使用 JobScout 自己诚实的 User-Agent。最坏的情况是暂时的 IP 速率限制,而不是账户封禁。
- **最小请求量。** 每次运行一个搜索请求(仅第一页),以及最多 8 个职位的完整描述获取,至少间隔一秒。
- **立即停止。** 任何速率限制响应(HTTP 429/999)都会停止该运行的剩余 LinkedIn 请求。
- **只读**,像其他适配器一样 — 仅限 GET 请求。
这些都不会改变它本质:自动访问,LinkedIn 条款禁止。如果这种权衡对您来说是不可接受的,请关闭它 — 其他所有来源都不会受到影响。
## 部署 / 生产路径
本地容器(当前可用):
```bash
docker build -t jobscout .
docker run -it --env-file .env -v $(pwd)/profile:/app/profile -v $(pwd)/logs:/app/logs jobscout
```
生产路径:
1. **定时运行** — 容器上的每日 cron(Cloud Run Jobs / ECS Scheduled Tasks);结果进入队列而不是终端。
2. **人工干预** — 替换 CLI 关卡为电子邮件/Slack 批准消息;关卡合约(`hitl_gate()`)已经隔离。
3. **状态** — 将 `.jobscout_memory.json` 和审计日志移动到小型数据库;两者都在单文件模块后面。
4. **密钥** — 环境变量已经;用密钥管理器替换 `.env`。
5. **多用户** — 用户配置文件、用户内存、用户评分预算的速率限制(`--max-score` 已经是成本旋钮)。
## 诚实的局限性
- **无自动提交,按设计** — JobScout 准备;您申请。
- 适配器关键词匹配基于子字符串;特定措辞可能会错过。
- B 类适配器(Adzuna、USAJOBS)已实现,但比无密钥 A 类板块锻炼得少。
- 评分成本代币:默认上限为 6 个职位/运行(`--max-score` 可更改)。
- 大多数板块上的工资数据都很稀疏;工资下限过滤器仅在职位发布时才会触发,发布的工资上限低于您的下限。
- **就业类型检测是推断,而不是验证。** Lever 和 Ashby 暴露了一个真正的结构化字段,因此这些是准确的。其他地方 — 包括 LinkedIn,`f_JT` 搜索过滤器 **不** 由 LinkedIn 自己严格执行 — JobScout 从标题和描述文本推断。这是专门针对实习生可靠的(真正的实习生几乎总是自我标记),这就是为什么未标记(“未知”)的职位在实习生是您唯一选择的职位类型时被完全排除 — 但它仍然是文本推断,不是来源的真实情况。
- **`--auto` 模式下的失败草稿尚无重试路径。** 如果起草调用出错(偶尔的结构化输出问题),该职位仍被标记为“已查看” — 它不会在未来的搜索中重新出现,历史记录中也没有“现在起草”按钮用于仅限评分的条目。很少见,但如果您遇到它,您需要以其他方式手动起草那个职位。
## 仓库地图
```
CLAUDE.md 代理操作规则(规范优先,安全)
specs/ 真相来源:设计 + BDD 场景
skills/ 5 个代理技能(渐进式披露)
mcp-server/ MCP 服务器 + 规范化模式 + 11 个适配器
src/ 协调器、子代理、防护栏、内存、吸收、管道(UI 助手)、记录(历史)
app.py Streamlit UI(配置文件 / 运行 / 历史)
profile/profile.example.yaml 已提交模板(真实配置文件被 gitignored)
profile/documents/ 可选的补充文档(gitignored README 除外)
evals/ 黄金集 + LLM-as-judge 套件
tests/ pytest 用于确定性部分
Dockerfile 可部署性
```
MCP 配置
以下是此 MCP Server 的配置信息,可直接复制到 Cursor 或其他 MCP 客户端中使用。
mcp.json
连接信息
猜你喜欢
暂无推荐服务器