第 13 章

第 13 章:招聘与线索生成——Recruiting / Lead generation

第 13 章:招聘与线索生成——Recruiting / Lead generation

招聘和线索生成看着是两个部门的事,底层却是同一道题:面对一批海量、非结构化、质量参差的入站对象(简历、线索),在有限的人力和预算下,判断谁先看、谁可以自动淘汰、谁必须转人工。这类"漏斗前端"的判定规则写不干净,又必须被重复执行成千上万次——正是决策层的主场。本章讲清这道题怎么用类型化决策拆开。

招聘与线索:同一条漏斗上的两类判定

招聘是一条漏斗:简历投递 → 分诊/淘汰 → 与 JD 匹配 → 面试 → 反馈结构化 → 录用决策。线索生成是另一条漏斗:线索进入 → ICP 匹配 → 评分分级 → 去重与丰富 → 分配给销售跟进。两条漏斗形状完全一样——入口宽、出口窄,绝大多数对象会被淘汰或被降级,只有少数值得投入人力。

把这件事交给 chat 模型,会遇到三个具体麻烦。输出不封闭:你问"这个人合适吗",模型回你一段评价,你还得写解析器。拿不到可用概率:没有概率就没法设阈值,也就没法做"自动淘汰 / 转人工"的分流。规模不对等:每份简历、每条线索都要判一次,用生成模型等于用卡车送信。

Jev 把每次判定压缩成一个封闭的类型化问题:要不要淘汰(Noul)、匹配到什么程度(Score)、属于哪一条通道(Choice),返回带概率的决策,代码拿走概率去做分流。

