职位搜索的排序做完了怎么证明它排得好这是招聘平台、猎头系统、搜索推荐团队里绕不开的老问题。难点在于职位搜索的 query 背后是复杂的用户意图既要关键词相关又要技能匹配还要考虑工作地点、经验年限、薪资范围这些结构化条件。传统做法是拉一批标注团队手动打分用 NDCG、MRR 算离线指标但标注周期长、成本高而且不同标注员的口径很难完全一致排错的原因也没法逐条解释。LLM judge 是另一种思路把排序结果直接交给大语言模型让它按照预先定义的评估维度逐条打分再聚合成排序质量分。它不需要大规模人工标注评估维度可以按业务自定义批量跑完之后还能和人工打标结果做一致性校验。这篇文章就围绕这套方案展开从一个可落地的评估框架出发依次给出数据格式、Prompt 模板、单条评估代码、批量任务实现和排序质量聚合指标。整套方案不绑定具体模型厂商只要模型服务支持 OpenAI 兼容的接口格式就能接入现有评测流程。先说明一点这不是某个特定开源仓库的使用教程而是一套通用的LLM judge 评估排序工程实践。你可以在自己团队的搜索评测流水线里直接改造使用。1. 核心能力速览先把这套方案的关键属性列出来方便你快速判断适不适合自己的场景。能力项说明评估任务职位搜索 query 对应的排序结果质量评估LLM 角色Judge 评分器输出维度分、每个职位的总体分和评估理由评估维度相关性、技能匹配、经验适配、地点适配等可按业务增减所需硬件无特殊要求调用 LLM API 或内网模型服务即可不需要本地 GPU支持批量任务支持JSONL 输入、并发调用、失败重试输出形式JSONL 原始结果 聚合指标平均分、位置质量曲线、LLM-NDCG与传统指标关系可与 NDCG、MRR 并行使用也可把 LLM 分数当作相关性标签计算 nDCG人工对齐支持与人工标注计算 Cohens Kappa、Spearman 相关系数适合场景排序模型离线回归、搜索体验监控、A/B 实验辅助、人工评估抽样复核这套方案的核心优势有三个第一评估口径稳定同一套 Prompt 在低温参数下可以重复复现第二可解释性强LLM 会输出理由定位问题 case 时不用猜第三成本低于全量人工标注适合排序迭代过程中的高频回归测试。2. 为什么用 LLM judge 评估职位搜索排序传统搜索排序评估主要靠两类手段。一类是离线指标比如 NDCG、MRR、RecallK这些指标依赖人工标注的相关性标签标注成本高更新慢另一类是线上指标比如点击率、转化率、停留时长但线上指标受位置偏差影响大第一名的点击率高可能只是因为位置靠前并不代表排序真的合理。职位搜索场景还有一个特殊问题排序结果的正确性不只看关键词相关性还看候选人与职位之间的供需匹配度。比如用户搜索Java 后端开发工程师一个标题里同时包含 Java 和 Go 的岗位关键词相关度很高但它要求 5 年以上大规模分布式系统经验而候选人是 2 年经验的校招生这个岗位排在前面就是有问题的。这类判断传统标注员需要看完整职位描述才能做成本非常高。LLM judge 在解决这个问题上有几个明显优点评估维度可编程可以在 Prompt 里明确要求模型同时评估关键词相关性技能重叠度经验年限适配地点适配输出结构化的维度分数。理由可追溯每个分数后面跟着一句自然语言理由开发人员可以直接从理由里看出排序问题的具体原因比如该岗位要求 5 年以上 Kafka 经验候选人技能列表中无 Kafka。一致性稳定把 temperature 设为 0同一输入基本能得到相同评分。即使需要更高稳定性也可以多次采样取平均分。扩展成本低新增一个评估维度只改 Prompt 和输出 schema不需要重新培训标注团队。但也要明确边界。LLM judge 不能完全替代人工评估尤其在两类场景下要谨慎一是涉及招聘决策的高风险岗位比如高管、法务、医疗岗位最终筛选结果必须有人工复核二是评估数据中包含真实候选人简历、联系方式时直接把 PII 数据发给外部 API 存在隐私风险需要脱敏或使用内网模型服务。3. LLM judge 评估体系设计评估体系是整个方案的地基。Prompt 设计得好不好直接影响评分质量和稳定性。建议先想清楚三件事评估维度、评分标准、输出格式。3.1 评估维度职位搜索排序的核心评估维度不需要太多控制在五个以内会让模型输出更稳定。推荐一组初始维度维度说明常见判断依据relevance搜索 query 与职位标题、描述的相关性关键词命中、语义相似度skill_match候选人的技能与职位要求技能的匹配程度必需技能、加分技能、技能缺失experience_fit候选人的工作年限、职级与职位要求是否匹配年限范围、职级要求、项目复杂度location_fit候选人的所在地与工作地点是否匹配当前城市、是否接受远程、是否标注可搬迁如果需要可以增加薪资匹配公司规模偏好等维度。但建议分阶段增加先用 4 个维度跑通再根据实际失败 case 决定是否添加。维度过多时模型容易顾此失彼输出质量会下降。每个维度都需要一个明确的操作性定义。比如 skill_match不是简单数一下技能重合个数而是要区分职位要求中明确列为必需的技能和加分项技能。这个规则要写进 Prompt。3.2 评分标准每个职位按 0 到 4 分五档打分。具体定义建议如下分数含义判断标准4完全匹配核心条件和多数加分条件都满足排在该位置合理3大部分匹配主要条件满足有少量不一致但影响不大2部分匹配一部分条件满足存在明显不满足项1弱匹配少数条件满足整体匹配度差0完全不匹配属于误召回明显不应出现在此 query 下评分标准要在 Prompt 中完整描述否则模型可能打出各种奇怪的中间值。3.3 评估 Prompt 模板这里给出一套可以直接使用的 Prompt 模板。系统 Prompt 负责定义角色、评分标准、输出 JSON schema用户消息携带具体的 query、候选人画像和职位排序列表。JUDGE_SYSTEM_PROMPT 你是一个职位搜索排序质量评估专家。你的任务是评估一组排序结果对给定搜索意图的匹配质量。 ## 评估流程 1. 阅读用户提供的搜索 query、候选人画像和按当前排序输出的职位列表。 2. 先概括搜索意图。 3. 对职位列表中的每个职位逐一评分必须使用 rank 字段标识职位顺序。 4. 输出 JSON不要输出额外解释。 ## 评分维度 - relevancequery 与职位标题/描述的语义相关性关键词命中程度。 - skill_match候选人技能与职位要求技能的匹配程度重点看职位必需技能。 - experience_fit候选人经验年限与职位要求的匹配程度。 - location_fit候选人所在地与工作地点的匹配程度若职位支持远程需充分考虑。 ## 评分标准每个维度 - 4完全满足无瑕疵 - 3基本满足有少量不足 - 2部分满足存在明显不满足项 - 1少数满足整体匹配度差 - 0完全不满足 ## output JSON schema { intent_summary: 一句话概括搜索意图, job_scores: [ { rank: 1, overall_score: 0, dimension_scores: { relevance: 0, skill_match: 0, experience_fit: 0, location_fit: 0 }, reason: 一句话说明给分理由必须引用职位或候选人信息 } ], list_feedback: 对整个排序列表的总体评价指出头部排序是否合理 } ## 注意事项 1. 只根据提供的职位信息和候选人画像做判断禁止编造职位描述中不存在的条件。 2. 如果某个维度信息不足该维度给 2 分并在 reason 中标注信息不足。 3. job_scores 数组必须覆盖列表中的每个职位缺失一个 rank 都算失败。 用户消息部分按实际数据组装。这里的关键是让模型看到完整的上下文query、候选人画像、职位列表。职位描述建议截断到 500 字以内的摘要避免 token 超限也避免模型被冗长描述干扰。4. 数据准备与预处理4.1 输入数据格式推荐使用 JSONL 文件一行一个评估 case。每个 case 包含 query、候选人画像和当前排序的职位列表。示例如下{ case_id: case_0001, query: Java 后端开发工程师 上海 3-5年, candidate_profile: { years_of_experience: 4, current_city: 上海, skills: [Java, Spring Boot, MySQL, Redis, Kafka], expected_salary: 30K-40K }, ranked_jobs: [ { rank: 1, job_id: JOB-3341, title: Java后端开发工程师, company: 某金融科技公司, location: 上海·浦东, salary_range: 25K-40K, experience_required: 3-5年, skills: [Java, Spring Boot, MySQL, Kafka], description_snippet: 负责交易系统后端开发要求扎实的 Java 基础熟悉高并发场景有金融系统经验优先。 }, { rank: 2, job_id: JOB-2210, title: 高级Python后端开发工程师, company: 某电商公司, location: 杭州, salary_range: 35K-50K, experience_required: 5-10年, skills: [Python, Django, Go], description_snippet: 负责电商中台服务开发需要使用 Python 和 Go 进行微服务设计。 } ] }注意两个细节。第一description_snippet不要放完整的长文本建议只保留前 200 到 500 字并做截断第二rank字段是排序位置必须和线上排序输出保持一致否则后面的位置质量曲线会算错。4.2 数据清洗与脱敏职位搜索评估中可能涉及求职者和企业的敏感数据。在构造评估 case 前必须做以下处理删除候选人画像中的姓名、手机号、邮箱、身份证号等个人身份信息。公司名称如果涉及内部敏感信息可以替换为某金融科技公司这类占位。如果职位描述中包含面试官姓名、内部系统链接等内容要一并清洗。确认职位信息、简历数据具备合法使用授权后再用于评估尤其是调用外部 LLM API 时。脱敏不只是合规要求也是评测质量要求。如果模型在理由中引用了候选人姓名说明输入数据里还残留 PII会干扰后续结果分析和数据共享。5. 基础评估实现单条调用5.1 调用方式假设你的 LLM 服务提供 OpenAI 兼容的/chat/completions接口可以用 requests 直接调用。这里把 base_url 放在环境变量里方便切换外部 API 或内网模型服务。import json import os import re import time import requests LLM_BASE_URL os.getenv(LLM_BASE_URL, https://api.openai.com/v1) LLM_API_KEY os.getenv(LLM_API_KEY, ) LLM_MODEL os.getenv(LLM_MODEL, gpt-4o-mini) def ask_llm_judge(case_data: dict, retry: int 2) - dict: headers {Content-Type: application/json} if LLM_API_KEY: headers[Authorization] fBearer {LLM_API_KEY} messages [ {role: system, content: JUDGE_SYSTEM_PROMPT}, {role: user, content: json.dumps(case_data, ensure_asciiFalse)}, ] body { model: LLM_MODEL, messages: messages, temperature: 0, response_format: {type: json_object}, } url f{LLM_BASE_URL}/chat/completions last_exc None for attempt in range(retry 1): try: resp requests.post(url, headersheaders, jsonbody, timeout60) resp.raise_for_status() content resp.json()[choices][0][message][content] return parse_llm_json(content) except Exception as exc: last_exc exc print(f[LLM call failed] attempt{attempt 1}, error{exc}) time.sleep(2 ** attempt) raise last_exc两个注意点。第一temperature必须设为 0这是评分稳定性的基础第二response_format的json_object参数只有部分模型服务支持如果服务不支持就把它去掉但要增加一个健壮的 JSON 解析函数。5.2 返回结果解析模型可能输出带 Markdown 代码块的 JSON也可能在 JSON 前后夹带额外文字。所以解析函数要做两层兜底先尝试json.loads失败则用正则提取 JSON 块。def parse_llm_json(content: str) - dict: try: return json.loads(content) except json.JSONDecodeError: pass pattern r\{[\s\S]*\} match re.search(pattern, content) if match: try: return json.loads(match.group(0)) except json.JSONDecodeError as exc: raise ValueError(ffailed to parse LLM output: {content[:500]}) from exc raise ValueError(fno JSON object found in LLM output: {content[:500]})解析成功后要校验job_scores是否覆盖了输入列表中的所有 rank。缺失任何一个 rank 都说明模型输出不完整需要重试或标记失败。def validate_judge_output(case_data: dict, judge_output: dict) - bool: jobs case_data.get(ranked_jobs, []) expected_ranks {job[rank] for job in jobs} actual_ranks {item.get(rank) for item in judge_output.get(job_scores, [])} return expected_ranks actual_ranks这一步很关键。批量跑完几百个 case 后如果没做完整性校验后面聚合指标会静默出错。6. 批量评估任务框架单条评估跑通后就可以进入批量阶段。批量任务的核心是三个问题输入输出怎么组织、并发开多大、失败怎么重试。6.1 JSONL 输入输出输入文件用eval_cases.jsonl每个 case 一行输出文件用judge_results.jsonl每行对应一个 case 的评估结果。这样做的好处是可以断点续跑处理完的 case 已经落盘程序中断后不需要重跑全部数据。import random from concurrent.futures import ThreadPoolExecutor, as_completed INPUT_PATH ./data/eval_cases.jsonl OUTPUT_PATH ./outputs/judge_results.jsonl MAX_WORKERS 8 MAX_RETRY 3 def process_one_case(line_no: int, line: str) - dict: case json.loads(line) for attempt in range(MAX_RETRY): try: output ask_llm_judge(case) if not validate_judge_output(case, output): raise ValueError(fjudge output missing ranks, line{line_no}) output[case_id] case.get(case_id, line_no) output[query] case.get(query, ) return output except Exception as exc: print(f[line {line_no}] attempt {attempt 1} failed: {exc}) time.sleep(2 ** attempt random.uniform(0, 1)) return { case_id: case.get(case_id, line_no), query: case.get(query, ), error: failed after retries, job_scores: [], } def run_batch(): with open(INPUT_PATH, r, encodingutf-8) as f: lines f.readlines() with ThreadPoolExecutor(max_workersMAX_WORKERS) as pool: future_map { pool.submit(process_one_case, idx, line): idx for idx, line in enumerate(lines) } with open(OUTPUT_PATH, w, encodingutf-8) as out: for future in as_completed(future_map): result future.result() out.write(json.dumps(result, ensure_asciiFalse) \n) out.flush()6.2 并发控制与失败重试并发数不是越大越好。外部 API 普遍有限流策略并发过高会触发 429 或超时反而降低整体吞吐。建议从 4 到 8 个 worker 开始观察一段时间的成功率和耗时再逐步上调。失败重试采用指数退避策略第一次失败等 2 秒第二次等 4 秒第三次等 8 秒并加入少量随机抖动避免同一时刻大量任务同时重试。连续失败三次后该 case 写成 error 记录不阻塞整个批次。如果用的是内网部署的本地 LLM 服务例如 vLLM、Ollama 这类自建推理服务LLM 服务不要求和你跑批量评估脚本的机器放在同一台服务器上只要设置LLM_BASE_URL为内网地址即可。用 OpenAI 的 Python SDK 时也支持自定义base_url本质上只是换一个 HTTP 端点。7. 排序质量聚合指标批量评估完成后原始 JSONL 只是一堆打分结果还需要聚合出可决策的指标。推荐从三个维度看整体平均分、位置质量曲线、LLM-NDCG。7.1 整体平均分与维度得分import json import pandas as pd def load_results(path: str) - list[dict]: results [] with open(path, r, encodingutf-8) as f: for line in f: if line.strip(): results.append(json.loads(line)) return results def build_score_frame(results: list[dict]) - pd.DataFrame: rows [] for res in results: for job_score in res.get(job_scores, []): rows.append({ case_id: res.get(case_id), rank: job_score.get(rank), overall_score: job_score.get(overall_score), relevance: job_score[dimension_scores].get(relevance), skill_match: job_score[dimension_scores].get(skill_match), experience_fit: job_score[dimension_scores].get(experience_fit), location_fit: job_score[dimension_scores].get(location_fit), }) return pd.DataFrame(rows) results load_results(./outputs/judge_results.jsonl) df build_score_frame(results) print( 整体均分 ) print(df[overall_score].describe()) print( 按维度均分 ) print(df[[relevance, skill_match, experience_fit, location_fit]].mean())7.2 位置质量曲线位置质量曲线统计每个排名位置的平均分。一个合理的排序结果应该是分数随 rank 递减也就是第 1 名最高第 2 名次之依次下降。如果某个位置出现明显反弹比如第 3 名平均分高于第 2 名说明排序模型在该位置附近有系统性错误。def position_quality_curve(df: pd.DataFrame) - pd.DataFrame: curve ( df.groupby(rank)[overall_score] .agg([mean, count, std]) .reset_index() ) return curve print( 位置质量曲线 ) print(position_quality_curve(df))这条曲线在排序模型迭代中非常直观。每次替换排序模型后重新跑同样的评估 case 集合对比位置曲线变化就能快速确认头部排序是否改善。7.3 LLM-NDCG 与人工一致性可以把 LLM 对每个职位的overall_score当作相关性标签然后计算 nDCGK。这样 LLM judge 的结果可以无缝接入传统排序指标体系兼容你已有的回归流程。import math from itertools import islice def ndcg_at_k(scores: list[float], k: int 10) - float: dcg sum((2 ** s - 1) / math.log2(i 2) for i, s in enumerate(scores[:k])) ideal sum((2 ** s - 1) / math.log2(i 2) for i, s in enumerate(sorted(scores, reverseTrue)[:k])) return dcg / ideal if ideal 0 else 0.0 df_sorted df.sort_values([case_id, rank]) case_ndcg ( df_sorted.groupby(case_id)[overall_score] .apply(lambda x: ndcg_at_k(x.tolist(), k10)) ) print( LLM-NDCG10 ) print(case_ndcg.mean())与人工评估的一致性可以用 Cohens Kappa 计算。你需要准备一组人工对每个 case 的预期 top1 是否合理或每个职位是否匹配的二分类标注再和 LLM 的最高分职位对比。from sklearn.metrics import cohen_kappa_score # human_binary: 0/1 列表表示人工认为 top1 是否合理 # llm_binary: 0/1 列表表示 LLM 认为 top1 是否合理例如 top1 的 overall_score 3 记为 1 kappa cohen_kappa_score(human_binary, llm_binary) print(fCohens Kappa: {kappa:.3f})一致性达到什么水平算可用没有绝对标准。经验上 Kappa 在 0.6 以上可以认为 LLM judge 和人工评审有较好的一致性0.4 到 0.6 之间说明有参考价值但需要继续调整 Prompt低于 0.4 就要重点检查评估维度和评分标准是否偏离业务认知。8. 性能与成本观察批量评估脚本跑起来后要重点关注三个指标单 case 耗时、Token 消耗、失败率。单 case 耗时取决于输入大小和模型响应速度。一个包含 10 个职位的 case可能产生 2000 到 4000 个 Token 的输入和 1000 个 Token 左右的输出。外部 API 每个请求通常需要 1 到 3 秒。如果列表很长建议先截断 description_snippet或者限制每次评估最多打分 20 个职位超出部分拆分多次评估。Token 消耗是主要成本来源。可以在请求响应中读取 usage 字段并写入日志。resp_json resp.json() usage resp_json.get(usage, {}) print(fprompt_tokens{usage.get(prompt_tokens)}, fcompletion_tokens{usage.get(completion_tokens)})如果预算有限有几个降本手段减少每个职位的描述长度只保留 title、skills、experience_required、location 等结构化字段。先用小模型跑全量再用大模型只复核小模型打分异常或置信度低的 case。对结果做缓存同一 query 和同一职位列表的评估结果直接复用。失败率主要来自限流和超时。如果失败率超过 5%先降低并发数再检查服务端的限流策略不要盲目加大重试次数。大量超时通常说明输入 token 过大优先优化输入格式。9. 常见问题与排查方法问题现象可能原因排查方式解决方案LLM 输出不是合法 JSON模型能力不足或服务不支持 json_object 模式打印原始输出查看前后文是否有附加文字关闭 response_format改用正则提取 JSON或换更强的模型job_scores 数组缺失某些 rank职位列表过长模型漏掉部分职位打印批次的校验失败日志增大上下文、拆分职位列表、在 Prompt 中强调必须覆盖全部 rank同一 case 两次评分不一致temperature 非 0或模型本身随机性较大用相同输入跑多次记录分数分布设置 temperature0多次采样取平均分批次任务频繁失败API 限流、超时、输入 token 超出上限查看异常类型和 HTTP 状态码降低并发、指数退避重试、截断职位描述评分结果与人工评估偏差大评估维度定义不清晰或 Prompt 缺少 few-shot 示例抽样 20 个 case 对比 LLM 理由与人工理由迭代 Prompt增加正反示例细化评分标准评估耗时过长职位列表过长单请求 token 量过大查看 usage 日志和单请求耗时截断 description_snippet或限制单次评估职位数数据隐私风险输入中残留候选人姓名、手机号抽查构造的 case 文件完善脱敏流程优先使用内网模型服务位置质量曲线异常输入数据 rank 字段与线上排序不一致对比线上日志与 case 文件的 rank修正数据导出逻辑确保 rank 按最终排序输出写入10. 最佳实践与使用建议先小样本跑通再全量执行。建议先准备 50 到 100 个覆盖不同 query 类型的 case人工抽检一遍 LLM 的评分质量确认方向正确后再扩展到全量数据集。建立校准集。选取 10 到 20 个边界情况 case比如跨城市搜索、经验不足但技能匹配、职位描述含糊等情况放到 Prompt 的 few-shot 示例中帮助模型理解评估口径。保留最小可运行配置。把JUDGE_SYSTEM_PROMPT、评测维度定义、评分标准、代码脚本放到同一个目录用配置文件维护方便排序模型迭代时复用。批量任务要留日志和缓存。每个 case 的耗时、Token 用量、重试次数都要记录方便后续排查问题相同输入避免重复调用节省成本。评估完必须人工抽检。LLM judge 适合做初筛不适合做终审。每次跑完批量任务抽样 20 到 30 个结果重点看理由是否合理、评分是否符合业务直觉。注意 Prompt 注入。职位描述是外部内容可能包含忽略以上指令之类的对抗性文本。在系统 Prompt 中要明确要求模型只依据结构化字段和评分标准判断不接受职位描述中的额外指令。合规和数据授权。评估数据如果涉及候选人简历、真实求职者信息必须先做脱敏和授权确认。涉及招聘决策的环节LLM judge 的评分只能作为辅助参考不能直接替代人工判断。11. 总结与下一步这套 LLM judge 评估方案最值得试的地方是用不到人工标注十分之一的成本得到一套可解释、可复现、可按业务定制维度的排序质量评估流程。建议你先跑通单条评估确认输出 JSON 解析和完整性校验没问