基于OpenAI Responses API的多智能体求职自动化系统架构与实现
1. 项目概述:从手动海投到智能猎手
如果你也经历过求职季,一定对那种重复劳动深有体会:每天花几小时在LinkedIn、Indeed上刷职位,看到合适的就点进去,然后开始复制粘贴简历信息,绞尽脑汁根据每家公司的要求微调简历和求职信,最后点击提交——整个过程机械、耗时,而且随着投递数量增加,质量难免下降。更不用说,很多公司的申请系统(ATS)还有自己的筛选逻辑,简历格式不对可能第一轮就被机器刷掉了。
这就是我开发HunterAgent的初衷。这本质上是一个由多个AI智能体协同工作的自动化求职系统,它能把上面提到的所有繁琐步骤——从职位发现、简历定制、求职信撰写到最终提交——全部自动化。你只需要设定好目标岗位、技能和经验范围,系统就会像一位不知疲倦的猎头,7x24小时为你搜寻机会,并针对每一个机会生成高度个性化的申请材料。
整个系统的核心是6个各司其职的AI智能体,它们都基于OpenAI最新的Responses API构建,形成了一个分工明确的工作流。最近,我已经成功将所有智能体迁移到了Responses API,并实现了真实的网页搜索集成,这意味着它不再依赖于静态的测试数据,而是能真正去LinkedIn、Indeed这些招聘网站上抓取实时的职位信息。基础架构已经打磨完毕,所有核心组件都经过了验证,为真正的自动化求职申请打下了生产就绪的基础。
无论你是正在找工作的开发者、设计师,还是任何领域的专业人士,如果你希望从重复的申请劳动中解放出来,把精力集中在面试准备和技能提升上,那么这个项目的思路和实现细节,或许能给你带来一些启发。接下来,我会详细拆解整个系统的架构、每个智能体的工作原理、技术栈选型的考量,以及在实际搭建过程中踩过的那些坑。
2. 智能体架构设计与核心思路拆解
2.1 为何选择多智能体架构而非单一模型
在项目初期,一个很自然的想法是:用一个强大的语言模型(比如GPT-4)来处理所有步骤不就好了?让它读职位描述,然后生成简历和求职信。但深入思考后,我放弃了这种“全能模型”的思路,转而采用了多智能体(Multi-Agent)架构。原因主要有三点:
第一,职责分离与专业化。 求职申请过程中的每个环节,其任务目标、所需输入和输出格式都截然不同。职位发现需要精准的网络搜索和结果过滤;简历优化需要深入理解你的经历并与职位要求做匹配;求职信撰写则需要了解公司文化并调整语气。让一个模型同时记住所有这些上下文并切换“角色”,不仅会显著增加提示词(Prompt)的复杂度和长度,更容易导致任务混淆,比如在优化简历时不小心写出了求职信的开头。而拆分成多个智能体,每个智能体都可以被精心设计成某个领域的“专家”,拥有高度定制化的系统指令(System Instruction)和提示词模板,专注做好一件事。
第二,提升可靠性与可维护性。 在自动化流程中,任何一个环节的失败都可能导致整个流程中断。如果使用单一模型,一旦在某个步骤(比如解析复杂的职位描述)上产生幻觉或错误输出,后续所有步骤都会基于这个错误输入进行,最终结果可能完全不可用。多智能体架构将流程模块化,每个智能体都是一个独立的“黑盒”,有明确的输入输出接口。这样,当某个环节出错时,我们可以快速定位到是哪个智能体的问题,单独对其进行调试、优化甚至替换,而不影响其他部分。系统的鲁棒性大大增强。
3. 实现工作流编排与状态管理。 求职申请本身就是一个标准的工作流:先找到职位(JobDiscovery),然后为这个职位优化简历(ResumeOptimizer),接着撰写求职信(CoverLetter),最后提交(ApplicationSubmitter)。多智能体架构天然适合这种管道式(Pipeline)或工作流式(Workflow)的任务。我们可以很容易地设计一个“协调者”(Orchestrator)来管理这些智能体的执行顺序,传递中间状态(比如将发现的职位ID传递给简历优化器),并处理可能出现的分支逻辑(比如,如果发现是高级职位,则启用更正式的求职信语气)。这种结构清晰,逻辑上也更符合人类的操作习惯。
基于以上考量,我最终设计了6个核心智能体,它们像一支分工明确的团队,共同完成“智能求职”这个任务。
2.2 六大智能体分工与协同机制详解
这六个智能体构成了HunterAgent的核心引擎。它们并非孤立运行,而是通过一个中央协调逻辑(目前集成在主后端应用中)进行有序的接力。
1. JobDiscoveryAgent(职位发现智能体) 这是工作流的起点。它的核心任务是充当你的“信息侦察兵”。你给它一个搜索查询(例如“Python后端开发 远程 美国”),它就会利用集成的网络搜索工具,去真实的招聘网站(如LinkedIn, Indeed)进行爬取。这里的关键在于“真实”和“验证”。它不仅仅是模拟搜索,而是通过 linkedin-mcp-server 和 requests+BeautifulSoup 这样的工具,实际访问招聘页面,抓取结构化的职位信息,包括职位标题、公司、地点、描述、申请链接等。它还会对抓取到的URL进行有效性验证,确保链接是可访问的,避免后续步骤卡在死链上。
2. ResumeOptimizerAgent(简历优化智能体,也称ResumeManager) 这是提升申请成功率的关键一环。它的输入是你的基础简历(一份包含你所有经历和技能的“母版”)和JobDiscoveryAgent发现的某个具体职位描述。它的任务不是重写你的简历,而是进行“针对性优化”。这个过程包含几个子任务:
- 行业与技能研究: 智能体会分析职位描述中的关键词、技术栈和软技能要求。
- ATS优化: 许多公司使用申请人跟踪系统(ATS)进行初筛。智能体会确保优化后的简历包含足够多的、与职位描述匹配的关键词(同时保持自然),并采用对ATS友好的格式(如避免复杂的表格和图形)。
- 经历重组与强调: 它会调整你工作经历中项目描述的先后顺序,将与目标职位最相关的经历和成就提到更显眼的位置,并用更符合该职位要求的语言进行重述。
- 生成多版本: 有时,针对一个职位可以准备侧重不同技能的简历版本。智能体可以生成1-3个不同侧重点的优化版本,供你选择或由后续流程自动选取最佳匹配。
3. CoverLetterAgent(求职信撰写智能体,也称LetterWriter) 如果说简历是“硬实力”的清单,求职信就是“软实力”和动机的陈述。这个智能体的目标是生成一封让人感觉真诚、有针对性、而非模板化的求职信。为此,它做了两件事:
- 实时公司研究: 在撰写前,它会尝试访问目标公司的官网、新闻页面或LinkedIn公司主页,抓取公司的近期动态、文化价值观、产品信息等。这使得求职信中可以自然地提及“我对贵公司最近发布的XX产品很感兴趣”或“我非常认同贵公司倡导的XX价值观”等内容,显著提升个性化程度。
- 多语气变体: 不同的公司和职位需要不同的沟通风格。申请一家初创公司可能需要展现热情和活力,而申请一家传统金融机构则可能需要更稳重和专业。CoverLetterAgent可以根据预设或自动判断,生成“专业正式”、“热情创新”、“简洁直接”等不同语气风格的求职信草稿。
4. ApplicationSubmitterAgent(申请提交智能体) 这是将数字材料转化为实际申请动作的“执行者”。它的核心是利用浏览器自动化工具(计划使用Playwright MCP集成)来模拟人类操作,自动填写申请表单。这包括上传简历和求职信文件、填写个人信息、回答筛选问题等。这个环节技术挑战最大,因为不同公司的申请页面千差万别,需要智能体具备一定的网页结构理解和自适应填写能力。 (注:根据输入材料,此功能尚在规划中,未完全实现)
5. EmailNotificationAgent(邮件通知智能体) 自动化系统需要与用户保持沟通。这个智能体负责汇总工作流的结果。它会在每天定时(或每当有新的匹配职位时)运行,收集JobDiscoveryAgent发现的所有符合条件的职位列表,以及ResumeOptimizerAgent和CoverLetterAgent为它们生成的材料概要,整理成一份清晰的“求职日报”,通过Gmail的SMTP服务发送到你的邮箱。这样,你无需登录系统,就能对自动化进程一目了然,并可以手动审核那些特别感兴趣的职位。
6. NetworkContactsAgent(人脉联络智能体) 这是一个增值功能,旨在利用网络扩大机会。当系统锁定一个目标公司后,这个智能体会尝试在LinkedIn等平台上,搜索并识别出该公司内的3名相关员工(例如,目标部门的招聘经理、技术主管或潜在同事),或者获取公司的员工目录信息。这些信息可以用于:1) 在求职信中提及(如果合适且自然);2) 为你后续可能的主动联络提供线索;3) 帮助系统更好地理解公司的组织架构。
这六个智能体通过共享的数据库(Supabase)和内部状态管理进行协作。例如,JobDiscoveryAgent将发现的职位存入数据库,并标记状态为“待处理”。ResumeOptimizerAgent会轮询或接收事件,获取“待处理”的职位和对应的简历任务,处理完成后将优化后的简历文件存入云存储(Supabase Storage),并更新任务状态。整个流程像一个高效的流水线,每个“工人”(智能体)只专注于自己的工序。
3. 技术栈选型与核心组件解析
3.1 后端与AI层:为什么是Python + OpenAI Responses API?
选择Python作为后端和AI智能体的开发语言,几乎是自然而然的决定。其丰富的生态系统,特别是在数据科学、网络爬虫和AI集成方面的库,是其他语言难以比拟的。Streamlit用于快速构建管理前端,Pandas用于处理抓取到的职位数据表格,都非常顺手。
AI框架的核心抉择:从Completion API到Responses API 项目早期,智能体是基于OpenAI传统的Chat Completion API构建的。每个智能体都是一系列函数,内部调用 client.chat.completions.create ,并附上长长的系统指令和用户消息。但随着智能体数量增多和逻辑变复杂,问题开始显现:
- 上下文管理混乱: 每个智能体的提示词模板、历史对话管理都需要自己实现,代码重复且不易维护。
- 流式处理与工具调用分离: 想要实现流式输出(一边生成一边显示)和函数调用(Tool Calling)需要额外的逻辑。
- 状态维护困难: 对于需要多轮对话的复杂智能体(比如先问用户澄清一个问题,再继续),需要手动维护对话历史。
OpenAI推出的 Responses API 完美地解决了这些问题。它不再是简单地发送一个请求获取一个回复,而是允许你创建一个持久的“响应”(Response)会话。在这个会话中,你可以:
- 管理多轮对话: 自动维护上下文,无需自己拼接消息历史。
- 集成流式输出: 原生支持,轻松实现打字机效果。
- 无缝使用工具(Tools): 将外部工具(如搜索函数、数据库查询函数)定义给智能体,当智能体认为需要时,会自动发起工具调用,并将结果纳入上下文继续生成。这对于JobDiscoveryAgent调用搜索工具、NetworkContactsAgent调用LinkedIn查询工具来说,是革命性的简化。
- 统一的智能体抽象: 每个智能体都可以被建模为一个独立的Response会话,拥有自己的系统指令、工具集和对话历史。代码结构变得异常清晰。
迁移到Responses API后,智能体的代码从一堆分散的函数调用,变成了对一个个“会话”的管理。可靠性和可维护性得到了质的提升。这也是输入材料中提到的“100% Responses API adoption”和“Unified BaseAgent pattern established”所指的核心进展。
3.2 数据持久化与存储:Supabase的集成考量
对于这样一个项目,数据层需要满足几个需求:存储结构化的任务和职位数据、存储非结构化的文件(简历、求职信PDF),以及提供简单的用户身份管理。自建数据库和文件存储服务器显然小题大做,而纯文件存储(如本地JSON+文件夹)又难以支撑多智能体并发访问和状态同步。
Supabase 成为了一个优雅的解决方案。它本质上是一个开源的Firebase替代品,提供了:
- PostgreSQL数据库: 完美的关系型数据库,用于存储用户信息、职位列表、任务队列、执行日志等所有结构化数据。我们可以用SQL定义清晰的数据模型,例如
jobs表、application_tasks表,并通过外键关联它们。 - 云存储(Storage): 内置的对象存储服务,类似于AWS S3的易用接口。优化后的简历(PDF/DOCX)和生成的求职信文件可以直接上传到Supabase Storage,并通过一个URL链接关联到数据库中的相应记录。这样,前端界面或邮件通知中可以直接引用这些文件的预览或下载链接。
- 实时订阅与认证: 虽然当前项目未深度使用,但Supabase的实时功能(监听数据库变化)和内置的用户认证系统,为未来实现多用户、实时看板等功能预留了可能性。
使用Supabase后,整个数据流变得非常清晰:智能体们读写同一个PostgreSQL数据库来同步状态,将生成的文件扔进统一的Storage桶,前端通过Supabase客户端直接查询和展示。它用一个服务解决了数据库、文件存储和API后端(通过其自动生成的RESTful API)三个问题,极大地简化了后端架构。
3.3 外部服务集成:爬虫、自动化与通信
网页爬虫:LinkedIn-MCP-Server与BeautifulSoup 职位发现的核心是数据获取。对于LinkedIn,我选择了 linkedin-mcp-server 。MCP(Model Context Protocol)是一种新兴的协议,旨在标准化大型语言模型与外部工具/数据源的连接。这个MCP服务器专门为LinkedIn设计,提供了相对稳定和结构化的接口来搜索职位、获取公司信息,比直接解析HTML页面更可靠。对于Indeed等其他网站,则采用经典的 requests 库抓取网页,再用 BeautifulSoup4 解析HTML结构。这里的关键是 设置合理的请求头、处理JavaScript渲染(可能需要初步的浏览器上下文)以及遵守网站的robots.txt协议 ,避免因请求过快或行为不当导致IP被封。
浏览器自动化:Playwright的定位 ApplicationSubmitterAgent的终极目标是自动填写申请表单。这是一个充满挑战的领域,因为每个公司的申请页面都是独特的“雪花”。Playwright作为现代浏览器自动化工具,支持Chromium、Firefox和WebKit,能可靠地模拟用户操作。计划通过MCP集成,是希望将Playwright的能力也“工具化”,让AI智能体在需要时,可以调用“点击元素”、“填写输入框”、“上传文件”等工具,并结合一些网页结构分析,来尝试自适应地完成申请。 这部分的复杂性最高,也是当前标注为“planned”的原因,需要大量的测试和异常处理逻辑。
邮件通知:Python smtplib与Gmail SMTP EmailNotificationAgent使用Python内置的 smtplib 库,通过Gmail的SMTP服务器发送邮件。实现本身不复杂,但有几个关键点:
- 安全性: 不建议在代码中硬编码密码。应使用Gmail的“应用专用密码”或通过OAuth 2.0授权获取访问令牌。
- 邮件内容设计: 日报邮件需要清晰易读。通常我会用HTML格式,包含一个表格,列出职位名称、公司、状态、优化后的简历和求职信链接,让用户一眼就能掌握全局。
- 发送频率与限制: 需注意Gmail的每日发送限制,避免被当作垃圾邮件发送者。
4. 核心实现细节与避坑指南
4.1 基于OpenAI Responses API的智能体统一模式
将所有智能体迁移到Responses API后,我建立了一个统一的 BaseAgent 模式。这个模式封装了与Responses API交互的通用逻辑,让每个具体智能体只需关注自己的业务提示词和工具。
import openai
from typing import List, Dict, Any, Optional
class BaseAgent:
def __init__(self, name: str, system_instruction: str, tools: List[Dict] = None):
self.client = openai.OpenAI(api_key=your_api_key)
self.name = name
self.system_instruction = system_instruction
self.tools = tools or []
self.response_session = None # 持久化响应会话
def initialize_session(self):
"""创建或恢复一个Responses API会话"""
if not self.response_session:
# 创建新会话,关联系统指令和工具
self.response_session = self.client.responses.create(
model="gpt-4-turbo",
instructions=self.system_instruction,
tools=self.tools,
# 可以传入metadata标记智能体类型
)
return self.response_session
def process(self, input_text: str, stream: bool = False) -> str:
"""处理输入,返回智能体的文本回复。支持流式输出。"""
session = self.initialize_session()
# 向现有会话追加用户消息
message = self.client.responses.messages.create(
response_id=session.id,
content=input_text,
role="user"
)
# 获取模型的回复(流式或非流式)
if stream:
# 这里处理流式输出,逐块yield
pass
else:
# 阻塞式获取完整回复
response = self.client.responses.get(session.id)
# 解析回复中的文本和可能的工具调用
final_text = self._parse_response(response)
return final_text
def _parse_response(self, response) -> str:
"""解析响应,处理可能发生的工具调用。"""
# 简化示例:实际需要递归处理工具调用和结果提交
for content in response.output:
if content.type == 'text':
return content.text
elif content.type == 'tool_calls':
# 执行工具调用,并将结果提交回会话
tool_results = self._execute_tools(content.tool_calls)
# 提交工具结果,并继续获取模型回复
self.client.responses.messages.create(
response_id=response.id,
content=tool_results,
role="tool"
)
# 递归获取新的回复
new_response = self.client.responses.get(response.id)
return self._parse_response(new_response)
return ""
def _execute_tools(self, tool_calls):
"""执行具体的工具调用,由子类或外部工具集实现。"""
# 根据tool_calls的信息,调用对应的函数
results = []
for call in tool_calls:
if call.name == "search_web":
# 调用实际的网络搜索函数
search_result = perform_web_search(call.arguments.query)
results.append({
"tool_call_id": call.id,
"output": search_result
})
return results
避坑指南:Responses API的会话管理
- 会话生命周期: Responses会话默认会在一段时间不活动后过期。对于需要长期保持状态的智能体,需要在代码中实现会话的持久化(保存session.id到数据库)和恢复机制。
- 工具调用循环:
_parse_response方法中的递归逻辑是关键。模型可能会在一次回复中要求调用多个工具,也可能在得到工具结果后继续生成文本或发起新的工具调用。必须妥善处理这个循环,直到模型返回最终的文本输出。 - 成本与上下文长度: 持久化会话意味着整个对话历史(包括工具调用的输入输出)都会计入上下文令牌数。对于长流程任务,需要监控上下文长度,必要时主动总结或清理早期历史,以控制成本。
4.2 JobDiscoveryAgent:实时网络搜索与反爬策略
这是系统与真实世界连接的桥梁。其核心函数 perform_web_search 需要兼顾效率与稳健性。
def perform_web_search(query: str, site: str = "linkedin") -> Dict[str, Any]:
"""
执行针对特定招聘网站的职位搜索。
返回结构化的职位列表。
"""
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ...',
'Accept-Language': 'en-US,en;q=0.9',
}
jobs = []
if site == "linkedin":
# 使用LinkedIn MCP Server(假设其运行在本地某个端口)
try:
# 这里示意与MCP服务器的通信,实际可能是gRPC或HTTP
mcp_client = LinkedInMCPClient()
search_results = mcp_client.search_jobs(query, limit=20)
for result in search_results:
# 验证URL可访问性
if validate_url(result['url']):
jobs.append({
'title': result['title'],
'company': result['company'],
'location': result['location'],
'url': result['url'],
'description_snippet': result['snippet'],
'posted_date': result['date'],
'source': 'linkedin'
})
except Exception as e:
log_error(f"LinkedIn MCP搜索失败: {e}")
# 可降级到备用方案
elif site == "indeed":
# 直接爬取Indeed
base_url = f"https://www.indeed.com/jobs"
params = {'q': query, 'l': 'Remote', 'sort': 'date'}
try:
resp = requests.get(base_url, params=params, headers=headers, timeout=10)
soup = BeautifulSoup(resp.text, 'html.parser')
job_cards = soup.find_all('div', class_='job_seen_beacon') # Indeed的卡片类名
for card in job_cards[:15]: # 限制数量
title_elem = card.find('h2', class_='jobTitle')
company_elem = card.find('span', class_='companyName')
# ... 解析其他字段
if title_elem and company_elem:
job_url = "https://www.indeed.com" + title_elem.find('a')['href']
if validate_url(job_url):
jobs.append({
'title': title_elem.text.strip(),
'company': company_elem.text.strip(),
'url': job_url,
'source': 'indeed'
})
except Exception as e:
log_error(f"Indeed爬取失败: {e}")
return {'query': query, 'jobs_found': len(jobs), 'jobs': jobs}
避坑指南:网络爬虫的生存法则
- 尊重robots.txt: 在爬取任何网站前,务必检查其
robots.txt文件(如https://www.linkedin.com/robots.txt)。虽然招聘信息通常允许被搜索引擎索引,但频繁的爬取可能被禁止。遵守规则,设置合理的爬取间隔(如每次搜索间隔5-10秒)。 - 轮换User-Agent与使用代理: 固定的User-Agent和IP地址是容易被识别和封禁的特征。准备一个常见的浏览器User-Agent列表进行轮换。对于大规模爬取,考虑使用住宅代理IP池。
- 健壮的错误处理: 网络请求可能因超时、连接错误、页面结构变化而失败。代码中必须有完善的try-except块,记录错误日志,并考虑重试机制(但需谨慎,避免在短时间内对同一目标重试太多次)。
- 验证URL有效性: 爬取到的链接可能已经失效或需要登录才能访问。在将职位加入待处理队列前,用一个简单的HEAD请求验证链接可访问,可以避免后续智能体处理时浪费API调用。
- 备用数据源: 不要依赖单一网站。集成多个招聘源(如LinkedIn, Indeed, Glassdoor等)可以提高系统的鲁棒性和职位覆盖范围。
4.3 ResumeOptimizerAgent:ATS优化与个性化定制的平衡术
这是最体现AI价值的环节之一。其提示词设计至关重要。
# ResumeOptimizerAgent 的系统指令示例
RESUME_OPTIMIZER_SYSTEM_INSTRUCTION = """
你是一位顶尖的职业顾问和简历优化专家。你的任务是根据用户提供的原始简历和特定的职位描述,生成一份高度优化、针对性强、且能通过ATS(申请人跟踪系统)筛选的简历版本。
请遵循以下步骤:
1. **深度分析职位描述:** 提取关键技能(硬技能和软技能)、行业术语、职位核心职责和任职要求。
2. **映射与匹配:** 将原始简历中的每段经历、每个项目、每项技能与职位描述的要求进行映射。找出最相关的部分。
3. **优化策略:**
a. **关键词融合:** 自然地将职位描述中的关键词融入简历的“技能”部分和工作经历描述中。避免生硬堆砌。
b. **成就量化:** 使用STAR法则(情境、任务、行动、结果)重写工作经历,尽可能用量化数据(如“提升效率30%”、“管理50万美元预算”)来突出成就。
c. **相关性排序:** 将与目标职位最相关的工作经历和项目放在更靠前的位置。对于不直接相关的经历,可以简化描述。
d. **ATS友好格式:** 确保使用简单的标题、清晰的章节、标准的字体(如Arial, Calibri)。避免使用表格、图形、文本框等可能被ATS解析错误的设计元素。
4. **输出格式:** 输出优化后的完整简历文本,使用Markdown格式,包含“联系方式”、“摘要”、“工作经历”、“项目经验”、“技能”、“教育”等标准章节。同时,在最后提供一个“本次优化重点”的总结,列出你针对该职位所做的关键改动。
原始简历和职位描述将作为用户输入提供。
"""
避坑指南:简历优化的艺术与科学
- 不要丢失真实性: AI优化容易过度,导致简历看起来“太好以至于不真实”。必须严格基于用户提供的真实经历进行优化,不能凭空捏造技能或项目。在系统指令中要强调“基于原始简历”。
- 平衡关键词密度与可读性: ATS依赖关键词,但最终阅读简历的是人。确保句子通顺、专业,关键词的融入要自然。可以建议智能体在优化后,以“人类读者”的角度通读一遍,检查流畅度。
- 处理经历空白或转行: 对于职业空窗期或转行的候选人,AI需要更巧妙的策略。例如,将空窗期的学习、自由职业或志愿工作转化为有价值的经历;将过去不相关行业的技能迁移到新岗位的需求上(如“项目管理”、“数据分析”等通用技能)。
- 提供多版本选项: 针对一个职位,可以要求智能体生成2-3个略有侧重点的版本(例如,一个版本强调技术深度,另一个版本强调团队领导力)。这给了用户选择权,也展示了AI的多角度思考能力。
- 保留原始版本: 任何优化都必须基于一个“主简历”副本。系统应该始终保存用户的原始简历,每次优化都是生成一个新的衍生版本,而不是覆盖原稿。
5. 系统集成、部署与运维实践
5.1 工作流编排与任务队列管理
六个智能体如何有序协作?我采用了一个基于数据库状态的任务队列模型。核心表结构设计如下:
-- jobs表:存储发现的职位
CREATE TABLE jobs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
title TEXT NOT NULL,
company TEXT NOT NULL,
location TEXT,
url TEXT UNIQUE NOT NULL,
description TEXT,
source TEXT,
discovered_at TIMESTAMPTZ DEFAULT NOW(),
status TEXT DEFAULT 'new' -- 'new', 'processing', 'resume_optimized', 'letter_generated', 'applied', 'archived'
);
-- tasks表:存储每个职位对应的处理任务
CREATE TABLE application_tasks (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
job_id UUID REFERENCES jobs(id) ON DELETE CASCADE,
task_type TEXT NOT NULL, -- 'resume_optimization', 'cover_letter', 'application_submit'
status TEXT DEFAULT 'pending', -- 'pending', 'in_progress', 'completed', 'failed'
input_data JSONB, -- 存储任务输入,如原始简历内容、职位描述等
output_data JSONB, -- 存储任务输出,如优化后的简历文本、求职信文本、文件存储路径等
assigned_agent TEXT, -- 记录是哪个智能体实例处理的
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
error_message TEXT
);
工作流引擎(一个独立的Python服务或主应用中的调度模块)会定期执行以下逻辑:
- 发现阶段: 调用JobDiscoveryAgent,将新发现的职位插入
jobs表(status='new')。 - 任务创建: 对于每个
status='new'的职位,创建两条初始任务记录插入tasks表:一条task_type='resume_optimization',一条task_type='cover_letter',状态均为pending。 - 任务分发: 有多个“工人”(Worker)进程或线程在轮询
tasks表,寻找status='pending'的任务。当ResumeOptimizerAgent的工人找到一个简历优化任务时,它会锁定该任务(将状态改为in_progress),从input_data中读取原始简历和职位描述,调用AI进行处理,将结果存入output_data,并更新任务状态为completed,同时将对应jobs记录的status更新为resume_optimized。 - 触发下游任务: CoverLetterAgent的工人会寻找那些
job.status='resume_optimized'且自身task_type='cover_letter'的pending任务。它执行类似流程,生成求职信。 - 状态同步与通知: 当某个职位的简历和求职信任务都完成时,工作流引擎可以将
job.status更新为ready_for_application,并触发EmailNotificationAgent发送摘要邮件。
这种基于数据库状态机的设计,简单可靠,易于扩展和调试。每个智能体都是无状态的,它们只关心处理下一个任务。
5.2 部署架构与监控考量
对于个人或小规模使用,部署可以相对简单:
- 后端服务: 将Python应用(包含智能体逻辑、工作流引擎、API接口)部署在云服务器(如AWS EC2、DigitalOcean Droplet)或容器平台(如Fly.io、Railway)。使用
gunicorn或uvicorn作为WSGI/ASGI服务器。 - 数据库与存储: 直接使用Supabase的云服务,无需自维护。
- 前端: Streamlit应用可以和后端部署在一起,或者单独部署。由于其是Python应用,也可以和后端共享同一个服务。
- 任务调度: 使用
celery+redis作为分布式任务队列是更专业的选择,但对于初期,使用schedule库或apscheduler在单个进程中实现定时轮询(如每30分钟运行一次JobDiscoveryAgent)也是可行的。
监控与日志:
- 应用日志: 使用
structlog或logging库记录每个智能体的运行开始、结束、输入输出摘要、错误信息。日志应输出到文件,并集成到如Sentry这样的错误监控平台。 - 性能监控: 记录每个API调用(尤其是OpenAI API)的耗时和令牌使用量,用于成本分析和性能优化。
- 数据库监控: Supabase控制台提供了基本的查询性能和存储使用监控。
- 健康检查: 为后端服务设置一个
/health端点,用于监控服务是否存活。
5.3 成本控制与优化策略
AI自动化项目的运行成本主要来自OpenAI API调用。必须进行精细化管理:
- 缓存策略: 对于相同的职位描述和原始简历,优化结果很可能是相同的。可以在数据库中缓存
(job_description_hash, resume_hash) -> optimized_resume_text的映射,避免重复计算。求职信生成也可以采用类似缓存,但需考虑公司研究信息可能变化。 - 模型选型: 并非所有任务都需要最强大的模型。例如,初步的职位信息提取和分类,可以尝试使用
gpt-3.5-turbo,成本远低于gpt-4-turbo。而需要深度分析和创造性写作的简历优化和求职信撰写,则值得使用更强大的模型。 - 令牌使用优化: 在系统指令和提示词中避免冗长。将不必要的历史上下文及时清理。对于Responses API,注意监控整个会话的令牌消耗。
- 用量配额与告警: 在OpenAI控制台设置每日/每月使用量配额和预算告警,防止意外超支。
- 任务去重: JobDiscoveryAgent应能识别并过滤掉已经处理过的重复职位(基于URL或标题+公司的哈希值),避免为同一职位重复生成材料。
6. 伦理考量、局限性及未来方向
6.1 自动化求职的伦理边界
在构建这样一个系统时,必须严肃考虑其伦理影响:
- 信息真实性: 系统必须基于用户提供的真实信息进行优化,绝不能伪造学历、工作经验或技能。在提示词中应加入强伦理约束。
- 公平性与偏见: AI模型可能隐含训练数据中的偏见。需要监控优化后的简历是否无意中强化了某些性别、种族或文化刻板印象。应鼓励多样性、公平性的表述。
- 对招聘方的影响: 大规模、高质量的自动化申请可能会增加招聘方的筛选负担。系统的设计初衷应是帮助认真求职的人提升效率和质量,而非用于“简历轰炸”或欺诈。
- 用户知情与同意: 如果未来发展为多用户服务,必须明确告知用户其申请材料将由AI生成和优化,并获取用户的明确授权。
6.2 当前系统的局限性
认识到局限性是改进的开始:
- 申请提交的“最后一公里”: ApplicationSubmitterAgent是最大的技术挑战。网页表单的多样性、验证码、多步骤流程、需要登录的申请系统等,使得完全通用的自动化提交极其困难。目前更可行的路径可能是半自动化:系统准备好所有材料,并生成详细的填写指南,由用户手动完成最终提交。
- 对动态内容的处理: 一些招聘页面大量使用JavaScript动态加载内容,简单的
requests+BeautifulSoup无法抓取。这需要引入无头浏览器(如Playwright)进行渲染,增加了复杂性和资源消耗。 - 个性化与“过度优化”的风险: AI生成的求职信如果过于完美或使用套路化语言,可能被有经验的招聘人员识别出来,反而产生负面效果。需要在“个性化”和“自然感”之间找到平衡。
- 依赖外部API: 系统的核心能力依赖于OpenAI API的可用性和成本。也依赖于LinkedIn等第三方网站的结构稳定性,它们的变化可能导致爬虫失效。
6.3 迭代路线图与扩展可能
尽管已有坚实基础,但仍有广阔的进化空间:
- 智能申请提交(攻坚): 集中精力攻克Playwright MCP集成,实现针对主流招聘平台(如Greenhouse, Lever, Workday)的适配器,逐步提高自动提交的成功率。
- 面试准备助手: 扩展系统能力,在职位申请后,自动分析该职位的描述,为用户生成可能的面试问题、准备要点,甚至进行模拟面试。
- 技能差距分析: 分析用户简历与目标职位描述之间的差距,生成个性化的学习建议和技能提升路径图。
- 市场趋势洞察: 聚合分析所有抓取到的职位数据,为用户提供所在领域的薪资趋势、热门技能需求、活跃招聘公司等洞察报告。
- 多模态输出: 除了文本简历,未来是否可以生成简单的作品集介绍视频脚本或领英个人资料优化建议?
构建HunterAgent的过程,是一个将前沿AI能力与具体、繁琐的现实需求相结合的过程。它目前还不是一个可以完全“撒手不管”的魔法黑盒,但它已经能够承担求职过程中最耗时、最重复的环节——信息搜集和材料初稿生成。对我个人而言,开发它的过程本身,就是一次对AI智能体架构、工作流自动化和实际问题解决的深度探索。如果你也对这个领域感兴趣,不妨从一个小而具体的智能体开始,比如先做一个能帮你自动总结并分类RSS订阅文章摘要的智能体,体会一下让AI分担重复性认知劳动的乐趣。
更多推荐



所有评论(0)