两周前,几个同事在聊天群里兴奋地分享新玩法——把Claude Code和各自的年度回顾报告凑到一起,只为了搞清楚一个问题:“我到底是哪只宝可梦?”他们说,大语言模型不光能回答你是谁,还能正经八百地从初代御三家到传说宝可梦里,列出几只和你性格最像的,附带一串让你无法反驳的理由。我在对话框这头笑得停不下来,脑子里却已经调了一个方向:这件事能不能在我那台只有Core i5、8G内存、连独显都没有的旧笔记本上跑起来?而且从头到尾,只用纯开源组件。
这个念头的后半句才是真正的约束。如果只是调一个商业API,敲几行Prompt,谁都能做。但我想知道的是,完全不依赖外部算力、不调用任何付费模型、也不需要GPU来推理,能否从一份简历文本和宝可梦百科数据出发,用嵌入(Embeddings)和一点传统的机器学习手法,把这个匹配系统完整地做出来。带着这种纯本地折腾的执念,我打开了PokeAPI的文档,开始尝试第一版原型。
## 数据层:把宝可梦翻成一份可检索的职业档案
第一步是搭好宝可梦侧的语料。我写了一个脚本 build_dataset.py,它直接拉取整个PokeAPI上每一只宝可梦的基础信息——名字、属性、六维种族值、精灵图、所属种类,以及英文图鉴里的风味文字(Flavor Text)。但光有这些战斗数值还不够,简历里可不会写“特攻135、速度110”。我需要的是一套能把怪物生物学和人设职业兴趣打通的语言层。
于是脚本自动为每只宝可梦生成了一段“职业原型档案”。规则并不复杂:根据属性的组合和种族值分布来转换成人类职场对应的形容词句。比方说,钢属性会对应“一位自律、精确、稳健的结构系统工程师”,而虫加飞行的组合可能会被写成“适应力强、敏捷、擅长多线切换的项目协调者”。属性本身隐含的象征意被抽了出来,变成一段工程师、分析师、策略师们能对号入座的人格描述。再加上原始图鉴文字里那些更富故事性的背景说明,每只宝可梦就同时拥有了“物性参数”和“性格侧写”。
这一步真正关键的地方在于统一向量空间。我把每只宝可梦的描述和职业档案拼接起来,用 BAAI/bge-m3 模型生成嵌入向量,然后将这些向量跟宝可梦的结构化元数据一并打包,存成 data/pokemon.json 和 data/embeddings.npy 两个文件。此后整个应用再也不会去实时调 PokeAPI,所有匹配计算都离线完成,完美符合我那台机器“别联网也别吃内存”的底线。
## 简历侧:只取能表达“倾向”的语义信号
有了宝可梦的静态知识库,下一个问题是怎么把一份简历转成可对比的同一种嵌入。我的第一份测试样例就是自己在 registry.jsonresume.org 上已经公开的标准化简历。并不是简历里每一项都要用。在教育背景、证书、语言能力这些相对行政化的部分上,大部分人同质化严重,很难榨出什么独特人格信号。反而是概述(basics.summary)、技能列表、工作经历、志愿经历、项目经验、兴趣这几块,背后藏着真实的行为偏好和职业气质。
我把这些筛选后的段落分别拆成简短的自然语言句子,每一句都可以单独作为一条“语义事实”。比方说,项目经验里“主导了一次从单体服务到微服务架构的迁移”就会变成一条独立短句,而不是一大段块状文本。这样做既能让后续检索的粒度更细,也能避免简历整体太长被嵌入模型平均化后信息淹没。所有切好的短句同样喂给 BAAI/bge-m3,跑出和宝可梦知识库完全同构的嵌入向量。到这里,两边的数据其实已经能在同一个高维空间里互相对望了。
## 检索与重排序:余弦相遇,交叉编码器再打分
最直觉的做法是算余弦相似度,从七百多只宝可梦里捞出和简历最接近的十几只。整个短名单靠一顿高效的矩阵乘法就能出来,对没有GPU的笔记本来说毫无压力。但余弦检索有一个老毛病:它能发现大致相关的东西,却很难精排。一只水属性的宝可梦也许只是因为“水”这个字和简历中某句“数据流处理”的共性被拎进来,但真正匹配度未必高。
所以我把这十几只短名单送进了一个交叉编码器(Cross-Encoder),具体模型是 BAAI/bge-reranker-v2-m3。和嵌入模型只看单句不同,交叉编码器会把(简历片段,宝可梦描述)作为句子对一并输入,给出一个更深层的联合相关性分数。这一步对于多属性、多面相的宝妈妈、多才多艺的图图犬这类角色尤其重要——纯余弦检索可能捞它进来却解释不出它为什么排第一,而重排序后,只有真正和简历里多个工作主题产生交集的那几只,分数才会冲上来。
解释性也跟着自然落地。引用匹配环节,我把得分最高的简历片段和对应宝可梦的职业档案、图鉴原文并排抽出。你可以直接看到,为什么这只宝可梦出现在你列表里:是因为你简历上写的那句“喜欢在快节奏环境中搭建原型”,刚好撞上了它档案里那句“快速进化、善于在多领域之间交叉迁移”。这种用原文引用的方式做解释,比凭空生成的文案可靠得多,也符合我一开始就想严守的原则——任何结论都只基于已经存在的数据,不额外编造人格。
## 排名信号:如何让第一名看起来就是第一名
实际跑起来后,我发现一个明显的问题:一份信息量大的简历会同时和许多宝可梦产生中等相似度,导致余弦分数和重排序分数挤在一个很窄的区间里。比如第一名0.89分,第二名0.87分,第十名可能还有0.82分。把这种原始分数直接展示给用户,会让“最适合我的那只”显得毫无统治力。我不是在做分类器,而是在做一个发现工具,需要让差异肉眼可读。
于是我引入了一个纯粹后置的映射手段——相对适合度。具体做法是把短名单上所有候选宝可梦的重排序分数先做一次标准化(z-score),计算出每只宝可梦在这个小池塘里的相对位置,然后通过一个sigmoid函数把分布“拉开”,让最匹配的那只稳定地趋近100%级别,而后面几名的得分平滑但快速地往下掉。它不是绝对概率,而是一个“相对池里的相对适合值”。这意味着,如果你的简历非常聚焦,第一名甚至会以超过99%的相对适合度单独霸榜;如果你的职业背景跨了好几个领域,那么可能前几名都分布在60%~75%之间,也能直观反映出一个“多类型体质”的信号。
类型判定也从同一个重排序后的短名单里导出,而不是单独再做一次类型分类。我会统计短名单里所有宝可梦的属性出现频次,摘取两个最具代表性的作为“最佳匹配类型”。这样保证类型标签和展示出来的宝可梦排名形成自洽闭环,不会出现“排名第一的妙蛙种子,但属性推荐却是火+飞行”的尴尬矛盾。
## 置信度读数:告诉你这个匹配有多“决定性的”
最后再加一层心理预期管理。很多这类人格匹配工具会给一个笼统的“匹配度”,用户看了也不清楚这分数算高还是算低。所以我在最终卡片上额外展示了一个校准置信度的读数——本质上是顶级匹配的标准化得分与短名单内其余候选之间的离群程度。用一个简单的z分数值来标志:如果最佳匹配远远甩开第二名,那就是一个“决定性匹配”,你可以比较放心地对号入座;如果分数曲线平缓,前几名难分伯仲,它也会诚实标示为“分散型多属性轮廓”,暗示你或许就是那种水火草系特质混合的神奇存在。
这个信号对玩法体验意外的有用。测试时我拿自己那份简历跑了几轮,有时跳出来一只“钢+超能力”的巨金怪,置信度很高,理由是我的简历里大量出现系统架构和逻辑推理的线索;换一份更偏运营的简历,前几名就在雷丘、胖可丁之间挤得很密,系统很坦率地标注“分散轮廓”。这种透明度的微妙提升,正好是我在整个原型里不断想保留的东西——所有算法解释都是可回溯的,任何一句“为什么是我”都能找到具体的原文佐证。
整个流程跑在我那台不起眼的i5笔记本上,从加载数据集到最终排完,一次匹配不到几秒。它没有用任何千亿参数模型,也不需要API密钥,甚至断网了照样能玩。我觉得这个原型最迷人的地方,不是它能告诉你自己是哪只宝可梦,而是它以一个很轻量的工程路径,演示了如何把一份完全不相关的知识库(PokeAPI)和个人职业生涯的文本,通过嵌入与重排序的串联,变成一个说得通、看得见、还能自圆其说的发现体验。对那种总想“哪怕不联网也能造点什么”的人来说,这种纯粹性本身就有点浪漫。
热门跟贴