项目地址:https://github.com/Xhan-985/SkillGap-Agent 60 秒演示视频(B 站):求职工具的匹配分到底怎么算的?——我做了一个每个数字都能点开看证据的开源项目
从 202 条真实 JD 到可解释的技能推荐:我开源了一个证据化 Skill Gap 分析系统
一、起因:一个查不到答案的问题
"现在学什么还来得及?"
这个问题在中文技术社区每天被问,但几乎没人能给出数据支撑的答案。不是没人想问,是没有数据源:
- 中文 AI 岗位的技能频率(比如"RAG 岗位占比多少""Agent 岗要什么技术栈"),网上查不到;想自己统计,又没有合法的采集渠道;
- 各种求职工具能给你一个"匹配度 78 分",但你问它这个分怎么算的、我该先补什么,它答不上来;
- 直接问大模型,它讲得头头是道,可你没法验证——没有样本量,没有原文出处,本质是"听起来很有道理"。
这三件事其实指向同一个问题:结论和证据被割裂了。
所以我做了一个反过来设计的工具:不问"你该学什么",只回答"市场要什么、你缺什么、每个数字从哪来"。取名叫SkillGap Agent,已经开源。
二、设计主张:把三种东西分开
这个项目最重要的一条设计纪律,是把系统里所有环节切成三层,并且严格禁止它们互相越界:
层 | 负责什么 | 禁止什么 |
Deterministic(确定性层) | 统计、评分、排序、ROI 计算——全部用 SQL 与纯函数 | 禁止出现任何 LLM 调用 |
LLM 层 | 只做两件事:从 JD 里抽取结构化技能、把结果翻译成人话 | 输出必须过结构化校验;失败就明示不可用,不许静默降级 |
Evidence(证据层) | 每个数值都携带 | 没有证据的数字不允许出现在界面上 |
为什么非要这么切?
因为最容易出问题的系统,是让大模型既算数又下结论的系统。模型一旦参与计算,结果就不可复现;模型一旦直接下结论,用户就无法验证。分层的代价是多写一些代码,换来的是:任何一个数字都能被追问到底。
三、数据从哪来:202 条真实岗位
数据是整个项目的地基。项目定义了三个采集通道,并有一条红线:不爬虫——所有数据来自人工摘录或用户主动提交,不建立在反爬对抗上。
通道 | 现状 |
公开 API 拉取(海外岗位) | 代码就绪,尚未拉取(海外市场暂为 0,页面显示灰态) |
公司官方招聘页人工摘录 | 74 条 |
招聘平台页人工摘录 | 128 条 |
用户主动贡献 / CSV 批量导入 | 通道已就绪 |
最终得到202 条 active 的中文 AI 岗位 JD,覆盖 20 个城市,分 3 批收集导入。
这里有两个我觉得值得说的设计:
第一,每条记录都带来源信息。来源类型、原始 URL、采集时间、内容哈希这些字段在数据库层是NOT NULL强制的——也就是说,系统从结构上不允许出现"来路不明的数据"。
第二,数据集可以完整重建。批次 CSV 随仓库分发(data/batch_1~3.csv),任何人不调用一次大模型就能从零重建整个数据集。这一点很重要:统计结果不依赖"你相信我",你可以自己跑一遍验证。
这里有个我自己很在意的设计:样本量少于 30 条时,系统拒绝出统计结果,界面上直接显示"数据不足",而不是给你一个基于 5 个样本的百分比。
宁可页面是灰的,也不给一个不可信的数字——这是一条产品底线,不是技术限制。
四、LLM 抽取:为什么必须保留原文证据
从 JD 里抽取技能,是整个链路里唯一必须用 LLM 的地方——因为岗位描述是自然语言,格式千变万化。
但这里有个容易被忽略的点:抽取结果必须能回到原文。
所以每一次抽取,除了得到"这个岗位要求 LangGraph、Python、SQL"之外,还要得到每一条技能对应的原文片段引用。这样界面上才能做到:你点任何一个技能,都能看到它是从 JD 的哪句话里抽出来的。
技术上,输出走 JSON 结构化格式,并且必须通过 schema 校验才能入库。校验不通过时,系统明确标记这次抽取失败,不做兜底猜测——因为一条凭空生成的技能,会污染后面所有的统计。
这套抽取的评测结果是:F1 = 0.8644(N = 53 条标注),证据保留率 100%。
五、统计与评分:为什么一行 LLM 都不许用
技能抽取完成之后,剩下的全是数学问题:
- 市场技能频率 = 纯 SQL 统计;
- 个人技能画像的置信度 = 证据加权的纯函数;
- Skill Gap 量化 = 个人水平与市场要求的差值;
- 匹配分 = 四个维度的加权拆解;
- 学习优先级 =
Demand × Gap ÷ Cost(市场需求 × 能力差距 ÷ 学习成本)。
这些全部用 SQL 和纯函数实现,没有一行经过大模型。
好处有三个,都很实际:
- 可复现——同样的输入永远得到同样的输出,可以写测试;
- 可解释——匹配分能拆成四维展示给用户,而不是一个黑盒数字;
- 可回归——改了权重或口径,评测能立刻发现指标漂移。
第 3 点后来救了我一次:项目的评测门禁在 CI 里拦截过一次真实缺陷——评测集引用发生漂移后,匹配相关性指标从0.8277 掉到 0.445,门禁直接exit 1挡住了合并。如果没有这层确定性,这种问题会一直到线上才被发现。
六、Agent 层:让它只负责说话
项目里有一个基于LangGraph的 Career Planner Agent,负责把冷冰冰的数字翻译成"你接下来该怎么做"的叙述。
它的图结构是四个节点:
注意最后那个降级分支:如果校验反复不通过,系统宁可退回模板拼接,也不放一段数字对不对都不知道的文本出去。
这背后是一条硬规则:数值路径对 LLM 零权限。Agent 只能解释,不能改数。
七、怎么证明它好用:三层评测
"能用"和"好用"之间差着一套评测。我给这个项目建了三层,各自回答不同的问题:
评测 | 回答什么问题 | 规模 | 结果 |
E1 技能抽取 | 抽取准不准? | N=53 | F1 = 0.8644,recall = 0.8173 |
E2 岗位匹配 | 匹配分和人的判断是否一致? | N=25 | Spearman ρ = 0.8277,MAE = 9.67 |
E3 学习推荐 | 推荐的前几条是不是真该先学? | N=5 | nDCG@5 = 0.6497,hit_rate@3 = 1.0 |
三个数字里,E2 的 ρ = 0.8277 是核心——它证明"匹配分"这个最容易变成黑盒的东西,和人工标注判断是强相关的(而且这一层完全没有用 LLM,纯计算)。
三层的详细口径、阈值和方差分析,都写在docs/EVALUATION.md里。
八、工程实现
部分 | 技术选型 | 为什么 |
后端 | FastAPI | 13 个契约端点,接口先行 |
前端 | Jinja2 服务端渲染 + 原生 JS | 六页 Dashboard,零框架零构建零 CDN——单人项目,少一层依赖少一分维护 |
存储 | PostgreSQL 16 + pgvector | 结构化关系与向量检索放一个库,避免两套数据源 |
Agent | LangGraph | 状态机式编排,流程可控可测 |
部署 | Docker Compose | 三条命令从 clone 到浏览器 |
空库启动时市场页会显示"数据不足"(这是预期行为),导入演示数据即可看到完整视图:
项目规模:提交 160+ 次,531 项测试,CI 全绿。
九、这个项目的局限
技术文章最容易犯的毛病是把项目说得完美。以下都是真实存在的边界,写在 README 里,也写在这里:
- 全球市场没有数据:海外数据源的通道代码就绪,但还没拉取,global 市场样本量为 0,页面上是灰态;
- 单用户无鉴权:默认绑定本机地址,任何公网部署之前必须先加认证与限流;
- 评测集规模有限:E1 53 条 / E2 25 条 / E3 仅 5 条——这些数字只支撑版本之间的相对回归比较,不支撑任何绝对能力宣称。E3 的 5 条样本尤其小,只能算流程验证;
- 岗位数据规模:202 条,统计切片在样本量不足时会拒绝出数;
- 简历输入目前是纯文本:PDF 解析还没做。
我特意把这段放在正文里,是因为一个项目敢说清自己的边界,比多两个功能更让人相信它前面的数字。
十、写在最后
这个项目里我最有感触的一点是:AI 应用开发的难点不在"接上模型",在"知道哪些事不该交给模型"。
统计不该交给模型,因为它必须可复现; 结论不该交给模型,因为它必须可追溯; 数据不足的时候更不该交给模型,因为它一定会给你一个看起来合理的答案。
把该确定的事情做成确定的,把该解释的事情交给模型解释——这是我做这个项目最大的收获。
相关链接
- GitHub:https://github.com/Xhan-985/SkillGap-Agent
- 60 秒演示视频:https://github.com/Xhan-985/SkillGap-Agent/releases/download/v1.0.0/skillgap-explainer.mp4
- B 站完整演示:求职工具的匹配分到底怎么算的?——我做了一个每个数字都能点开看证据的开源项目
如果这个项目对你有帮助,欢迎 Star 或提 Issue。也欢迎在评论区说说你所在方向的技能需求情况——如果你愿意提供样本,我可以把它纳入下一版数据集。