Jev 案例:简历技能标签抽取(完整代码)
TL;DR: 本案例为招聘平台搭建一条简历打标流水线:第一次是多选题,把自由文本简历映射到一张固定的 12 个标准化技能标签表上;第二次是打分题,给每个提取出的技能打一个 0–100 的熟练度置信分。完整请求体、示例数据(example fixture)格式的响应、可直接运行的 Python 脚本,以及与正则/关键词匹配的对比讨论都在下文。
场景
我们给一家招聘平台做打标后端,每周大约进 4000 份简历。每份简历都要打上技能标签供招聘方筛选,而且标签必须来自一张固定词表——招聘人员搜索的是 python,不是”有 Python 数据管道经验”,标签和词表差一个词,所有筛选器里它就等于不存在。
这条流水线之前,打标靠人工。两个问题叠加在一起:成本和一致性。成本在每周 4000 份的量上很好算。一致性是更麻烦的问题——同一份简历,一个外包会把”built AWS Lambda functions”标成 aws,另一个会直接跳过,因为简历里没出现光杆的”AWS”两个字。同一份简历不同人打出的标签不一样,下游所有筛选器的数据就这样悄悄被污染了。
我们把人工打标换成了每份简历两次 Jev 调用:
- 多选题——从固定的 12 个标签(
python、sql、aws、docker、communication等共 12 个)里,把简历有证据支撑的标签全部选出来。这里必须用多选:单选题会逼出一个失真的”主技能”,而招聘人员会过度看重这个标签。 - 打分题——对每个提取出的技能,按 0–100 打一个分数,衡量简历对”真实熟练度”(而不是随口一提)的支持程度。
Jev 是 TypeSafe AI 在 2026 年 9 月发布的 System One 判断模型。它的类型化 JSON 输出每个答案都带置信度,这才让第二次调用有意义——一个不带置信度的分数只会把一致性问题往下游推。下文的示例简历刻意用了同义词和项目描述,而不是规整的技能清单,这恰恰是关键词匹配最吃亏的地方。
请求
第一次调用——标签提取(多选题):
{
"model": "typesafe/jev-1.13",
"messages": [
{
"role": "system",
"content": "Multi-select choice question. From this fixed tag list: python, sql, aws, docker, kubernetes, react, machine-learning, data-analysis, project-management, communication, leadership, customer-service — select every tag the resume clearly supports. Do not infer tags from job titles alone; require concrete evidence in the text. Reply with JSON only: {\"answer\": [<tag>, ...], \"confidence\": <0-1>, \"rationale\": <one sentence>}"
},
{
"role": "user",
"content": "Resume: Data analyst with 4 years of experience. Built ETL pipelines in Python pulling from a Postgres warehouse, wrote daily reporting queries in SQL. Migrated our team's batch jobs to AWS (Lambda + S3) and containerized local dev with Docker. Led a 3-person analytics pod, presented findings to non-technical stakeholders every sprint. Side project: a small scikit-learn model predicting churn."
}
]
}
第二次调用——熟练度打分(打分题),每个提取出的技能一次调用:
{
"model": "typesafe/jev-1.13",
"messages": [
{
"role": "system",
"content": "Scoring question. Rate 0-100 how confidently this resume demonstrates working proficiency with the given skill — 0 means no evidence, 50 means mentioned without depth, 100 means sustained professional use with concrete outcomes. Reply with JSON only: {\"answer\": <0-100>, \"confidence\": <0-1>, \"rationale\": <one sentence>}"
},
{
"role": "user",
"content": "Skill: aws. Resume excerpt: \"Migrated our team's batch jobs to AWS (Lambda + S3).\""
}
]
}
响应
以下两个响应均为示例数据(example fixture),用于说明格式,不是线上实测抓包。
第一次调用:
{
"answer": ["python", "sql", "aws", "docker", "leadership", "communication", "machine-learning"],
"confidence": 0.86,
"rationale": "Each tag has concrete textual evidence: pipelines in Python, SQL reporting queries, Lambda/S3 migration, Docker for dev, a led pod, stakeholder presentations, and a scikit-learn project."
}
第二次调用(针对 aws):
{
"answer": 65,
"confidence": 0.83,
"rationale": "A real production migration is described, but scope is one team's batch jobs with no scale or outcome details."
}
复现脚本
保存为 skill_tags.py,填入你自己的 BASE_URL 和 API key 后运行 python3 skill_tags.py。脚本使用 OpenAI 兼容 SDK,任何 OpenAI 兼容端点都能跑:
import json, os
from openai import OpenAI
BASE_URL = "https://YOUR-ENDPOINT.example.com/v1" # replace with your provider
client = OpenAI(base_url=BASE_URL, api_key=os.environ["JEV_API_KEY"])
MODEL = "typesafe/jev-1.13" # confirm exact slug with your provider
TAGS = ("python, sql, aws, docker, kubernetes, react, machine-learning, "
"data-analysis, project-management, communication, leadership, "
"customer-service")
def ask(system, user):
res = client.chat.completions.create(
model=MODEL,
messages=[{"role": "system", "content": system},
{"role": "user", "content": user}])
return json.loads(res.choices[0].message.content)
EXTRACT = ('Multi-select choice question. From this fixed tag list: ' + TAGS +
' — select every tag the resume clearly supports. Do not infer '
'tags from job titles alone; require concrete evidence in the '
'text. Reply with JSON only: {"answer": [<tag>, ...], '
'"confidence": <0-1>, "rationale": <one sentence>}')
SCORE = ('Scoring question. Rate 0-100 how confidently this resume '
'demonstrates working proficiency with the given skill — 0 means '
'no evidence, 50 means mentioned without depth, 100 means '
'sustained professional use with concrete outcomes. Reply with '
'JSON only: {"answer": <0-100>, "confidence": <0-1>, '
'"rationale": <one sentence>}')
resume = ("Data analyst with 4 years of experience. Built ETL pipelines in "
"Python pulling from a Postgres warehouse, wrote daily reporting "
"queries in SQL. Migrated our team's batch jobs to AWS (Lambda + "
"S3) and containerized local dev with Docker. Led a 3-person "
"analytics pod, presented findings to non-technical stakeholders "
"every sprint. Side project: a small scikit-learn model "
"predicting churn.")
tags = ask(EXTRACT, "Resume: " + resume)
print(tags)
tagged = {}
for tag in tags["answer"]:
scored = ask(SCORE, "Skill: " + tag + ". Resume excerpt: " + resume)
if scored["answer"] >= 40 and scored["confidence"] >= 0.5:
tagged[tag] = scored["answer"]
print(json.dumps(tagged, indent=2))
结尾这道后置过滤(分数 ≥ 40 且置信度 ≥ 0.5)正是 12 标签固定词表发挥作用的地方:把弱证据丢掉,标签集才够小,招聘人员才会信它。
为什么不用正则或关键词匹配?
| 方案 | 能抓到 | 会漏掉 | 误标 |
|---|---|---|---|
| 关键词匹配 | Python、python3、字面 AWS | “containerized local dev with Docker” 只有硬编码 docker 才能抓到;同义词;项目描述 | 很多——“managed databases” 不等于 sql 熟练,但 database 这个词能匹配上 |
| 带同义词的正则 | 同上,外加手工列的变体 | 凡是没列进去的变体;12 个标签规模下扩展性很差 | 依然看不懂上下文 |
| Jev 两次调用 | 同义词、项目描述、隐含证据 | 少——只要 prompt 里把证据规则写清楚 | 更少——置信度分数给了你一个下手裁剪的地方 |
关键词匹配仍然值得保留作为基线:它免费、即时,能兜住那些字面命中的场景,否则这些场景也要花一次模型调用。我们的做法是先跑关键词,只把关键词阶段搞不定的简历(或标签候选)送进 Jev。这条流水线不是替代便宜方案,而是接住便宜方案漏掉的部分。
关键参数
| 字段 | 取值 | 说明 |
|---|---|---|
model | typesafe/jev-1.13 | 固定版本号,避免标签词表行为随版本漂移。准确 slug 请以服务方为准。 |
messages[0](system) | 完整 12 标签列表 + 证据规则 + JSON 结构 | 固定词表是整套方案的命根子——不写死,模型就会发明筛选器看不见的标签。 |
messages[1](user) | 简历原文 | 多选题那一遍要传完整简历;摘录会藏掉证据。 |
answer(响应) | 标签数组 / 0–100 分数(示例数据) | 标签直接进搜索索引;分数变成展示给招聘方的熟练度。 |
confidence(响应) | 0.86 / 0.83(示例数据) | 在它上面做门禁:低置信度标签要么丢弃,要么进人工抽检队列。 |
rationale(响应) | 一句话 | 出现标签争议时,不用重读简历就能说清来龙去脉。 |
注意事项
- 上面的响应均为示例数据(example fixture);字段名请与官方文档核对后再依赖。官方 API 细节在 typesafe.ai,入口是System One 模型发布公告。
- 原始 HTTP 响应是 OpenAI 兼容的 chat completion 结构;本文展示的 JSON 是从 message content 里解析出来的模型类型化输出。
- 每个提取出的标签都要一次打分调用,标签多的简历成本会翻着涨。预算紧张时,按提取置信度取前 8 个标签再送打分。
- 改动标签词表后,先对一份固定的旧简历样本重新打标并 diff 前后差异,再上线——改词表会连带改变旧标签。
- 适用于 Jev 1.13(2026 年 9 月发布),演示渠道为 OpenRouter。