作为应届毕业生,找工作本身就是一份全职工作。你每周要从数百个招聘信息中筛选出少数几个值得投递的。你不停地点击“快速申请”,直到眼睛酸痛。你把同一封求职信写上四十遍。到了求职的第二个月,你开始申请那些你本来不会接受的职位,进入你并不在意的行业——因为到那时,思考每个招聘信息的成本已经高于提交申请的成本了。
观看快速演示:上传一份简历,观察查询流,阅读针对每个职位的推理过程。
工作原理
一次运行包含三个步骤。
- 查询。模型会读取简历和你设定的偏好(工作类型、工作模式、地点、自由备注),然后边推理边草拟一小套类似 LinkedIn 风格的搜索查询。
- 搜索。这些查询通过 JobSpy 逐一访问 LinkedIn。
- 评分。对于每个招聘信息,模型会读取(简历,职位)配对,并写出一个五维匹配度评分:* 技能匹配度 * 经验相关性 * 教育与认证 * 行业/领域匹配度 * 资历对齐度
图 1. 该框架的端到端步骤。
你得到的不是一份包含五十个职位的列表,而是一份带有可解释推理的简短候选名单。你可以阅读模型认为排名第二的职位为何优于排名第三的理由。
技术细节
数据集构建——教师模型与学生模型
教师模型是 DeepSeek V4 Pro。擅长结构化推理,愿意遵循严格的输出格式,且成本足够低,可以离线在大规模语料库上运行一次。它被用作标签生成器,而非推理时的依赖项。
学生模型是 Qwen3-8B。在量化到 Q4_K_M 后,它足够小,可以适配到单个 ZeroGPU 切片上;同时又足够大,能够吸收教师模型的结构化判断。
语料库来自一个闭环的、感知简历的端到端流程:
- 简历。2500 份,基于 Divyaamith/Kaggle-Resume 构建。
- 查询。教师模型首先根据每份简历草拟出类似 LinkedIn 风格的搜索查询。
- 职位。然后 JobSpy 从 LinkedIn 抓取这些查询实际返回的结果。大约 10,000 个招聘信息,每一个都是由教师模型为那份特定简历所写的查询所发现的。
- 标签。随后,教师根据推理时使用的同一套五个维度,对每一对(简历,职位)结果进行评分,每个维度附带一句推理说明。
所有内容以四个外键清晰的配置形式,打包在 build-small-hackathon/job-search-distill 中。
训练(Modal)
通过 Modal 在单张 A100 上运行两次 LoRA SFT 训练,每个任务一次:
- 适配器。秩为 16,alpha 为 16,关闭 dropout,使用注意力加 MLP 投影。
- 调度。每个任务一个 epoch。每 200 步保存一次 epoch 中间检查点,以便在完整运行完成前,可以对部分运行结果进行合理性检查。
- 输出。Safetensors 格式文件位于 build-small-hackathon/job-searcher-qwen3-8B,以及一个 Q4_K_M 基础模型加 LoRA-GGUF 侧车文件位于 build-small-hackathon/job-searcher-qwen3-8B-gguf,用于 llama.cpp 服务路径。
LoraConfig(
r=16,
lora_alpha=16,
task_type="CAUSAL_LM",
target_modules=[
"q_proj", "k_proj", "v_proj", "o_proj",
"gate_proj", "up_proj", "down_proj",
],
)
Space - 推理(llama.cpp)
该 Space 在 HuggingFace ZeroGPU Space 上,使用预构建的 CUDA wheel 运行 llama-cpp-python。有两个关键的设计选择:
- 在 @spaces.GPU 内部使用 Llama。ZeroGPU 每次调用都会回收 CUDA 上下文,因此模块级实例在第二次使用时将持有失效的上下文。
- 每次提交调用一次 GPU,而非每个职位调用一次。一次提交的所有适配度评估都在单个 @spaces.GPU 调用内完成。模型加载一次,并为每个职位生成事件,而不是为每个岗位发布都支付一次全新的冷启动和代理 token 请求开销。
流式传输使用 OpenAI 风格的 create_chat_completion(stream=True),因此推理过程会逐 token 地显示在 UI 中。在线演示位于 build-small-hackathon/job-search-assistant。
追踪记录
构建此 Space 的整个 Claude Code 会话已作为 HuggingFace agent-traces 数据集发布在 build-small-hackathon/job-search-assistant-agent-trace。包含原始 JSONL 事件、原生 HuggingFace 追踪查看器,以及每一次死胡同和恢复的记录。如果你想了解这个东西实际是如何组合起来的,而不是阅读清理后的版本,这会很有用。
试试看
把你的简历放到 huggingface.co/spaces/build-small-hackathon/job-search-assistant。别再大海捞针了。
我的收获
两个适配器胜过单个。我曾尝试将查询生成和适配评估合并到单个 LoRA 中。结果模型在两种任务间泄露了格式:查询任务中混入了 JSON,评估任务中混入了散文体。将两者拆分为同一基座模型上的两个独立头,每次调用时热切换,彻底消除了这类错误。
教师模型的提示词比学生模型的规模更重要。重写教师模型的标注提示词,使其针对简历中的具体细节进行评分(例如“四年 Rust 经验;岗位要求五年”而非“技术匹配度高”),这一改进通过知识蒸馏传递了下去。学生模型也习得了同样的习惯。
Job hunting as a new grad is a full-time job by itself. You sift through hundreds of postings every week to find a handful worth applying to. You click "Easy Apply" until your eyes hurt. You write the same cover letter forty times. By month two of a search, you're applying to roles you wouldn't take, in industries you don't care about, because at that point the cost of thinking about each listing is higher than the cost of submitting to one.
Watch the short tour: drop a resume, watch the queries stream, read the per-job reasoning.
How it works
A run has three steps.
- Queries. The student reads the resume and the preferences you set (job type, work modality, location, free-form notes) and drafts a small set of LinkedIn-shaped search queries, reasoning out loud as it goes.
- Search. Those queries hit LinkedIn through JobSpy, one at a time.
- Scoring. For each posting, the model reads the
(resume, job)pair and writes a five-dimension fit score: * skills match * experience relevance * education and certifications * industry / domain fit * seniority alignment
Figure 1. End-to-end steps of the framework.
What you get back isn't a list of fifty roles. It's a small shortlist with defensible reasoning. You can read why the model thinks the second-ranked job beats the third.
Technical Details
Dataset Curation - The teacher and the student
The teacher is DeepSeek V4 Pro. Strong at structured reasoning, willing to follow a strict output schema, cheap enough to run once over a large corpus offline. It is used as a label generator, not as an inference-time dependency.
The student is Qwen3-8B. Small enough to fit on a single ZeroGPU slice once quantized to Q4_K_M, large enough to absorb the teacher's structured judgement.
The corpus came from a closed loop, resume-aware end-to-end:
- Resumes. 2,500, built on Divyaamith/Kaggle-Resume.
- Queries. The teacher first drafted LinkedIn-shaped search queries from each resume.
- Jobs. JobSpy then scraped LinkedIn for what those queries actually returned. About 10,000 postings, every one of them surfaced by a query the teacher itself wrote for that specific resume.
- Labels. The teacher then scored every resulting
(resume, job)pair across the same five dimensions used at inference, with one sentence of reasoning per dimension.
Everything ships in four foreign-key-clean configs at build-small-hackathon/job-search-distill.
Training (Modal)
Two LoRA SFT runs on a single A100 via Modal, one per task:
- Adapter. Rank 16, alpha 16, dropout off, attention plus MLP projections.
- Schedule. One epoch per task. Mid-epoch checkpoints every 200 steps so a partial run could be sanity-checked before the full one finished.
- Output. Safetensors at
build-small-hackathon/job-searcher-qwen3-8B, and a Q4_K_M base plus LoRA-GGUF sidecars atbuild-small-hackathon/job-searcher-qwen3-8B-gguffor the llama.cpp serving path.
LoraConfig(
r=16,
lora_alpha=16,
task_type="CAUSAL_LM",
target_modules=[
"q_proj", "k_proj", "v_proj", "o_proj",
"gate_proj", "up_proj", "down_proj",
],
)
The Space - Inference (llama.cpp)
The Space runs llama-cpp-python with the pre-built CUDA wheel on a HuggingFace ZeroGPU Space. Two design choices that matter:
Llamainside@spaces.GPU. ZeroGPU recycles the CUDA context per call, so a module-level instance would hold a dead context on the second use.- One GPU call per submission, not per job. All fit evaluations for one submission run inside a single
@spaces.GPUcall. The model loads once and yields events for every job, instead of paying a fresh cold start and a fresh proxy-token request per posting.
Streaming uses the OpenAI-shaped create_chat_completion(stream=True) so the reasoning lands in the UI token by token. The live demo is at build-small-hackathon/job-search-assistant.
The traces
The entire Claude Code session that built this Space is published as an HuggingFace agent-traces dataset at build-small-hackathon/job-search-assistant-agent-trace. Raw JSONL events, native HuggingFace trace viewer, every dead end and recovery on the record. Useful if you want to see how this thing actually came together rather than read the cleaned-up version of it.
Try it
Drop your resume at huggingface.co/spaces/build-small-hackathon/job-search-assistant. Stop sifting.
What I learned
Two adapters beat one. I tried folding query generation and fit evaluation into a single LoRA. The model leaked formatting both ways, JSON on the query task and prose on the eval. Splitting them into two heads on the same base, hot-swapped per call, killed the whole class of bugs.
The teacher's prompt mattered more than the student's size. Rewriting the teacher's labelling prompt to score against specific resume details ("four years of Rust; the role asks for five" instead of "strong technical match") propagated through distillation. The student picked up the same habit.