职业规划Agent是一类专门服务于求职场景的智能体应用,它的核心任务是双向打通简历与岗位这两个信息源:一方面把岗位描述(JD)中的隐性要求显性化、结构化,另一方面把简历中的经历提炼成可量化、可匹配的能力标签,再通过语义相似度计算完成精准匹配,最后借助大语言模型完成简历的定向优化。整个流程看似简单,但每一步都有不少技术细节值得推敲。

一、整体架构设计:三个核心模块如何协作
一个完整的职业规划Agent通常拆分为三个模块:JD解析模块、简历解析模块和匹配决策模块。JD解析模块负责从岗位描述文本中抽取职位名称、技能要求、经验年限、学历门槛、软性要求等字段;简历解析模块负责把PDF或Word格式的简历转换成结构化数据,提取教育经历、工作经历、项目经验、技能栈等关键信息;匹配决策模块则基于前两个模块的输出,计算匹配得分并生成优化建议。
这种拆分的好处在于各模块可以独立迭代。例如JD解析对准确率要求高,可以用小模型加规则做精细抽取;匹配决策需要强推理能力,则交给大模型处理。下面是一个简化的模块定义示例:
from dataclasses import dataclass, field
@dataclass
class JobRequirement:
title: str # 岗位名称
hard_skills: list = field(default_factory=list) # 硬技能要求
soft_skills: list = field(default_factory=list) # 软技能要求
min_years: int = 0 # 最低经验年限
education: str = "" # 学历要求
keywords: list = field(default_factory=list) # 关键词
@dataclass
class ResumeProfile:
skills: list = field(default_factory=list)
experiences: list = field(default_factory=list)
projects: list = field(default_factory=list)
education: str = ""
@dataclass
class MatchResult:
score: float # 匹配得分,0到1之间
matched: list = field(default_factory=list) # 已满足的要求
gaps: list = field(default_factory=list) # 缺失项
suggestions: list = field(default_factory=list) # 优化建议
数据结构定清楚之后,后续每个模块只负责生产这些字段,模块之间通过数据对象传递信息,避免字符串来回拼接造成的解析混乱。这是搭建Agent系统时非常值得坚持的工程习惯。
二、JD解析与简历结构化:信息抽取的技术选型
JD文本的特点是格式高度不规范,有的用短横线列表,有的是大段自然语言,还夹杂着薪资范围、公司介绍等噪声。直接用正则表达式抽取虽然成本最低,但召回率很不稳定。实践中更推荐两种方案结合:先用大语言模型做整体理解,再用规则做字段校验与兜底。
以调用大模型抽取JD字段为例,提示词设计要强调输出格式的稳定性。可以要求模型只输出JSON,并明确每个字段的含义与缺失时的填充规则:
JD_EXTRACT_PROMPT = """你是一个岗位信息抽取助手。
请从下面的岗位描述中抽取信息,严格按JSON格式输出,不要输出其他内容。
字段说明:
- title: 岗位名称,字符串
- hard_skills: 硬技能列表,如Python、SQL,没有则输出空列表
- soft_skills: 软技能列表,如沟通能力,没有则输出空列表
- min_years: 最低工作年限,整数,未提及则为0
- education: 学历要求,未提及则输出"不限"
岗位描述:
{jd_text}
"""
def parse_jd(llm_client, jd_text: str) -> JobRequirement:
resp = llm_client.chat(
messages=[{"role": "user",
"content": JD_EXTRACT_PROMPT.format(jd_text=jd_text)}],
temperature=0 # 抽取任务温度设为0保证稳定
)
import json
data = json.loads(resp)
return JobRequirement(
title=data["title"],
hard_skills=data["hard_skills"],
soft_skills=data["soft_skills"],
min_years=int(data["min_years"]),
education=data["education"],
keywords=data["hard_skills"] + data["soft_skills"]
)
简历解析则多了一步文档处理。PDF简历需要先做文本提取,常用库包括pdfplumber和PyMuPDF,遇到双栏排版时要特别注意阅读顺序的还原,否则工作经历的时间线会被打乱。提取出原始文本后,同样交给大模型做结构化,得到ResumeProfile对象。经验上建议在提示词中要求模型把每段经历拆成职责与成果两部分,并尽量保留数字化的业绩描述,这会为后续的匹配打分提供高质量的输入。
三、匹配算法:从关键词重合到语义相似度
最朴素的匹配方式是关键词重合率,即计算简历技能列表与JD硬技能列表的交集占比。这种方式实现简单,但有个明显缺陷:同义词会被判为不匹配。比如JD要求熟悉关系型数据库,简历写的是熟练使用MySQL和PostgreSQL,关键词交集为零,但语义上完全匹配。
解决思路是引入向量语义匹配。把技能短语、经历描述都编码成向量,用余弦相似度衡量接近程度。常用的做法是使用Sentence-Transformers系列模型做句向量编码:
from sentence_transformers import SentenceTransformer, util
model = SentenceTransformer("paraphrase-multilingual-MiniLM-L12-v2")
def semantic_match(jd: JobRequirement, resume: ResumeProfile) -> MatchResult:
jd_vecs = model.encode(jd.keywords, convert_to_tensor=True)
resume_text = resume.skills + [e for exp in resume.experiences for e in exp]
resume_vecs = model.encode(resume_text, convert_to_tensor=True)
matched, gaps = [], []
for i, kw in enumerate(jd.keywords):
sim = util.cos_sim(jd_vecs[i], resume_vecs).max().item()
if sim > 0.6: # 阈值可根据实际数据调优
matched.append(kw)
else:
gaps.append(kw)
score = len(matched) / max(len(jd.keywords), 1)
return MatchResult(score=score, matched=matched, gaps=gaps)
阈值的选择需要结合真实数据做调优。阈值过低会虚报匹配度,让求职者投递大量不合适的岗位;阈值过高则会漏掉真正合适的机会。一种改进策略是分层设置:硬技能用较严格的阈值,软技能用较宽松的阈值,因为软技能的描述往往更模糊,语义距离天然偏大。
四、简历优化改写:让Agent生成可用的修改建议
匹配结果出来之后,Agent最有价值的部分才刚开始:针对gap项给出具体的简历改写建议。这里的改写不是简单地把JD关键词塞进简历,那种做法容易在面试环节露馅,甚至触发招聘系统的反作弊检测。正确的做法是基于求职者的真实经历做定向强化,把原本表述模糊的成果改写成与目标岗位强相关的表达。
具体实现上,可以把匹配结果、原始简历和目标JD一起送入大模型,要求它只做三类修改:一是调整技能描述的措辞,使其与JD术语体系一致;二是重新排列经历的呈现顺序,把与岗位最相关的项目放在前面;三是对量化成果做强调,比如把负责后端开发改成主导订单服务重构,接口平均响应时间从800毫秒降至200毫秒。
OPTIMIZE_PROMPT = """你是一名资深猎头顾问,请基于以下信息优化简历。
修改原则:
1. 不得编造求职者没有的经历
2. 术语表述尽量对齐岗位要求
3. 优先突出与岗位相关的项目
4. 量化成果必须保留原始数据
目标岗位要求:{jd_json}
当前匹配缺失项:{gaps}
原始简历内容:{resume_text}
请输出优化后的简历,并在末尾列出你做了哪些修改及原因。
"""
def optimize_resume(llm_client, jd: JobRequirement,
resume: ResumeProfile, match: MatchResult) -> str:
return llm_client.chat(
messages=[{"role": "user",
"content": OPTIMIZE_PROMPT.format(
jd_json=jd.__dict__,
gaps=match.gaps,
resume_text=resume.__dict__)}],
temperature=0.4 # 改写任务允许一定创造性
)
输出末尾要求列出修改原因这一步非常关键,它让Agent的行为变得可审计,求职者可以逐条确认修改是否属实,避免大模型幻觉导致简历内容失真。对于诚信问题,技术手段只能做辅助约束,最终把关的还是求职者本人。
五、落地时的几个工程注意事项
第一是大模型调用成本的控制。一个求职者可能同时追踪几十个岗位,每次匹配都要调用两次以上大模型,建议对JD解析结果建立缓存,相同岗位的解析结果直接复用。第二是解析失败的重试机制,大模型偶尔会输出不合法的JSON,需要做格式校验加自动重试,必要时降级到规则抽取兜底。第三是隐私合规,简历属于敏感个人信息,服务端处理后应及时清理临时数据,日志中不要落盘存储完整简历内容。
第四是评估体系的建立。Agent给出的匹配得分准不准,需要用真实投递结果来验证,可以统计匹配得分与面试邀约率之间的相关性,持续调整语义阈值与权重配置。只有形成数据闭环,职业规划Agent才能从能用的演示进化为真正好用的求职工具。