Jev 案例:简历技能标签抽取(完整代码)

更新于 适用版本 Jev 1.13 示例数据(example fixture)

TL;DR: 本案例为招聘平台搭建一条简历打标流水线:第一次是多选题,把自由文本简历映射到一张固定的 12 个标准化技能标签表上;第二次是打分题,给每个提取出的技能打一个 0–100 的熟练度置信分。完整请求体、示例数据(example fixture)格式的响应、可直接运行的 Python 脚本,以及与正则/关键词匹配的对比讨论都在下文。

场景

我们给一家招聘平台做打标后端,每周大约进 4000 份简历。每份简历都要打上技能标签供招聘方筛选,而且标签必须来自一张固定词表——招聘人员搜索的是 python,不是”有 Python 数据管道经验”,标签和词表差一个词,所有筛选器里它就等于不存在。

这条流水线之前,打标靠人工。两个问题叠加在一起:成本和一致性。成本在每周 4000 份的量上很好算。一致性是更麻烦的问题——同一份简历,一个外包会把”built AWS Lambda functions”标成 aws,另一个会直接跳过,因为简历里没出现光杆的”AWS”两个字。同一份简历不同人打出的标签不一样,下游所有筛选器的数据就这样悄悄被污染了。

我们把人工打标换成了每份简历两次 Jev 调用:

  1. 多选题——从固定的 12 个标签(python、sql、aws、docker、communication 等共 12 个)里,把简历有证据支撑的标签全部选出来。这里必须用多选:单选题会逼出一个失真的”主技能”,而招聘人员会过度看重这个标签。
  2. 打分题——对每个提取出的技能,按 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。这条流水线不是替代便宜方案,而是接住便宜方案漏掉的部分。

关键参数

字段取值说明
modeltypesafe/jev-1.13固定版本号,避免标签词表行为随版本漂移。准确 slug 请以服务方为准。
messages[0](system)完整 12 标签列表 + 证据规则 + JSON 结构固定词表是整套方案的命根子——不写死,模型就会发明筛选器看不见的标签。
messages[1](user)简历原文多选题那一遍要传完整简历;摘录会藏掉证据。
answer(响应)标签数组 / 0–100 分数(示例数据)标签直接进搜索索引;分数变成展示给招聘方的熟练度。
confidence(响应)0.86 / 0.83(示例数据)在它上面做门禁:低置信度标签要么丢弃,要么进人工抽检队列。
rationale(响应)一句话出现标签争议时,不用重读简历就能说清来龙去脉。

注意事项

继续阅读