年初开始认真整理自己的求职工具链,被一个很有意思的开源项目吸引了注意力——JobRadar。它不是一个普通的招聘网站爬虫,而是把 LLM Agent 的思路真正用到了招聘信息筛选场景里:用本地运行的 LLM 给每一条职位数据打分,再根据分数排序展示,帮你从大量 JD 里快速定位最值得投递的那几份。
这篇文章不打算只做项目介绍,而是围绕 JobRadar 的核心流程,拆解它背后的 agent 设计思路,并给出一套完整的部署、配置、运行和排错方案。无论你是想做 LLM 应用开发的初学者,还是想给自己搭一个自动化求职工具的开发者,都可以照着这篇文章从零跑通。
读完这篇文章,你会掌握以下内容:
- JobRadar 为什么选择“本地 LLM + 评分”而不是直接调云端 API;
- 一个招聘信息筛选 agent 的核心模块和流程;
- 本地大模型环境的搭建方法;
- 完整的评分 Prompt 设计和 JSON 结构化输出方案;
- 一份可运行的 Python 示例代码,覆盖抓取、评分、排序、通知完整链路;
- 常见报错排查和工程化落地的经验。
1. JobRadar 是什么,它解决什么问题
1.1 求职场景中的真实痛点
投递简历之前,大多数人都会经历一个很耗时的阶段:在各个招聘平台搜索关键词,打开一条条 JD,手动判断“这个岗位和我匹不匹配”“薪资范围是否合适”“地点能不能接受”“技能栈是不是我熟悉的”。
当职位数量少的时候,这个流程还能接受。但如果搜索一个比较通用的关键词,比如 “Python 后端工程师”,一次可能返回几十上百条结果。逐条阅读、筛选、记录、对比,通常要花掉整个下午。
我做求职工具时最头疼的就是这一点:信息太多了,但真正适合我的岗位可能只有三五个。人肉筛选效率太低,而且容易漏掉信息。
JobRadar 的切入点就在这里。它是一个开源的求职 agent,通过自动抓取职位信息,调用本地 LLM 对每条职位进行多维度的评分,最终输出一个排序后的推荐结果。这样你只需要把精力放在前几名的岗位上面,而不是在上百条 JD 里大海捞针。
1.2 用 agent 而不是普通脚本
如果只是“自动筛选”,其实写一个基于关键词匹配的脚本就能做到。
但关键词匹配的逻辑太死板了。比如 JD 里写的是 “熟悉 FastAPI 或 Flask”,关键词脚本可能只匹配到 “Flask”,不知道 “FastAPI” 也符合要求。又比如 “薪资 20K-30K”,脚本很难判断这个薪资在你所在市场的竞争力。
LLM 能理解语义,这正是 agent 优于普通脚本的地方。JobRadar 的做法可以概括为:
- 数据采集阶段:从职位源获取原始招聘信息;
- 预处理阶段:清洗 HTML、提取标题、公司、地点、薪资、技能标签等字段;
- LLM 评估阶段:把职位信息包装成 Prompt,让本地 LLM 按多个维度评分;
- 排序输出阶段:根据综合分排序,生成推荐列表;
- 通知阶段:可选地通过 Webhook 推送结果。
这个链路在很多 agent 项目里都有类似的影子。最近大家都在聊 agent 开发、LLM 应用开发,其实 JobRadar 就是一个非常典型的落地场景:数据获取、语义理解、结构化输出、自动化执行,全都有。
1.3 为什么用本地 LLM
JobRadar 比较突出的一个设计选择是使用本地 LLM 来做评分,而不是直接调用云端大模型 API。这里面的考虑主要有三点。
隐私与数据安全。招聘信息虽然不算高度敏感,但里面包含公司信息、薪资范围、职位细节。如果使用云端 API,这些数据会发送到第三方服务。对于注重数据隐私的用户,或者公司内部使用场景来说,本地推理明显更可控。
成本。每天定时跑一次,每次可能要对几十条甚至上百条职位做推理。如果都用云端 API,长时间跑下来是一笔不小的费用。而本地 LLM 只要设备能跑得动,边际成本几乎为零。
网络依赖。本地 LLM 不依赖外部 API 的可用性。只要你拉到了职位数据,即使没有外网,也能完成评分排序。对定时任务来说,少一个外部依赖,就少一类故障。
当然,本地 LLM 也有明显的短板:模型能力不如顶级云端模型,对复杂语义的理解和推理会弱一些;对机器配置有要求;部署和维护需要一定技术基础。后面我会在模型选型部分详细说明。
2. 环境准备与项目初始化
2.1 基础环境说明
JobRadar 这类项目的运行环境比较常规。本文示例以 Python 3.10 及以上版本为主,操作系统使用 Linux 或 macOS 都可以。Windows 也能跑,但后续定时任务的配置方式会不同。
因为项目迭代速度较快,不同版本的 JobRadar 在配置文件和模块命名上可能存在差异。本文按照通用工程化实现思路来演示核心流程,你实际操作时请以仓库内的 README 和实际代码为准。
安装 Python 依赖时,建议使用虚拟环境,避免污染系统 Python。下面创建项目目录和一个虚拟环境:
mkdir -p jobradar-demo cd jobradar-demo python3 -m venv venv source venv/bin/activateWindows 下激活命令对应为:
venv\Scripts\activate虚拟环境激活后,终端提示符前面会出现(venv)标记,说明当前已经进入虚拟环境。
2.2 安装 Python 依赖
JobRadar 的核心流程会涉及 HTTP 请求、Web 解析和 JSON 处理。为了演示方便,我们使用以下几个库:
requests:请求职位源和调用本地 LLM 接口;feedparser:读取 RSS 格式职位源;beautifulsoup4:清洗 HTML 文本;python-dotenv:管理环境变量;rich:让命令行输出结果更好看(可选)。
安装命令如下:
pip install requests feedparser beautifulsoup4 python-dotenv rich如果你的环境中已经安装了较新版本的 Python,这些库都能直接兼容,不需要额外处理。
2.3 准备本地 LLM 推理服务
JobRadar 使用本地 LLM 评分,通常依赖一个本地推理服务。目前社区里比较推荐的方式是安装 Ollama,它能把模型下载、启动、调用这几个环节封装得非常简单。
安装 Ollama 之后,拉取一个合适的模型。以qwen2.5:7b为例:
ollama pull qwen2.5:7b这个模型的大小大约在 5GB 左右,下载时间取决于网络条件。下载完成后,可以用下面这行命令确认模型能正常输出:
ollama run qwen2.5:7b "用一句话介绍 Python"如果本地机器内存不太够,可以考虑更小的模型,比如qwen2.5:3b或者llama3.2:3b。评分任务对模型推理能力有一定要求,但不需要模型像代码生成那样能力强。在实际项目里,7B 参数模型和 3B 参数模型的效果差距并不会特别夸张,关键还是 Prompt 设计。
如果你的设备没有 GPU,也不要紧。CPU 也能跑,只是推理速度会慢一些。在只有 CPU 的环境下,建议优先选择 3B 甚至更小的量化模型。
3. 核心原理:LLM 是如何给招聘信息评分的
3.1 整个 agent 的工作流程
在写代码之前,先把 JobRadar 的完整工作流程画在脑子里。
每天定时任务启动后,JobRadar 先从职位源抓取原始数据。职位源可以是招聘网站提供的 RSS、公开 API,也可以是自己维护的职位列表文件。拿到原始数据后,先做字段抽取和清洗,把混杂在 HTML 里的描述文本清理干净。
接下来是核心环节:把每一条职位信息和你的“求职偏好”拼接成 Prompt,发送给本地 LLM,让它输出一份 JSON 格式的评分结果。JSON 里包含技能匹配度、薪资竞争力、地点匹配度、成长潜力、综合分和推荐理由。
拿到所有职位的评分后,程序按综合分从高到低排序,然后格式化输出到终端,或者通过 Webhook 推送到企业微信、钉钉、飞书等平台。
整个链路看起来不复杂,但有几个关键细节会直接影响效果。下面逐一展开。
3.2 评分维度设计
评分维度是整个系统里最重要的一环。如果维度设计得不好,LLM 给出的分数就失去了参考意义。
我在这里设计了五个维度:
skill_match:JD 中的技术栈与你的技能匹配程度,满分 100;salary_level:薪资在当前市场环境的竞争力,满分 100;location_fit:办公地点与远程偏好匹配程度,满分 100;growth_potential:岗位的发展空间、业务方向、技术深度,满分 100;overall_score:综合分,由前四个维度加权计算,满分 100。
为什么需要多维评分而不是直接让模型给一个综合分?因为多维评分更可解释。当你看到一条职位综合分不高时,扫一眼各个维度就能知道是薪资不行,还是地点不合适。
在 Prompt 里,我会提供“候选人画像”,也就是你自己当前的技能栈、期望薪资、偏好城市、工作年限等信息。JobRadar 的配置文件里通常会有一个关于candidate_profile的配置块,原理是一样的。
3.3 让 LLM 输出结构化 JSON
LLM 应用开发里一个老生常谈的难点,就是怎么让模型稳定输出可解析的结构化数据。
很多初学者会直接让模型“返回 JSON”,然后发现模型经常在 JSON 前后加解释文字,或者把json.loads直接搞崩溃。JobRadar 这种评分任务对输出格式的要求很高,因为程序必须从模型输出里提取分数,再用于排序。
稳定输出 JSON 的方法通常有三个:
- 在系统 Prompt 中明确指定“只输出 JSON,不要任何解释文字”;
- 在 Prompt 中给出 JSON 结构样例;
- 在代码中增加兜底解析,用正则从模型输出里提取 JSON 片段。
本地小模型的能力弱于云端大模型,所以第三点尤其重要。即使模型偶尔输出了一些额外内容,程序也能尽量提取到有效 JSON。
下面是两个简单的对比。
不推荐的 Prompt 写法:
请评估这个职位,给出分数。推荐的 Prompt 写法:
你是一位资深招聘顾问。请根据职位信息和候选人画像,对职位进行评分。 评分维度说明: - skill_match:0-100 整数,技能匹配度; - salary_level:0-100 整数,薪资竞争力; - location_fit:0-100 整数,地点匹配度; - growth_potential:0-100 整数,成长潜力; - overall_score:0-100 整数,综合评分。 只允许输出 JSON,不要输出任何解释文字。JSON 结构如下: {"skill_match": 80, "salary_level": 70, "location_fit": 90, "growth_potential": 75, "overall_score": 79, "short_reason": "一句话推荐理由"}后面的示例代码会沿用这个思路。
3.4 本地 LLM 的接口调用方式
Ollama 提供了 OpenAI 兼容的 API 接口。也就是说,我们不需要引入额外的 SDK,直接用requests就能请求。
假设 Ollama 运行在本机,默认端口是 11434,那么接口地址是:
http://localhost:11434/v1/chat/completions请求体里的model字段写模型名称,messages里传系统提示词和用户内容,和 OpenAI 的格式保持一致。这种兼容设计对 agent 开发来说非常友好,意味着同一套代码,既可以用本地 Ollama,也可以切换到任何兼容 OpenAI 协议的服务。
4. 完整实战:从配置文件到第一轮评分
下面进入完整实战环节。我会把 JobRadar 的核心链路拆成几个文件来演示。
为了不依赖某个具体招聘网站,这里示例使用一个本地 JSON 文件作为职位数据源。实际项目中,你可以把load_listings函数替换为 RSS 解析、API 请求或网页爬虫。
4.1 创建项目结构
建议按下面的目录结构组织项目:
jobradar-demo/ ├── config.yaml ├── jobradar_demo.py ├── sample_listings.json ├── requirements.txt ├── logs/ │ └── run.log └── venv/config.yaml保存配置,jobradar_demo.py是主程序,sample_listings.json是示例职位数据,logs目录存放运行日志。
4.2 编写配置文件
创建一个config.yaml:
# 文件路径:config.yaml job_search: candidate_profile: | 候选人是一名 Python 后端开发工程师,熟悉 Python、FastAPI、Django、PostgreSQL、Redis。 期望薪资 25K-40K,偏好远程或北京。 工作年限 3 年,期望岗位方向为后端开发或 AI 应用开发。 llm: provider: "ollama" base_url: "http://localhost:11434/v1" model: "qwen2.5:7b" temperature: 0.2 scoring: # 综合分权重,四个维度相加应为 100 weight_skill_match: 40 weight_salary: 25 weight_location: 15 weight_growth: 20 notify: enabled: false webhook_url: ""配置项里的candidate_profile就是候选人画像,会直接拼进 Prompt。temperature设置为 0.2,可以适当降低模型输出的随机性,让评分更稳定。
scoring下面的权重用于最终计算overall_score。这里由执行综合分加权计算加权计算,而不是完全依赖模型自己输出综合分,是一个更可控的做法。
4.3 准备示例职位数据
创建sample_listings.json:
[ { "id": "job-001", "title": "Python 后端开发工程师", "company": "某科技公司", "location": "远程", "salary": "25K-40K", "tags": ["Python", "Django", "PostgreSQL", "Redis"], "description": "负责核心业务服务的设计与开发,参与系统性能优化。要求熟悉 Python 后端技术栈,有分布式系统经验者优先。", "url": "https://example.com/jobs/001" }, { "id": "job-002", "title": "AI 应用开发工程师", "company": "某 AI 创业公司", "location": "北京", "salary": "30K-50K", "tags": ["Python", "LLM", "FastAPI", "向量数据库"], "description": "参与 LLM 应用开发,负责 Agent 服务链路搭建和 Prompt 工程优化。要求熟悉 Python 和常见 LLM API,了解 RAG 流程。", "url": "https://example.com/jobs/002" }, { "id": "job-003", "title": "Java 后端工程师", "company": "某大型互联网公司", "location": "上海", "salary": "20K-35K", "tags": ["Java", "Spring", "MySQL"], "description": "负责内部系统开发,要求 3 年以上 Java 开发经验,熟悉 Spring 生态。", "url": "https://example.com/jobs/003" } ]这三条职位数据分别代表“高匹配度”“匹配但方向偏 AI”“技能不匹配”三种典型情况,方便我们观察评分结果是否符合直觉。
4.4 编写主程序
下面创建jobradar_demo.py。为了让代码清晰,我把整个流程拆成几个函数:
load_config:读取 YAML 配置;load_listings:加载职位原始数据;build_prompt:根据职位信息和候选人画像构造 Prompt;score_with_llm:调用本地 LLM,返回 JSON 评分结果;parse_score:从模型输出中解析 JSON;compute_overall:按权重计算综合分;main:串联整个流程。
完整代码如下:
# 文件路径:jobradar_demo.py import json import re import sys from typing import Any import requests import yaml try: from rich.console import Console from rich.table import Table except ImportError: Console = None def load_config(path: str = "config.yaml") -> dict: """读取 YAML 配置文件。""" with open(path, "r", encoding="utf-8") as f: return yaml.safe_load(f) def load_listings(path: str = "sample_listings.json") -> list[dict]: """加载职位数据。""" with open(path, "r", encoding="utf-8") as f: return json.load(f) def build_prompt(candidate_profile: str, listing: dict) -> str: """根据候选人画像和职位信息构造 LLM 评分 Prompt。""" return f""" 你是一位经验丰富的招聘顾问。请根据候选人画像和职位信息,对职位进行评分。 候选人画像: {candidate_profile} 职位信息: - 职位名称:{listing['title']} - 公司:{listing['company']} - 地点:{listing['location']} - 薪资:{listing['salary']} - 技能标签:{'、'.join(listing['tags'])} - 职位描述:{listing['description']} 评分维度(均为 0-100 整数): - skill_match:技能匹配度; - salary_level:薪资竞争力; - location_fit:地点匹配度; - growth_potential:成长潜力; - overall_score:综合评分。 只允许输出 JSON,不要输出任何解释文字。JSON 格式如下: {{"skill_match": 80, "salary_level": 70, "location_fit": 90, "growth_potential": 75, "overall_score": 79, "short_reason": "一句话推荐理由"}} """.strip() def score_with_llm(config: dict, listing: dict) -> dict: """ 调用 OpenAI 兼容接口的本地 LLM,获取评分结果。 """ llm_cfg = config["llm"] url = f"{llm_cfg['base_url']}/chat/completions" prompt = build_prompt( config["job_search"]["candidate_profile"], listing ) payload = { "model": llm_cfg["model"], "messages": [ {"role": "system", "content": "你是一个严谨的招聘评分助手。"}, {"role": "user", "content": prompt}, ], "temperature": llm_cfg.get("temperature", 0.2), } resp = requests.post(url, json=payload, timeout=120) resp.raise_for_status() data = resp.json() content = data["choices"][0]["message"]["content"] return parse_score(content) def parse_score(content: str) -> dict: """ 从模型输出中解析 JSON。 如果模型在 JSON 前后添加了额外内容,先用正则提取 JSON 片段。 """ # 常见情况:模型直接输出 JSON,可能被 ```json 包裹 json_match = re.search(r"\{.*\}", content, re.S) if not json_match: raise ValueError(f"无法从模型输出中解析 JSON:{content}") raw = json_match.group(0) try: result = json.loads(raw) except json.JSONDecodeError: # 有些模型会输出带注释或多余逗号的 JSON,这里做简单清洗 raw = re.sub(r",\s*([}\]])", r"\1", raw) raw = re.sub(r"//.*", "", raw) result = json.loads(raw) return result def compute_overall(score: dict, scoring_cfg: dict) -> float: """根据配置权重计算综合分。""" weights = scoring_cfg total = ( score["skill_match"] * weights["weight_skill_match"] + score["salary_level"] * weights["weight_salary"] + score["location_fit"] * weights["weight_location"] + score["growth_potential"] * weights["weight_growth"] ) / 100.0 return round(total, 1) def main() -> None: config = load_config() listings = load_listings() scoring_cfg = config["scoring"] results = [] for idx, listing in enumerate(listings, start=1): print(f"[{idx}/{len(listings)}] 正在评分:{listing['title']} - {listing['company']}") try: score = score_with_llm(config, listing) score["overall_score"] = compute_overall(score, scoring_cfg) score["id"] = listing["id"] score["title"] = listing["title"] score["company"] = listing["company"] score["salary"] = listing["salary"] score["location"] = listing["location"] score["url"] = listing["url"] results.append(score) except Exception as e: print(f"职位 {listing['id']} 评分失败:{e}", file=sys.stderr) results.sort(key=lambda x: x["overall_score"], reverse=True) # 输出排序结果 if Console is not None: table = Table(title="JobRadar 评分结果") table.add_column("排名", justify="right") table.add_column("职位", style="cyan") table.add_column("公司", style="magenta") table.add_column("薪资", justify="right") table.add_column("地点") table.add_column("综合分", justify="right") table.add_column("推荐理由") for i, r in enumerate(results, start=1): table.add_row( str(i), r["title"], r["company"], r["salary"], r["location"], str(r["overall_score"]), r.get("short_reason", ""), ) Console().print(table) else: for i, r in enumerate(results, start=1): print(f"{i}. {r['title']} | {r['company']} | {r['overall_score']}分 | {r.get('short_reason', '')}") if __name__ == "__main__": main()这段代码里有两个关键点值得强调。
第一,parse_score函数做了容错处理。本地小模型经常会在 JSON 输出前后加类似于“以下是评分结果:”这样的文字,或者把 JSON 包在 Markdown 代码块里。通过正则\{.*\}提取 JSON 片段,可以大幅提高解析成功率。
第二,overall_score不是完全信任模型输出,而是用四个维度的分数按权重加权计算。这样即使模型对综合分的理解有偏差,最终排序依然有相对稳定的参考价值。
4.5 运行与验证
先确保 Ollama 服务正在运行:
ollama serve启动后另一个终端里确认模型可用:
ollama list然后运行主程序:
python jobradar_demo.py如果一切正常,你会看到类似下面的输出:
[1/3] 正在评分:Python 后端开发工程师 - 某科技公司 [2/3] 正在评分:AI 应用开发工程师 - 某 AI 创业公司 [3/3] 正在评分:Java 后端工程师 - 某大型互联网公司然后输出一张排序后的结果表格。如果你用的是rich,表格会以彩色方式展示;如果没有安装rich,则输出纯文本排名。
对于上面三条示例数据,预期结果应该是:AI 应用开发工程师和Python 后端开发工程师的评分比较高,Java 后端工程师的评分明显偏低。这样我们就能直观看到,本地 LLM 通过语义理解,给出了符合直觉的排序结果。
4.6 定时运行
JobRadar 的价值在于持续运行。每天早上跑一次,推送当天结果,比手动刷招聘网站要高效很多。
Linux 或 macOS 下可以使用crontab -e添加定时任务:
0 8 * * * cd /home/user/jobradar-demo && /home/user/jobradar-demo/venv/bin/python jobradar_demo.py >> logs/run.log 2>&1这行配置表示每天早上 8 点执行一次评分。注意要使用虚拟环境里的 Python 绝对路径,避免 cron 环境找不到依赖。
Windows 下可以使用“任务计划程序”来添加定时任务,触发条件设置为每天 8 点,启动程序选择venv\Scripts\python.exe,参数传jobradar_demo.py,起始目录设置为项目目录。
5. 常见问题与排查思路
自己在跑 JobRadar 或者类似的 agent 项目时,比较容易遇到下面几类问题。我把它们的现象、原因和解决思路整理成了一张表。
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 调用本地 LLM 时进程被 kill,或者报内存不足 | 模型参数量超过物理内存,本地环境跑不动 | 更换更小的模型(3B、1.5B),使用量化版本,关闭其他占用内存的程序 |
| 模型返回的内容不是 JSON,程序报 JSONDecodeError | 小模型指令遵循能力较弱 | 在 Prompt 中增加 JSON 示例;使用正则提取 JSON 片段;在代码里做二次清洗 |
| 评分结果全部偏高或全部偏低 | Prompt 缺少评分标定标准 | 在 Prompt 里加入评分样例,比如“80 分表示匹配度很高,60 分表示匹配度一般” |
| 评分结果每次都不一样 | temperature设置太高 | 降低temperature,比如 0.2 或 0.1;如果稳定性仍不够,可以让模型先输出分析再给分 |
| 爬取职位源时被目标网站限制或封禁 | 请求频率过高、缺少 User-Agent | 降低请求频率,加入随机延迟,设置合理 UA,优先使用官方 API 或 RSS 作为数据源 |
| Webhook 通知没有触发 | 通知地址配置错误,或者网络不通 | 先用curl手动测试 Webhook 地址,确认能收到消息后再看主程序日志 |
启动时提示找不到yaml模块 | 没有安装 PyYAML | 执行pip install pyyaml;如果使用虚拟环境,检查是否激活了正确的虚拟环境 |
除了这些常规问题,还有两个比较隐蔽的坑,我在实际使用中感受很深。
第一个坑是模型输出里的 JSON 字段不完整。比如模型只返回了skill_match和salary_level,漏掉了其余字段。这种情况下直接访问score["location_fit"]会抛KeyError。处理方式是在解析后做一次字段完整性校验,缺失的字段默认给 0 分。为了稳妥,上面的示例代码可以再补一段:
default_score = { "skill_match": 0, "salary_level": 0, "location_fit": 0, "growth_potential": 0, "overall_score": 0, "short_reason": "", } default_score.update(result)第二个坑是本地模型对“薪资竞争力”的判断容易失真。因为模型训练数据里的薪资分布和当前市场有差异,所以它给出的薪资分仅供参考。如果你想更精确,可以把薪资分数单独放到代码里做规则判断,比如根据一个期望薪资区间,用脚本直接计算salary_level,而不是完全依赖 LLM。这种“规则 + LLM”混合的方式,在 agent 工程里很常见。
6. 最佳实践与工程化建议
6.1 数据源合规与请求控制
JobRadar 涉及职位数据的获取。无论是通过爬虫、RSS 还是公开 API,都要注意目标网站的服务条款和 robots 文件。不要高频请求,不要对同一个网站造成压力。合理的方式是设置请求间隔,例如每抓取一条数据后随机等待 1 到 3 秒。
如果数据源提供官方 API,优先使用官方 API。RSS 也是很好的选择,它既能拿到结构化数据,又对目标服务器友好。需要抓取 HTML 页面时,尽量使用beautifulsoup4做定向解析,而不是简单粗暴地正则匹配整页。
6.2 结构化输出与降级策略
LLM 应用开发里,模型输出不稳定是常态。JobRadar 这类系统必须对模型输出做严格的结构化处理。
推荐做法是:
- 在 Prompt 里给出 JSON 示例;
- 在代码里用正则提取 JSON 片段;
- 解析成功后做字段校验;
- 解析失败时记录原始输出到日志,方便回溯改进 Prompt。
需要注意,不要把“解析失败”直接当成任务失败。更好的策略是降级:如果模型这次没输出合法 JSON,可以重试一次,或者把这条职位标记为“待人工检查”,而不是直接丢弃。
6.3 使用缓存避免重复计算
职位数据每天都在变化,但变化幅度通常不会很大。如果每天跑一次全量评分,会产生很多重复的 LLM 推理调用,浪费计算资源。
工程上可以引入一个简单的缓存机制:以职位 ID 为 Key,把当天的评分结果缓存到本地 SQLite 或 JSON 文件。第二次运行时,如果职位没有任何变化,就直接读取缓存,不再调用 LLM。只有新职位或者信息发生变化的职位才需要重新评分。这个思路对降低资源消耗非常有效。
6.4 Prompt 版本管理
Prompt 是会持续迭代的。你可能会调整评分维度、修改候选人画像说明、调整输出格式要求。如果不做版本管理,每次修改 Prompt 后很难判断效果变化是因为模型问题还是 Prompt 问题。
建议把 Prompt 模板放到独立的文件中,例如prompts/sys_prompt.txt和prompts/user_template.txt,并使用 Python 的string.Template或 Jinja2 来渲染变量。这样修改 Prompt 时不需要修改代码,还可以很方便地用 Git 追踪每次变更。
6.5 日志与可观测性
定时任务最怕“静默失败”。如果某天运行出了问题,你希望能在日志里快速看到原因。
建议至少记录以下内容:
- 本次运行时间;
- 拉取到多少条职位数据;
- 成功评分多少条、失败多少条;
- 失败的职位 ID 和错误原因;
- 模型输出无法解析时的原始响应内容。
日志文件建议按天切分,避免单个文件无限增长。命令上可以这样追加到日志文件:
python jobradar_demo.py >> logs/run_$(date +\%Y\%m\%d).log 2>&16.6 模型选型与硬件评估
本地 LLM 的模型选型,需要结合你的机器配置和评分质量要求来判断。
一般来说,16GB 内存的机器跑 7B 量化模型比较稳妥;8GB 内存可以跑 3B 模型;如果内存仅有 4GB 左右,建议使用 1.5B 模型,或者考虑纯规则评分。
评分任务并不需要很强的逻辑推理,所以不必一味追求大模型。关键是 Prompt 设计要清晰,结构化输出要稳定。我用 3B 模型做简单评分排序时,效果也是可用的,只是偶尔需要解析容错。
6.7 agent 安全与权限边界
最近很多关于 LLM agent 的讨论都会涉及一个安全话题:如果 LLM 有了执行动作的能力,比如发邮件、操作数据库,它会不会因为幻觉或错误推理做出危险行为?这也是“exploiting LLM APIs with excessive agency”这类问题被频繁讨论的原因。
JobRadar 的 Agent 设计相对收敛:LLM 只负责“读”和“评”,不负责“写”和“执行”。即使模型输出错误的评分,最坏的结果也只是排序不准,不会造成破坏性影响。这是很合理的安全边界。
如果你打算在 JobRadar 基础上扩展功能,比如自动投递简历、自动发送邮件,一定要在代码层面做额外的人工确认步骤,绝不能把外发动作直接交给模型。权限最小化原则在 agent 开发里永远适用。
6.8 配置管理
config.yaml里不要写密钥类敏感信息。如果通知渠道需要 Webhook 地址或 Token,建议通过环境变量注入,并在代码中使用os.getenv读取。例如:
export JOBRADAR_WEBHOOK_URL="https://example.com/hook"然后在 Python 中:
import os webhook_url = os.getenv("JOBRADAR_WEBHOOK_URL", "")把配置和密钥分开,既方便不同环境之间切换,也避免代码仓库泄露敏感信息。
7. 总结与下一步学习路线
到这一步,你已经完整走通了 JobRadar 的核心链路:从职位数据源读取信息,构造候选人画像和职位描述组成的 Prompt,调用本地 LLM 得到多维评分,再按权重计算综合分并排序输出。
这个项目看似只是一个求职工具,但它其实覆盖了 agent 开发的几个核心能力:任务拆解、工具调用、LLM 结构化输出、容错处理、定时调度、配置管理。如果你想深入学习 agent 开发,把 JobRadar 当作练手项目非常合适,因为它足够小,但完整度很高。
下一步你可以从几个方向继续深入:
- Prompt Engineering:尝试改进评分 Prompt,增加评分样例,对比不同写法的稳定性;
- RAG 能力增强:把岗位描述切分后做向量检索,让模型基于“候选人的历史项目经历”做更精准的匹配;
- Function Calling:让模型具备调用检索工具的能力,先判断职位是否需要深入了解,再决定是否调用详情接口;
- 工程化部署:用 Docker 封装运行环境,把 JobRadar 部署到家里的 NAS 或云服务器上,配合定时任务长期运行;
- 多数据源扩展:接入多个职位源,设计统一的数据模型,让不同来源的数据都能进入同一套评分管线。
如果你现在准备动手,建议从一台至少 16GB 内存的开发机开始,先装好 Ollama 拉一个 7B 模型,再把文中的示例代码跑通。你会发现,本地 LLM 驱动的 agent 应用,并没有想象中那么复杂。
如果这篇文章对你有帮助,欢迎收藏备用。遇到部署问题,也欢迎在评论区交流你的报错信息和解决过程。