尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

基于Jev与Vercel AI Gateway的AI简历匹配工具实战

发布时间:2026/9/26 14:19:41

资讯中心
01
ARTICLE

基于Jev与Vercel AI Gateway的AI简历匹配工具实战

基于Jev与Vercel AI Gateway的AI简历匹配工具实战
招人最花时间的其实不是面试是筛简历。我最近实在受不了人工过几百份简历的折磨就动手做了个小工具让 AI 先把简历和 JD 过一遍输出匹配分数、关键点对齐情况和差距分析。模型选的是 Jev接入层用了 Vercel AI Gateway。这套组合跑通之后效果比我预期的好中间也踩了不少文档里查不到的坑。这篇文章就把整个实战从头到尾拆一遍为什么选 Jev Vercel AI Gateway、网关怎么配、Prompt 怎么写、接口怎么调通以及那些实际运行中才会遇到的问题。如果你正打算做类似的 AI 应用或者想把多个模型统一接进一个网关来管理这篇实战记录可以直接拿来抄作业。1. 从需求到选型为什么是 Jev Vercel AI Gateway1.1 简历匹配到底在解决什么问题之前团队每轮招聘都要收几百份简历初筛基本靠人工几个人背对背看标准很难统一。有人盯着学校背景有人只看工作年限有的人扫一眼技术栈就过了导致最终进入面试的人选高度依赖筛简历那个人的主观偏好。我想要的不是简单关键词命中而是让模型真正理解两份文本之间的语义关系候选人做过什么、JD 要求什么、哪些是强匹配、哪些有明显差距。这个需求拆开来看其实很清晰。输入有两个一份简历、一份职位描述。输出最好是一份结构化结果包含总分、各维度评分、匹配项列表、缺失项列表和改进建议。整个工具要能批量跑也能单条快速出结果。非功能需求同样重要调用成本不能失控、延迟在可接受范围内、API 密钥不能暴露在前端、以后想换更好的模型时业务代码不用大改。1.2 为什么用 Gateway 而不是直连模型最初我也想过直接在前端或者一个 Node 脚本里调用模型 API简单直接。但一旦考虑到这可能是一个长期维护的内部工具直连的问题就暴露了模型厂商一换所有调用代码都得跟着改同一个模型来回传入相同的简历和 JD每次都重新计费请求一多还会触发限流没有统一的兜底策略。Vercel AI Gateway 说白了就是在模型厂商和你之间加了一层路由管理的代理把上面这些问题集中解决掉。对比项直连模型 API通过 Vercel AI Gateway模型切换改代码、改依赖网关后台改配置代码不动缓存自己实现网关自带相同请求直接命中限流与重试每个供应商规则不同要自己写统一配置日志观测需要自己埋点网关侧有日志密钥安全容易暴露在前端环境密钥都收在网关侧网关的缓存机制是省钱的关键。同一份简历和同一份 JD 短时间内重复请求网关会直接返回缓存结果不会再往后端模型发起计费请求。内部工具里这类重复查询其实不少比如同一个岗位多轮沟通时反复跑同一批简历缓存一开成本能明显降下来。1.3 Jev 模型在这个场景里的定位Jev 在整个系统里扮演的是推理引擎角色。简历匹配这种事模型要能理解长文本、能按指令输出结构化内容还要有足够稳定的指令遵循能力。我在选型时核心关注三点一是能不能通过 API 访问二是返回的 JSON 结构稳不稳定三是中文简历和 JD 的理解能力。最终选了 Jev主要是看中它在长上下文和中文语义理解上的表现而且它提供了 OpenAI 兼容的 API 端点接入成本很低。先别纠结模型到底开不开源只要它有可用的 API 就能接入到项目里。Jev 的具体开源信息可以去看官方仓库这里我按通过网关路由 Jev 模型的思路来操作。把 Jev 配在网关后面业务代码里不直接写死某一家的 SDK而是统一走网关的 OpenAI 兼容接口这样以后想换成其他模型改网关配置就够了代码一行不动。2. 环境准备与网关配置2.1 账号、密钥与项目初始化先说准备工作。你需要一个 Vercel 账号这是使用 AI Gateway 的前提。另外去 Jev 的官方控制台申请一个 API Key这是模型侧的真实凭证。两个密钥都要保管好后面一个配在网关里一个用在网关调用上别混。项目我用 Next.js 来搭App Router 模式。初始化命令很简单npx create-next-applatest resume-matching --typescript --eslint --app进入目录后安装依赖。核心就两个OpenAI 客户端用来调网关和 pdf-parse用来解析 PDF 简历。顺手把 dotenv 也装上方便本地跑的时候管理环境变量。cd resume-matching npm install openai pdf-parse dotenv如果你只需要处理纯文本或手动粘贴的简历pdf-parse 可以暂时不装。但我强烈建议加上因为实际场景里收到的简历八成是 PDF 格式。2.2 在 Vercel 控制台创建 Gateway打开 Vercel 项目进入 Storage 或 AI 相关菜单找到 AI Gateway。创建网关时会让你给它起个名字这一步没什么讲究叫resume-matcher或者cv-gateway都行。创建完之后下一步是配置模型供应商 Provider也就是把你申请的 Jev API Key 填进去并设置路由规则。关键点来了Gateway 支持标准 OpenAI 兼容端点所以我把 Jev 配成了一个类 OpenAI 的 Provider模型名随便起一个内部别名比如jev。调用的时候就是用这个名字来访问。配置完成后Vercel 会给你分配一个专属的 Base URL 和 Gateway API Key这两样就是业务代码里真正要用的。注意Gateway 的 Base URL 和 Gateway API Key 是访问代理层的凭证它们和你最初申请的 Jev API Key 不是一回事。Jev 的原始 Key 只需要在网关后台保存永远不要出现在业务代码或前端环境里。2.3 环境变量梳理在项目根目录创建.env.local把网关信息填进去。我习惯把变量名拆成模型名、Base URL、Key三段这样自己在代码里一目了然。JEV_MODELjev JEV_BASE_URLhttps://gateway.vercel.ai/v1 JEV_API_KEY你的_gateway_key这里有一个非常容易踩的坑JEV_API_KEY填的是 Gateway 自己的 Key不是 Jev 原始 Key。我一开始填反了结果请求返回 401排查了半小时才发现。把这两层密钥关系理清楚后面的流程就顺了。3. 简历匹配的核心设计与 Prompt 工程3.1 简历文本抽取与预处理简历输入这个环节看起来简单实际上坑最多。PDF 简历格式五花八门有的是两个栏位有的是表格有的是扫描版图片。pdf-parse只能处理文本型 PDF遇到扫描件就是一堆空字符。所以我在工具里做了三层兜底支持直接粘贴纯文本、支持上传 .txt、支持上传 PDF。PDF 解析失败时提示用户改用粘贴不阻塞主流程。import fs from fs; import pdf from pdf-parse; export async function extractTextFromPdf(buffer: Buffer): Promisestring { const data await pdf(buffer); const text data.text.replace(/\n{3,}/g, \n\n).trim(); if (text.length 50) { throw new Error(PDF 无法提取文本可能是扫描件请改为粘贴文本); } return text; }文本预处理的核心是控制长度。简历动辄几千字加上职位描述直接涌进模型Token 数会迅速膨胀成本上升而且响应变慢。我的做法是统一截断简历正文到 3000 字左右职位描述保留完整然后在 Prompt 里明确告诉模型如果内容被截断基于已有信息分析。这样既能有效控制成本也不会因为截断导致关键信息全丢。3.2 Prompt 设计如何让模型稳定输出结构化 JSON简历匹配这种任务Prompt 设计直接决定结果质量。我一开始用的是开放式提问让模型分析一下匹配度结果输出的东西五花八门有的写一大段散文有的用 Markdown 列表有的给了表格根本没法程序化处理。后来我把 Prompt 改成严格的结构化约束结果稳定了很多。系统提示词的核心内容是这样的你是一名资深的简历筛选专家。你的任务是根据职位描述分析简历匹配度。 只输出 JSON不要输出任何其他内容。JSON 结构如下 { total_score: 0-100, dimensions: { experience: 0-100, skills: 0-100, education: 0-100 }, matched_points: [强匹配点1, 强匹配点2], missing_points: [缺失或不足点1], suggestion: 一句话总结与建议 } 评分标准技术栈直接匹配加分项目经历与岗位职责高度相关加分 候选人在相关领域有完整项目落地经验加分学历信息缺失时不扣分 但要在 missing_points 中注明。用户提示词分两部分拼接。前半部分是职位描述后半部分是简历原文。同时要求模型在给出分数时附带理由比如total_score是 82就要在matched_points或missing_points里体现依据。这个分数依据的联动约束很重要能防止模型乱给分。温度参数我调到了 0.2。简历匹配不需要创造性越低越稳定。另外我明确要求模型只输出 JSON不开任何 Markdown 代码块包裹。实践下来这个参数设置能显著减少后续 JSON 解析失败的次数。3.3 匹配结果的解析与展示模型返回的 JSON 字符串我用JSON.parse直接解析。如果解析失败我会做一个兜底从返回文本中提取第一个{到最后一个}之间的子串再试一次。这个方案虽然有点粗暴但在实际运行中命中率挺高能扛住模型偶尔多输出一个解释性句子的情况。export function parseJsonLoose(text: string): Recordstring, unknown { try { return JSON.parse(text); } catch { const start text.indexOf({); const end text.lastIndexOf(}); if (start ! -1 end ! -1 end start) { return JSON.parse(text.slice(start, end 1)); } throw new Error(Not JSON); } }展示层面我做了两个东西一个直观的分数条和维度雷达图以及一个匹配点/缺失点对照列表。分数的意义在于让 HR 能快速排序匹配点和缺失点才是真正的价值招聘的人一眼就能看出候选人强在哪、弱在哪。我还在结果页加了一行直接给建议方便 HR 决定是约面试还是婉拒。4. 手写简历匹配接口与完整实现4.1 项目结构与接口设计我按业务功能把代码分成了几个模块结构非常清晰resume-matching/ app/ page.tsx # 前端页面 api/ match/ route.ts # 简历匹配接口 lib/ prompt.ts # Prompt 拼接逻辑 pdf.ts # PDF 文本抽取 json.ts # JSON 宽松解析API 路由设计成 POST 接口接收 Multipart 表单数据包含两个字段job职位描述和resumeFile简历文件可选同时也支持resumeText字段直接传文本。接口内部做四件事解析上传文件、拼 Prompt、调网关拿模型结果、格式化返回。4.2 API 路由的完整实现先看核心接口代码。我用的是 OpenAI 客户端指向 Vercel AI Gateway 的 Base URL。因为 Gateway 提供 OpenAI 兼容端点所以不需要额外的 SDK 适配。import { NextRequest, NextResponse } from next/server; import OpenAI from openai; import { extractTextFromPdf } from /lib/pdf; import { buildPrompt } from /lib/prompt; import { parseJsonLoose } from /lib/json; const client new OpenAI({ apiKey: process.env.JEV_API_KEY, baseURL: process.env.JEV_BASE_URL, }); export async function POST(req: NextRequest) { try { const form await req.formData(); const job form.get(job) as string; const resumeText (form.get(resumeText) as string) || ; const file form.get(resumeFile) as File | null; if (!job || !job.trim()) { return NextResponse.json({ error: 职位描述不能为空 }, { status: 400 }); } let resume resumeText; if (file resume.length 0) { const buffer Buffer.from(await file.arrayBuffer()); resume await extractTextFromPdf(buffer); } if (!resume || resume.trim().length 50) { return NextResponse.json( { error: 简历内容太短请提供完整的简历文本 }, { status: 400 } ); } const messages buildPrompt({ job, resume }); const completion await client.chat.completions.create({ model: process.env.JEV_MODEL || jev, messages, temperature: 0.2, max_tokens: 1500, response_format: { type: json_object }, }); const content completion.choices[0].message.content || {}; const result parseJsonLoose(content); return NextResponse.json({ result }); } catch (error) { const msg error instanceof Error ? error.message : unknown error; return NextResponse.json({ error: msg }, { status: 500 }); } }这个接口有三处细节值得展开说一下。第一response_format: { type: json_object }必须开。虽然网关背后接的是 Jev走 OpenAI 兼容协议时这个参数普遍有效它会从底层约束模型输出合法 JSON。这个参数加上之后模型的输出稳定性上了个台阶。第二max_tokens设成 1500 是经过考量的。一个完整匹配结果 JSON 一般在 300 到 800 Token 之间留到 1500 是为了防止模型在matched_points和missing_points里过度展开写一大堆。如果要处理更长的分析可以适当增加到 2000但没必要无脑拉高。第三错误处理必须兜全。PDF 解析失败、网关超时、JSON 解析失败属于三类不同异常接口统一返回结构化的错误体前端才能准确展示错误原因。exposure 这些信息给用户也能快速判断是模型问题还是输入问题。4.3 Prompt 拼接逻辑buildPrompt函数看起来简单但有几行代码决定了结果的稳定程度。我的实现是import type { ChatCompletionMessageParam } from openai/resources/chat/completions; export function buildPrompt({ job, resume, }: { job: string; resume: string; }): ChatCompletionMessageParam[] { return [ { role: system, content: 你是一名资深简历筛选专家。请你严格根据职位描述和简历内容评估匹配度。 只输出 JSON不要输出任何多余文字不要用 Markdown 代码块包裹。 评分字段必须为数字 0-100。, }, { role: user, content: 以下是职位描述\n${job.slice(0, 2000)}\n\n以下是候选人简历\n${resume.slice(0, 3000)}, }, ]; }这里我做了两个截断职位描述最多 2000 字简历最多 3000 字。对于绝大多数职位和简历这个长度完全够用而且能避免因输入过长导致成本飙升和响应时间拉长。如果截断后关键内容被切掉模型也会因为忠实于文本而产生一定的信息缺失所以截断的策略是在边界处做行的自然截断不要硬切。4.4 前端页面的简易实现前端没有用太复杂的东西。核心就是一个表单左边粘贴职位描述右边上传简历文件或者粘贴简历文本点击开始匹配按钮页面请求/api/match接口把结果渲染到卡片上。use client; import { useState } from react; export default function Home() { const [job, setJob] useState(); const [resumeText, setResumeText] useState(); const [file, setFile] useStateFile | null(null); const [loading, setLoading] useState(false); const [result, setResult] useStateany(null); const [error, setError] useState(); async function onMatch() { setLoading(true); setError(); try { const form new FormData(); form.append(job, job); form.append(resumeText, resumeText); if (file) form.append(resumeFile, file); const res await fetch(/api/match, { method: POST, body: form }); const data await res.json(); if (!res.ok) throw new Error(data.error || 请求失败); setResult(data.result); } catch (e) { setError(e instanceof Error ? e.message : 请求失败); } finally { setLoading(false); } } return ( main style{{ maxWidth: 900, margin: 0 auto, padding: 32 }} h1简历匹配工具/h1 textarea value{job} onChange{(e) setJob(e.target.value)} placeholder粘贴职位描述 rows{6} style{{ width: 100% }} / textarea value{resumeText} onChange{(e) setResumeText(e.target.value)} placeholder粘贴简历文本或上传 PDF rows{10} style{{ width: 100% }} / input typefile accept.pdf,.txt onChange{(e) setFile(e.target.files?.[0] || null)} / button onClick{onMatch} disabled{loading} {loading ? 匹配中... : 开始匹配} /button {error p style{{ color: red }}{error}/p} {result ( div h2总分{result.total_score}/h2 p经验{result.dimensions?.experience}技能{result.dimensions?.skills}学历{result.dimensions?.education}/p h3强匹配点/h3 ul{(result.matched_points || []).map((it: string, idx: number) li key{idx}{it}/li)}/ul h3缺失点/h3 ul{(result.missing_points || []).map((it: string, idx: number) li key{idx}{it}/li)}/ul p{result.suggestion}/p /div )} /main ); }页面设计完全面向 HR 使用习惯不做花哨交互。提交后如果模型还在响应按钮会禁掉避免重复提交重复计费。这看起来是个小细节实际用下来很有帮助——HR 在结果出来前反复点按钮的情况太常见了网关有缓存还好没缓存就是真金白银。4.5 部署验证的完整流程代码写完之后部署到 Vercel 很简单。先把我本地.env.local里的三个变量原样配到 Vercel 项目的 Environment Variables 里然后 push 到 Git 仓库Vercel 自动识别 Next.js 项目并构建。部署完验证顺序建议这样走先用 curl 直接测接口确保后端通。curl -X POST https://你的项目.vercel.app/api/match \ -F job招聘一名前端工程师要求熟悉 React、TypeScript、有性能优化经验 \ -F resumeText我使用 React 开发过三个项目熟悉 TypeScript做过首屏性能优化预期返回结果里total_score应该偏高matched_points里会列出 React 和 TypeScript 匹配。确认接口没问题后再用页面做真实文件上传测试重点验证 PDF 解析环节是否稳定。5. 实战中的问题与排查记录5.1 高频问题速查表现象原因解决方式401 UnauthorizedGateway Key 填错或者填成 Jev 原始 Key检查环境变量确认填的是网关自己的 Key400 Bad Request入参缺失或简历文本过短检查前端是否完整传入 job 和 resumeText模型返回非 JSONPrompt 约束不足或响应格式参数没开开启 response_format温度调到 0.2 以下JSON 解析失败模型在 JSON 外输出了解释文字使用宽松解析提取首尾大括号PDF 解析为空简历是扫描件或图片型 PDF提示用户改为粘贴文本请求超时输入过长导致 Token 数过多在 Prompt 拼接时控制文本长度5.2 三个最容易翻车的地方第一个坑是环境变量错位。这个我已经说过了但值得再强调一遍JEV_API_KEY必须填网关的 KeyJEV_BASE_URL必须指向网关端点。如果你的前端代码里出现了 Jev 的原始 API Key那就是重大安全隐患网关的存在意义就没了。自查的标准很简单登录 Vercel 后台找到 Gateway 页面展示的那个 Key和本地环境的比对一下。第二个坑是响应格式不稳定。哪怕开了response_format: { type: json_object }边界情况下模型依然可能给你一段破 JSON比如某个字符串值里带了没转义的单引号。我的经验是双保险既要开参数又要在代码里做宽松解析。两件事分开看前者提高正常情况的稳定性后者兜住异常情况缺一不可。第三个坑是 PDF 解析质量参差。pdf-parse对纯文本型 PDF 效果不错但遇到分栏简历的时候读出来的文本顺序可能是乱的第二栏的内容会混到第一栏后面。这直接影响模型理解简历的结构。我的方案是在 PDF 解析结果里做一个分行整理尽量按空行分段清洗一遍虽然不能根治分栏问题但能显著降低混乱程度。5.3 成本与性能的实测观察整体跑下来的体感是单次匹配请求平均耗时在 4 到 8 秒之间其中大部分时间花在模型生成上网关本身带来的额外延迟几乎可以忽略。开启缓存后相同简历和 JD 的重复请求耗时直接降到几十毫秒级别成本也趋近于零。给个人开发者一个成本控制的建议给网关配置一个偏保守的 Rate Limit比如每分钟 30 次。内部工具完全够用还能防止某个同事顺便写个脚本把你的 Key 当公共 API 刷。之前我就在日志里看到过短时间内几百次请求的异常记录幸亏有网关的限流规则兜底不然账单就难看了。我个人实操下来最满意的一点是项目结构没有和特定模型 SDK 强绑定全部通过网关走 OpenAI 兼容协议。下次如果发现 Jev 在某个任务上表现不佳换模型只需要在网关后台加一个新的 Provider然后改一个环境变量JEV_MODEL就完事。这也是我用 Vercel AI Gateway 来做这个项目最核心的原因业务逻辑永远是稳定的模型可以灵活替换。最近我还在考虑把网关这套方案用到另一个场景上给团队的周报做自动摘要和待办提取目前看架构完全可以复用只需要换一套 Prompt 而已。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。