flowchart TB
    A["入站简历 / 线索
大而杂,非结构化"] --> B{"Noul:先问一票否决的淘汰问题"} B -- "P ≥ 高阈值" --> X["自动淘汰"] B -- "中间地带" --> H["转人工复核"] B -- "P < 低阈值" --> C{"Choice / Score:匹配与分级"} C -- "低置信" --> H C -- "高置信" --> D["进入完整评估 / 分配跟进"] D --> E["反馈结构化
Noul / Choice / Score"] E --> F["Score 汇总排序"]

简历分诊:先问那个"淘汰问题"

jrev-resume-disqualifier(AiPersonacademy/jev-resume-disqualifier)的做法很干脆:不在 25 毫秒内判断候选人优不优秀,而是先把"有没有一票否决的硬伤"这个最便宜的问题问出来,让大部分简历在进入完整评估之前就出局。项目的定位一句话说清——"先问那个取消资格的问题,让只有幸存者才进入完整评估"(knocks a resume out of a pipeline in under 25 ms by asking the disqualifying question first)。

这是分诊的通用原则:把最便宜、最确定的判断放在最前面。绝大多数简历在有硬伤时就应该被挡掉,给它们跑一遍昂贵的完整评估是纯浪费。

from typesafe import TypesafeClient

client = TypesafeClient()

def triage(resume: str) -> str:
    d = client.systemone(
        state=resume,
        questions={
            "hard_disqualify": {
                "type": "noul",
                "question": "这份简历是否命中任一硬性淘汰条件:必备资质或经验年限不符,或存在明显造假?"
            }
        },
        instructions="你是招聘分诊员。只在证据明确时才判为淘汰;有任何疑虑一律不淘汰。"
    )
    p = d["hard_disqualify"].probability
    if p >= 0.85:
        return "reject"          # 高置信硬伤,直接出局
    if p >= 0.50:
        return "human_review"    # 边界地带,交给人
    return "full_eval"           # 进入完整评估

注意这里用了双阈值:0.85 以上才敢自动淘汰,0.50 以下放行,中间的窄带一律转人工。把不确定丢给人,比让模型硬编一个答案安全得多;而对招聘这种"错杀一个人代价很高"的场景,把自动淘汰的阈值抬高本身就是一种公平性保护。

候选人—JD 匹配:五道证据门、四个维度、一路路由

jrev-resume-screening(nanami-0713/jev-resume-screening)把"一份简历对一份 JD"的判定压进一次请求里:五个 Noul 证据门、四个 Score 维度、一个 Choice 背景路由。Noul 门只问"简历里有没有 X 的可核实证据",不做好坏判断;Score 维度在有序尺度上打分;Choice 决定这位候选人该走哪条评估通道。

这个项目最有价值的部分是它的判据硬化过程。作者用"负样本简历"反复打磨标准,版本从 v1 迭代到 v3——一张刻意包装、自称"AI heavy user"的诱饵简历,其匹配分从 0.95 被压到了 0.49。换句话说,判据的进步就是把"关键词幻觉"压下去的过程:光会堆砌名词的候选人,不该和真正做过项目的人拿到同样的分。

def match_one(resume: str, jd: str) -> dict:
    d = client.systemone(
        state=f"【JD】\n{jd}\n\n【简历】\n{resume}",
        questions={
            "has_stack":     {"type": "noul",
                "question": "简历中有与 JD 核心技术栈直接对应的项目证据(而非仅罗列关键词)?"},
            "has_ownership": {"type": "noul",
                "question": "有明确主导或负责某项目的证据('主导/负责/owner',而非'参与')?"},
            "has_scale":     {"type": "noul",
                "question": "有可核实的规模化证据(用户量、数据量、QPS、团队规模等具体数字)?"},
            "years_fit":     {"type": "score", "question": "相关经验年限的匹配度?",
                              "labels": ["不符", "偏少", "匹配", "超出"]},
            "skills_fit":    {"type": "score", "question": "技能与 JD 的匹配度?",
                              "labels": ["低", "中", "高"]},
            "seniority_fit": {"type": "score", "question": "职级匹配度?",
                              "labels": ["偏低", "略低", "匹配", "偏高"]},
            "track":         {"type": "choice", "question": "这位候选人应走哪条评估通道?",
                              "choices": ["后端", "前端", "算法", "通用", "建议淘汰"]},
        },
        instructions="你是招聘评估助手。只依据简历中可核实的证据判断,关键词堆砌不算证据。"
    )

    gates = [d[k].value for k in ("has_stack", "has_ownership", "has_scale")]
    fit = (2 * d["years_fit"].score + 2 * d["skills_fit"].score
           + d["seniority_fit"].score) / 5
    conf = [max(d[k].probabilities.values())
            for k in ("years_fit", "skills_fit", "seniority_fit")]

    if sum(gates) < 2 or fit < 2.0 or min(conf) < 0.6:
        return {"decision": "human_review", "fit": round(fit, 2), "gates": gates}
    return {"decision": d["track"].choice, "fit": round(fit, 2), "gates": gates}

这里有两个可复用的工程动作:加权聚合(多个维度分用代码按权重合成一个连续 fit 分,而不是让模型直接给总分),以及低置信升级(任一维度不确定就把整条判定转入人工,而不是拼出一个看着像样的结论)。项目原文也说得很清楚:任何低置信度的答案都会被升级到人工复核。

政策可编辑,重打分不花钱

typesafe-jev CV screener(gtaras7/typesafe-jev)针对的是另一件事:标准会变。它用一个可编辑的 policy 对一整个文件夹的简历做类型化判定,并在政策改变时免费地重新打分(re-scoring candidates for free when the policy changes)。

这引出决策层的一条工程原则:判定标准应当是数据(policy),不是藏在提示词里的手艺。当招聘要求临时加一条"必须有生产环境的 Rust 经验",你不需要重新训练、也不需要重跑昂贵的 LLM 调用,只要拿同一批简历对着新 policy 再判一次。而决策调用足够便宜,重打分的边际成本近乎为零。

POLICY = [
    {"id": "must_rust", "type": "noul",
     "question": "简历中是否有生产环境使用 Rust 的证据?", "weight": 3},
    {"id": "must_ship", "type": "noul",
     "question": "有从 0 到 1 上线并长期维护产品的证据?", "weight": 2},
    {"id": "team_lead", "type": "score",
     "question": "团队协作与带人经验的强度?",
     "labels": ["无", "协作", "带小团队", "带团队"], "weight": 1},
]

def score_against(cv: str, policy: list[dict]) -> float:
    qs = {}
    for r in policy:
        q = {"type": r["type"], "question": r["question"]}
        if "labels" in r:
            q["labels"] = r["labels"]
        qs[r["id"]] = q
    d = client.systemone(state=cv, questions=qs,
                         instructions="你是招聘筛选助手,只依据可核实证据。")
    total = wsum = 0.0
    for r in policy:
        ans = d[r["id"]]
        val = ans.probability if r["type"] == "noul" else ans.score / len(r["labels"])
        total += r["weight"] * val
        wsum += r["weight"]
    return round(total / wsum, 3)   # 归一化到 0–1

面试反馈的结构化

面试结束后,面试官留下的是一段自由文本。把它变成可比对的字段,是决策层最典型的 Structured Data Extraction 用法。这里没有单一高星项目,属于通用做法:用一两个 Choice 定档、一个 Score 定级、若干个 Noul 标记风险信号。

def structure_feedback(notes: str) -> dict:
    d = client.systemone(
        state=notes,
        questions={
            "recommend": {"type": "choice", "question": "面试官的整体建议?",
                "choices": ["强烈推荐", "推荐", "中立", "不推荐", "强烈不推荐"]},
            "coding": {"type": "score", "question": "编码能力评级?",
                "labels": ["弱", "中", "强", "很强"]},
            "risk": {"type": "noul",
                "question": "反馈中是否出现了需要升级讨论的风险信号(价值观 / 协作 / 诚信)?"},
        },
        instructions="你是面试记录结构化助手。只提取反馈中已有的事实,不要补充面试官没说的判断。"
    )
    return {
        "recommend": d["recommend"].choice,
        "recommend_conf": max(d["recommend"].probabilities.values()),
        "coding": d["coding"].score,
        "risk_flag": d["risk"].value,
        "risk_p": d["risk"].probability,
    }

线索生成:ICP 匹配与评分分级

线索这条漏斗里,第一个判定是"这条线索像不像我们的理想客户(ICP)"。jev-fit 给的思路很有借鉴意义:它把一段想法和一个固定 rubric 一次送进 Jev,让一个 Choice 决定用哪种引擎处理,并且用代码做否决——当想法涉及图像时,代码直接否决 Jev、转给别的引擎;低置信时返回"not sure"而不是硬猜。搬到线索场景,就是"信息不足时宁可标不确定,也不要给一条来历不明的线索打高分"。

第二个判定是评分分级,用 Score 把匹配度和意向强度分别量出来,再用代码合成等级:

def score_lead(lead: str) -> dict:
    d = client.systemone(
        state=lead,
        questions={
            "icp_fit": {"type": "score",
                "question": "线索与理想客户画像(行业 / 规模 / 角色)的匹配度?",
                "labels": ["完全不匹配", "弱", "中", "强", "完全匹配"]},
            "intent": {"type": "score", "question": "购买意向强度?",
                "labels": ["无", "低", "中", "高"]},
            "is_probe": {"type": "noul",
                "question": "这是否是同行用于情报收集的虚假线索?"},
        },
        instructions="你是线索评级助手。依据线索自述信息判断,信息不足时给中间档而非满分。"
    )
    if d["is_probe"].probability >= 0.7:
        return {"grade": "X", "reason": "疑似同行试探"}
    fit, intent = d["icp_fit"].score, d["intent"].score
    if fit >= 3.5 and intent >= 2.5:
        grade = "A"
    elif fit >= 2.5:
        grade = "B"
    else:
        grade = "C"
    return {"grade": grade, "icp_fit": round(fit, 2), "intent": round(intent, 2)}

grokbot-jev-jobs(mcgalleg/grokbot-jev-jobs)展示了同一种打分的批处理用法:一个每天运行的 Vercel cron,把公开招聘帖逐条对照一份简历打分,只让看着靠谱的匹配浮上来。把它倒过来用——对着一份 ICP 描述给线索池打分——就是线索分级。

去重与丰富:别把同一条线索喂两遍

线索池的另一半工作是去重。sortwell(dharun-cohere/jev-sortwell)的做法值得抄:它用一次 Jev 请求同时问出"类别 / 项目 / 下一步动作"(Choice)和一个"是不是重复"(Noul),并且规定只有在 0.70 以上、且能指出具体的那条匹配项时,才标记为重复;归类同理,只在 0.45 以上才落到某个项目。也就是说,去重不是"像就算重",而是"高置信且能指认对象才动手"。

def dedup(candidate: str, existing: list[str]) -> dict:
    d = client.systemone(
        state=f"【候选】\n{candidate}\n\n【已有线索】\n" + "\n".join(existing),
        questions={
            "duplicate": {"type": "noul", "question": "候选是否与已有线索中某一条指向同一个对象?"},
            "of_which": {"type": "choice", "question": "若重复,重复的是哪一条?",
                "choices": [f"#{i}" for i in range(len(existing))] + ["不重复"]},
        },
        instructions="你是线索去重助手。只有在能明确指出对应的那一条时才判为重复。"
    )
    if d["duplicate"].probability >= 0.70 and d["of_which"].choice != "不重复":
        return {"duplicate": True, "of": d["of_which"].choice}
    return {"duplicate": False}

批处理、阈值组合与人工兜底

把流水线真正拼起来时,有几个工程细节会反复出现,社区的这些项目正好把每一种都演了一遍:

  • 批处理:Tab Sorter(AstonyCat/jev-tab-grouper)用一次并行的 Jev 调用给窗口里每个标签页问一个 Choice("该归到哪个分组"),判不出来的统一落到一个固定的 fallback 桶,手动建的分组不碰。线索池的批量分类可以照抄这套"并行问一遍 + fallback 兜底"。
  • 多阈值组合:Jev Wrapped(gaborishka/jev-wrapped)读一个公开 Telegram 频道最多 1500 条帖文,对每条约一个 Choice(十种帖子类型)加三个 Noul(付费广告、标题党、情绪施压),并规定广告从 0.7 起计,若类型本身也是广告则降到 0.4;标题党和情绪施压从 0.5 起计,最后把得分最高的帖子链出来供人工抽检。这正是"不同判定用不同阈值 + 高分区人工抽检"的范式。
  • 不确定进 Review:Jev-Mail(vynnlee/jev-mail)在用户自己的 Google Apps Script 上跑一个常驻 Gmail 分类器,给紧急度、重要度、类别打分,把不确定或可疑的邮件一律送进 Review。
  • 敏感场景排除:Qualm(RoderickQiu/qualm)每条用户规则问一个 Choice,另加一个 Noul 判断"当前页是不是支付 / 登录 / 银行页面",只有当某条规则概率越过阈值、且页面不敏感时才弹出提醒;它用本地版 Kev 在 119 个试放页面上报告短视频、信息流、直播、视频几条规则的 precision 为 1.00。招聘/线索场景同理——涉及隐私或敏感领域时应主动退出自动判定。
  • 路由稳定性:Switchboard(ruban-24/switchboard)为每个任务选好模型后就把选择固定下来,以保住 prompt 缓存连续性。线索在 pipeline 里跨阶段流转时,也应当避免"每一步都重判、结果来回跳"。
  • 成本口径:AI-decision-maker(zlZayn/AI-decision-maker)用 Jev 把 CSV 列归到 13 个类型码、把每个数据集归到六种场景之一,所有写入都在本地执行;它实测这类逐列分类任务上 Jev 的 token 用量达到了单次 LLM 调用的 6.6–12.7 倍,原因是"输出已经压缩到一个字符,而每个问题的判定标准会在输入里重复"。这条数据提醒我们:看决策层的成本不能只看 token 数,输入里的问题定义会重复、输出几乎为零,性价比要用总账来算。

概率分流、公平性与可解释性

招聘和线索是对人的判定,所以比别处更需要把概率用对、把标准摊开。

用概率分流,而不是用布尔一刀切。 三个阈值区间——高置信自动处理、中间地带转人工、低置信自动通过——是本章所有项目共享的形状。AI-decision-maker 那边也提示,判定标准是可以写成 policy 的,重打分很便宜;jev-fit 那边则示范了"低置信就返回 not sure"。三者合起来就是一句话:把不确定性交给人,而不是让模型硬编答案。

把标准做成可评审的数据。 typesafe-jev CV screener 的 policy 是可编辑的,jev-resume-screening 的判据是被负样本反复打磨过的。一条招聘判据应该能被 HR、法务和工程一起评审,能回答"为什么这条简历被淘汰"。同时,敏感属性(性别、年龄、种族等)不应进入 state;判据要用可核实的证据门,而不是主观印象词。

保留审计痕迹。 每个决策都带回概率分布。不看 choice、而看整个分布——0.90 对 0.10 说明判得很笃定,0.35/0.30/0.20/0.15 说明这条线索本身就模糊,应该追问而不是硬分。这类分布记录使"追溯某次淘汰是怎么发生的"成为可能,也正是可在招聘里讲清公平性的前提。

小结

  • 招聘与线索是同一道题的两面:宽进窄出,绝大多数对象要被淘汰或降级,只有少数值得投人力;有两种漏斗形状和同一套判定骨架。
  • 简历分诊把最便宜的"一票否决"问题前置,jrev-resume-disqualifier 用不到 25 毫秒完成淘汰门,只有幸存者才进入完整评估。
  • 候选人—JD 匹配用"证据门(Noul)+ 维度分(Score)+ 路由(Choice)"一次调用完成;jrev-resume-screening 用负样本把判据从 v1 硬化到 v3,把诱饵简历的分数从 0.95 压到 0.49。
  • 判定标准应写成可编辑、可评审的 policy(typesafe-jev CV screener),政策一变重打分近乎免费;AI-decision-maker 提醒 token 数不等于总成本。
  • 去重要"高置信且能指认对象"才动手(sortwell:≥0.70 且指明具体匹配项);批量分类用并行调用加 fallback 桶兜底(Tab Sorter)。
  • 用双阈值把不确定性交给人:自动淘汰 / 转人工 / 自动通过;把敏感属性挡在 state 之外,保留概率分布做审计。

下一章我们从"漏斗前端"走到"漏斗后端",看客户支持系统怎么用同一套决策原语,把告警、工单、回复草拟和升级策略串成一条稳定的流水线。

📑 Jev 实战:用类型化决策重构软件

1 第 1 章:为什么需要决策层——从"生成一切"到"只做判定" 2 第 2 章:三种类型化输出——Choice、Score、Noul 与调用契约 3 第 3 章:十个决策形态——什么时候该把判断交给 Jev 4 第 4 章:Classification 与 Routing——把分诊做成一等公民 5 第 5 章:Detection 与 Scoring——二元判断与有序评分 6 第 6 章:Search 与 Retrieval——在候选集里找相关 7 第 7 章:Ranking 与 Verification——排序与校验 8 第 8 章:ML Feature Extraction 与 Structured Data Extraction——把文本变特征与结构化数据 9 第 9 章:检索与知识图谱——Search and retrieval / Graphs and knowledge graphs 10 第 10 章:模型路由与 LLM 护栏——Model routing / LLM guardrails 11 第 11 章:语义代码检查——Semantic code linting 12 第 12 章:特征提取与需求预测——Feature extraction / Demand forecasting 13 第 13 章:招聘与线索生成——Recruiting / Lead generation 14 第 14 章:客户支持——Customer support 15 第 15 章:理赔、金融犯罪与风险评估——Insurance claims / Financial crime / Risk assessment 16 第 16 章:法律与合规——Legal and compliance 17 第 17 章:电商市场与广告——E-commerce marketplaces / Advertising 18 第 18 章:内容审核与信任安全——Moderation and trust and safety 19 第 19 章:游戏——Gaming 20 第 20 章:实时应用——150 毫秒预算 21 第 21 章:科学发现——Scientific discovery 22 第 22 章:大数据上的 AI Map Reduce——为什么便宜 100 倍 23 第 23 章:通用验证——Universal Verification 24 第 24 章:Harness Engineering——把决策织进智能体骨架
← 返回本书大纲